Preview Environments with Bitbucket Pipelines: Automated Per-PR Deployments with Bunnyshell
Why Integrate Bunnyshell with Bitbucket Pipelines?
Your team uses Bitbucket for source control and Pipelines for CI/CD. Code reviews happen in pull requests. But reviewing code in a diff is only half the picture — reviewers need to see the running application with the PR's changes to catch layout regressions, broken API flows, and integration issues that no diff can reveal.
Bunnyshell creates isolated preview environments for every pull request — a full-stack deployment with your app, database, cache, and any other service — running in Kubernetes with a unique HTTPS URL. The question is: how do you connect Bunnyshell to your Bitbucket workflow?
Two options:
| Approach | Pipeline changes | Control level | Best for |
|---|---|---|---|
| Approach A: Webhook-Only | None | Bunnyshell manages everything | Teams that want the fastest setup |
| Approach B: Pipeline Integration | Add steps to bitbucket-pipelines.yml | Full control over lifecycle | Teams needing custom steps (migrations, seeds, tests) |
Both approaches produce the same result: an isolated environment per PR with automatic cleanup on merge.
How It Works
Webhook-Only Flow
1PR opened → Bitbucket webhook → Bunnyshell API
2 ↓
3 Create environment
4 Build images
5 Deploy to K8s
6 ↓
7 Post comment on PR
8 with preview URLBunnyshell registers a webhook on your Bitbucket repository. When a PR is opened, updated, or closed, Bitbucket sends events directly to Bunnyshell. No pipeline involvement.
Pipeline-Triggered Flow
1PR opened → Bitbucket Pipeline triggers
2 ↓
3 Pipeline step installs Bunnyshell CLI
4 CLI creates/deploys environment
5 CLI retrieves preview URL
6 ↓
7 Post-deploy steps (migrations, seeds, tests)
8 ↓
9 Pipeline posts preview URL as PR commentYour bitbucket-pipelines.yml controls when and how Bunnyshell environments are created, giving you the ability to run custom commands before or after deployment.
Prerequisites
Before you begin, ensure you have:
- A Bunnyshell account — Sign up at bunnyshell.com
- A Bunnyshell project and primary environment — At least one environment deployed and in Running or Stopped state
- A Bitbucket repository with Pipelines enabled (Settings > Pipelines > Enable Pipelines)
- A Bunnyshell API token — Generate one in Bunnyshell under Settings > API Tokens
- Your Bunnyshell IDs — You'll need your Organization ID, Project ID, Environment ID, and Cluster ID (find these in the Bunnyshell dashboard URLs or via CLI)
If you haven't set up a Bunnyshell environment yet, follow one of the framework-specific guides first (e.g., Laravel, Django, Express) to create your primary environment. This guide focuses on the CI/CD integration layer.
Approach A: Webhook-Only (Zero Pipeline Config)
This is the fastest path. Bunnyshell handles everything — no bitbucket-pipelines.yml changes required.
Step 1: Connect Your Bitbucket Repository
- Log into Bunnyshell
- Navigate to your Project and select your primary environment
- Go to Settings > Git Repository
- If not already connected, add your Bitbucket integration under Settings > Integrations > Bitbucket
- Authorize Bunnyshell to access your Bitbucket workspace
Bunnyshell uses Bitbucket's OAuth to read repository data and register webhooks.
Step 2: Enable Automatic Preview Environments
- In your environment, go to Settings
- Find the Ephemeral environments section
- Toggle "Create ephemeral environments on pull request" to ON
- Toggle "Destroy environment after merge or close pull request" to ON
- Select the target Kubernetes cluster for ephemeral environments
Step 3: Verify the Webhook
After enabling, Bunnyshell automatically adds a webhook to your Bitbucket repository:
- In Bitbucket, go to Repository Settings > Webhooks
- You should see a Bunnyshell webhook with these events:
pullrequest:createdpullrequest:updatedpullrequest:fulfilled(merged)pullrequest:rejected(declined)repo:push
Step 4: Test It
- Create a new branch and make a change
- Open a pull request in Bitbucket
- Bunnyshell automatically creates an ephemeral environment cloned from your primary environment, using the PR branch
- Within a few minutes, Bunnyshell posts a comment on the PR with the preview URL
- Merge or decline the PR — the environment is automatically destroyed
The primary environment must be in Running or Stopped status. If it's still deploying or in an error state, ephemeral environment creation will fail.
That's it. No pipeline config, no YAML, no tokens to manage. Bunnyshell handles the full lifecycle.
Approach B: Pipeline Integration with Bunnyshell CLI
For teams that need custom pre- or post-deploy steps — database migrations, data seeding, smoke tests, or custom notifications — you can drive Bunnyshell from your bitbucket-pipelines.yml.
Step 1: Store the Bunnyshell Token
- In Bitbucket, go to Repository Settings > Repository variables
- Add a new variable:
- Name:
BUNNYSHELL_TOKEN - Value: Your Bunnyshell API token
- Secured: Check this box (the value will be masked in logs)
- Name:
- Add additional variables for your Bunnyshell resource IDs:
BUNNYSHELL_ORG_ID— Your organization IDBUNNYSHELL_PROJECT_ID— Your project IDBUNNYSHELL_CLUSTER_ID— Target Kubernetes cluster IDBUNNYSHELL_ENV_ID— The primary environment ID to clone from
Always mark BUNNYSHELL_TOKEN as Secured in Bitbucket. Secured variables are encrypted at rest and masked in pipeline logs. Never hardcode tokens in bitbucket-pipelines.yml.
Step 2: Create the Pipeline Configuration
Create or update bitbucket-pipelines.yml in your repository root:
1# bitbucket-pipelines.yml
2image: atlassian/default-image:4
3
4definitions:
5 steps:
6 # ── Create or update a preview environment ──
7 - step: &deploy-preview
8 name: Deploy Preview Environment
9 script:
10 # Install Bunnyshell CLI
11 - curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
12 - export PATH="$HOME/.bunnyshell/bin:$PATH"
13
14 # Set CLI authentication
15 - export BUNNYSHELL_TOKEN=$BUNNYSHELL_TOKEN
16
17 # Derive a unique environment name from the PR
18 - export ENV_NAME="pr-${BITBUCKET_PR_ID}"
19 - echo "Environment name:\ $ENV_NAME"
20
21 # Check if the environment already exists
22 - |
23 EXISTING_ENV=$(bns environments list \
24 --organization "$BUNNYSHELL_ORG_ID" \
25 --project "$BUNNYSHELL_PROJECT_ID" \
26 --search "$ENV_NAME" \
27 --output json 2>/dev/null | \
28 python3 -c "import sys,json; items=json.load(sys.stdin).get('_embedded',{}).get('item',[]); print(items[0]['id'] if items else '')" \
29 ) || true
30
31 - |
32 if [ -n "$EXISTING_ENV" ]; then
33 echo "Environment $ENV_NAME already exists (ID: $EXISTING_ENV). Redeploying..."
34 bns environments deploy \
35 --id "$EXISTING_ENV" \
36 --wait \
37 --timeout 900
38 DEPLOY_ENV_ID="$EXISTING_ENV"
39 else
40 echo "Creating new environment: $ENV_NAME"
41 CREATE_OUTPUT=$(bns environments create \
42 --from-environment "$BUNNYSHELL_ENV_ID" \
43 --name "$ENV_NAME" \
44 --k8s "$BUNNYSHELL_CLUSTER_ID" \
45 --output json)
46 DEPLOY_ENV_ID=$(echo "$CREATE_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
47 echo "Created environment ID: $DEPLOY_ENV_ID"
48
49 # Deploy the new environment
50 bns environments deploy \
51 --id "$DEPLOY_ENV_ID" \
52 --wait \
53 --timeout 900
54 fi
55
56 # Retrieve the preview URL
57 - |
58 PREVIEW_URL=$(bns environments show \
59 --id "$DEPLOY_ENV_ID" \
60 --output json | \
61 python3 -c "
62 import sys, json
63 data = json.load(sys.stdin)
64 endpoints = data.get('endpoints', [])
65 if endpoints:
66 print(endpoints[0].get('url', 'N/A'))
67 else:
68 print('N/A')
69 ")
70 echo "Preview URL: $PREVIEW_URL"
71
72 # Export for subsequent steps
73 - echo "DEPLOY_ENV_ID=$DEPLOY_ENV_ID" >> .env_vars
74 - echo "PREVIEW_URL=$PREVIEW_URL" >> .env_vars
75 artifacts:
76 - .env_vars
77
78 # ── Post-deploy tasks (migrations, seeds, etc.) ──
79 - step: &post-deploy
80 name: Post-Deploy Tasks
81 script:
82 - source .env_vars
83 - curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
84 - export PATH="$HOME/.bunnyshell/bin:$PATH"
85 - export BUNNYSHELL_TOKEN=$BUNNYSHELL_TOKEN
86
87 # Wait for all components to be running
88 - sleep 30
89
90 # Run migrations
91 - |
92 COMPONENT_ID=$(bns components list \
93 --environment "$DEPLOY_ENV_ID" \
94 --output json | \
95 python3 -c "
96 import sys, json
97 items = json.load(sys.stdin).get('_embedded', {}).get('item', [])
98 for item in items:
99 if 'app' in item.get('name', '').lower():
100 print(item['id'])
101 break
102 ")
103 - |
104 if [ -n "$COMPONENT_ID" ]; then
105 echo "Running migrations on component $COMPONENT_ID..."
106 bns exec "$COMPONENT_ID" -- php artisan migrate --force || true
107 bns exec "$COMPONENT_ID" -- php artisan db:seed --force || true
108 fi
109
110 # Post the preview URL as a PR comment
111 - |
112 if [ "$PREVIEW_URL" != "N/A" ]; then
113 curl -s -X POST \
114 -H "Content-Type: application/json" \
115 -u "${BITBUCKET_USERNAME}:${BITBUCKET_APP_PASSWORD}" \
116 "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_FULL_NAME}/pullrequests/${BITBUCKET_PR_ID}/comments" \
117 -d "{\"content\":{\"raw\":\"## Preview Environment Ready\\n\\nYour preview environment is live:\\n\\n**URL:** ${PREVIEW_URL}\\n\\nEnvironment ID: \`${DEPLOY_ENV_ID}\`\\n\\n---\\n_Deployed by Bunnyshell via Bitbucket Pipelines_\"}}"
118 fi
119
120 # ── Destroy preview environment ──
121 - step: &destroy-preview
122 name: Destroy Preview Environment
123 script:
124 - curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
125 - export PATH="$HOME/.bunnyshell/bin:$PATH"
126 - export BUNNYSHELL_TOKEN=$BUNNYSHELL_TOKEN
127
128 - export ENV_NAME="pr-${BITBUCKET_PR_ID}"
129
130 # Find and destroy the environment
131 - |
132 EXISTING_ENV=$(bns environments list \
133 --organization "$BUNNYSHELL_ORG_ID" \
134 --project "$BUNNYSHELL_PROJECT_ID" \
135 --search "$ENV_NAME" \
136 --output json 2>/dev/null | \
137 python3 -c "import sys,json; items=json.load(sys.stdin).get('_embedded',{}).get('item',[]); print(items[0]['id'] if items else '')" \
138 ) || true
139
140 - |
141 if [ -n "$EXISTING_ENV" ]; then
142 echo "Destroying environment $ENV_NAME (ID: $EXISTING_ENV)..."
143 bns environments delete \
144 --id "$EXISTING_ENV" \
145 --force
146 echo "Environment destroyed."
147 else
148 echo "No environment found for $ENV_NAME. Nothing to destroy."
149 fi
150
151pipelines:
152 # ── Trigger on pull requests targeting main/master ──
153 pull-requests:
154 '**':
155 - step: *deploy-preview
156 - step: *post-deploy
157
158 # ── Destroy preview environment on merge to main ──
159 branches:
160 main:
161 - step: *destroy-preview
162 master:
163 - step: *destroy-previewUnderstanding the Pipeline Configuration
Let's break down the key parts:
Pull request trigger:
1pull-requests:
2 '**':
3 - step: *deploy-preview
4 - step: *post-deployThe pull-requests section triggers on every PR, regardless of source branch (the '**' glob pattern matches all branches). Bitbucket Pipelines automatically sets BITBUCKET_PR_ID and BITBUCKET_PR_DESTINATION_BRANCH as environment variables.
To limit preview environments to PRs targeting specific branches, replace '**' with the branch pattern. For example, use main or develop instead of '**' to only trigger on PRs targeting those branches.
Branch filtering for cleanup:
1branches:
2 main:
3 - step: *destroy-preview
4 master:
5 - step: *destroy-previewWhen a PR is merged, the merge commit triggers the branch pipeline. This step finds the environment associated with the PR and destroys it.
Changeset filtering (optional):
If you only want to trigger preview environments when certain files change, add a condition:
1pull-requests:
2 '**':
3 - step:
4 <<: *deploy-preview
5 condition:
6 changesets:
7 includePaths:
8 - "src/**"
9 - "docker/**"
10 - "Dockerfile"
11 - "bunnyshell.yaml"This prevents preview deployments from triggering on documentation-only changes.
Destination branch filtering:
To only create preview environments for PRs targeting main:
1pull-requests:
2 '**':
3 - step:
4 <<: *deploy-preview
5 script:
6 - |
7 if [ "$BITBUCKET_PR_DESTINATION_BRANCH" != "main" ]; then
8 echo "PR targets $BITBUCKET_PR_DESTINATION_BRANCH, not main. Skipping."
9 exit 0
10 fi
11 # ... rest of deploy scriptSecrets and Variables
Repository Variables (Recommended)
Store sensitive values as repository variables in Bitbucket:
| Variable | Secured | Description |
|---|---|---|
BUNNYSHELL_TOKEN | Yes | Bunnyshell API token |
BUNNYSHELL_ORG_ID | No | Organization ID |
BUNNYSHELL_PROJECT_ID | No | Project ID |
BUNNYSHELL_CLUSTER_ID | No | Target K8s cluster ID |
BUNNYSHELL_ENV_ID | No | Primary environment ID to clone from |
BITBUCKET_APP_PASSWORD | Yes | App password for posting PR comments (if using Approach B) |
Workspace Variables
For teams with multiple repositories using Bunnyshell, store shared variables at the workspace level:
- Go to Workspace Settings > Pipelines > Workspace variables
- Add
BUNNYSHELL_TOKENandBUNNYSHELL_ORG_IDhere — they apply to all repositories in the workspace
Deployment Variables
For environment-specific overrides, use deployment variables:
- Go to Repository Settings > Pipelines > Deployments
- Create environments like "Preview" and "Production"
- Add variables scoped to each deployment environment
Bitbucket App Passwords are separate from repository variables. To post PR comments from the pipeline, you need a Bitbucket App Password with pullrequest:write scope. Create one at Personal Settings > App passwords.
PR Comments with Preview URL
In Approach A (webhook-only), Bunnyshell automatically posts a comment on the PR with the preview URL. No additional configuration needed.
In Approach B (pipeline integration), you post the comment yourself using the Bitbucket API. The pipeline example above includes this step, but here's the standalone version:
1# Post a PR comment with the preview URL
2curl -s -X POST \
3 -H "Content-Type: application/json" \
4 -u "${BITBUCKET_USERNAME}:${BITBUCKET_APP_PASSWORD}" \
5 "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_FULL_NAME}/pullrequests/${BITBUCKET_PR_ID}/comments" \
6 -d '{
7 "content": {
8 "raw": "## Preview Environment Ready\n\nYour preview environment is live:\n\n**URL:** '"${PREVIEW_URL}"'\n\nEnvironment ID: `'"${DEPLOY_ENV_ID}"'`\n\n---\n_Deployed by Bunnyshell via Bitbucket Pipelines_"
9 }
10 }'Required variables:
BITBUCKET_USERNAME— Your Bitbucket username (or a service account username)BITBUCKET_APP_PASSWORD— An App Password withpullrequest:writescopeBITBUCKET_REPO_FULL_NAME— Automatically set by Bitbucket Pipelines (e.g.,workspace/repo)BITBUCKET_PR_ID— Automatically set by Bitbucket Pipelines
For cleaner PR comments, consider updating the same comment instead of posting a new one on every push. Use the Bitbucket API to list existing comments, find the Bunnyshell comment by content, and update it with a PUT request.
Pipeline Examples
Deploy on PR Open (Minimal)
A stripped-down version that only creates the environment:
1image: atlassian/default-image:4
2
3pipelines:
4 pull-requests:
5 '**':
6 - step:
7 name: Deploy Preview
8 script:
9 - curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
10 - export PATH="$HOME/.bunnyshell/bin:$PATH"
11 - export BUNNYSHELL_TOKEN=$BUNNYSHELL_TOKEN
12 - export ENV_NAME="pr-${BITBUCKET_PR_ID}"
13 - |
14 bns environments create \
15 --from-environment "$BUNNYSHELL_ENV_ID" \
16 --name "$ENV_NAME" \
17 --k8s "$BUNNYSHELL_CLUSTER_ID"
18 - |
19 bns environments deploy \
20 --name "$ENV_NAME" \
21 --project "$BUNNYSHELL_PROJECT_ID" \
22 --wait \
23 --timeout 900Destroy on Merge
1pipelines:
2 branches:
3 main:
4 - step:
5 name: Cleanup Preview Environments
6 script:
7 - curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
8 - export PATH="$HOME/.bunnyshell/bin:$PATH"
9 - export BUNNYSHELL_TOKEN=$BUNNYSHELL_TOKEN
10 # Destroy all PR environments that reference merged branches
11 - |
12 ENVS=$(bns environments list \
13 --project "$BUNNYSHELL_PROJECT_ID" \
14 --output json | \
15 python3 -c "
16 import sys, json
17 items = json.load(sys.stdin).get('_embedded', {}).get('item', [])
18 for item in items:
19 name = item.get('name', '')
20 if name.startswith('pr-'):
21 print(item['id'])
22 ")
23 - |
24 for ENV_ID in $ENVS; do
25 echo "Checking environment $ENV_ID..."
26 bns environments delete --id "$ENV_ID" --force || true
27 doneDeploy with Custom Build Validation
1pipelines:
2 pull-requests:
3 '**':
4 - parallel:
5 - step:
6 name: Run Tests
7 script:
8 - npm install
9 - npm test
10 - step:
11 name: Lint
12 script:
13 - npm install
14 - npm run lint
15 - step:
16 name: Deploy Preview
17 script:
18 - curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
19 - export PATH="$HOME/.bunnyshell/bin:$PATH"
20 - export BUNNYSHELL_TOKEN=$BUNNYSHELL_TOKEN
21 - export ENV_NAME="pr-${BITBUCKET_PR_ID}"
22 - |
23 bns environments create \
24 --from-environment "$BUNNYSHELL_ENV_ID" \
25 --name "$ENV_NAME" \
26 --k8s "$BUNNYSHELL_CLUSTER_ID"
27 - |
28 bns environments deploy \
29 --name "$ENV_NAME" \
30 --project "$BUNNYSHELL_PROJECT_ID" \
31 --wait \
32 --timeout 900This runs tests and linting in parallel, then deploys the preview environment only if both pass.
Troubleshooting
| Issue | Solution |
|---|---|
Pipeline can't find bns command | Ensure the install script runs before any bns calls, and add $HOME/.bunnyshell/bin to PATH. The install script may output the path — check the logs. |
BITBUCKET_PR_ID is empty | This variable is only set in pull request pipelines, not branch pipelines. Make sure you're using the pull-requests: trigger, not branches:. |
| Environment creation fails with "already exists" | The pipeline tries to create a duplicate. Add a check for existing environments (shown in the full pipeline example) and redeploy instead. |
Timeout during --wait | Increase the --timeout value. Large images or slow clusters may need more than 900 seconds. Also check Bunnyshell build logs for stuck builds. |
| PR comment not posted | Verify BITBUCKET_APP_PASSWORD is set and has pullrequest:write scope. Check that BITBUCKET_USERNAME matches the app password owner. |
| Webhook not firing (Approach A) | Go to Repository Settings > Webhooks in Bitbucket. Check that the Bunnyshell webhook is active and not returning errors. Test with a manual trigger. |
| Environment not destroyed on merge | In Approach A, ensure "Destroy after merge" is toggled on. In Approach B, verify the branches: main: pipeline runs on merge and the environment name matches the pattern. |
| Pipeline runs on wrong PRs | Use destination branch filtering or changeset includePaths to narrow the trigger scope. |
bns environments list returns empty | Verify BUNNYSHELL_ORG_ID and BUNNYSHELL_PROJECT_ID are correct. The token must have access to the specified organization and project. |
Secured variable appears as *** in debug | This is expected behavior. Bitbucket masks secured variables in logs. If you need to debug, temporarily use a non-secured variable (but never commit the token). |
What's Next?
- Add smoke tests — Run API health checks or Cypress tests against the preview URL before notifying reviewers
- Integrate with Jira — Use Bitbucket's Jira integration to link preview environments to issues
- Parallel deploys — Use Bitbucket Pipelines'
parallelstep to run tests alongside the preview deployment - Custom cleanup schedules — Use Bunnyshell's auto-stop feature to pause idle environments and reduce costs
- Branch-specific configurations — Use different Bunnyshell environment configs for different target branches (e.g., staging vs. production-like)
Related Resources
- Bunnyshell CLI Reference
- Bunnyshell API Documentation
- Bitbucket Pipelines Configuration Reference
- Ephemeral Environments — Learn more about the concept
- All Guides — More technical guides
Ship faster starting today.
14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.