Preview Environments with Bitbucket Pipelines: Automated Per-PR Deployments with Bunnyshell
GuideMarch 20, 202611 min read

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:

ApproachPipeline changesControl levelBest for
Approach A: Webhook-OnlyNoneBunnyshell manages everythingTeams that want the fastest setup
Approach B: Pipeline IntegrationAdd steps to bitbucket-pipelines.ymlFull control over lifecycleTeams 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

Text
1PR opened → Bitbucket webhook → Bunnyshell API
23                              Create environment
4                              Build images
5                              Deploy to K8s
67                              Post comment on PR
8                              with preview URL

Bunnyshell 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

Text
1PR opened → Bitbucket Pipeline triggers
23          Pipeline step installs Bunnyshell CLI
4          CLI creates/deploys environment
5          CLI retrieves preview URL
67          Post-deploy steps (migrations, seeds, tests)
89          Pipeline posts preview URL as PR comment

Your 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

  1. Log into Bunnyshell
  2. Navigate to your Project and select your primary environment
  3. Go to Settings > Git Repository
  4. If not already connected, add your Bitbucket integration under Settings > Integrations > Bitbucket
  5. 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

  1. In your environment, go to Settings
  2. Find the Ephemeral environments section
  3. Toggle "Create ephemeral environments on pull request" to ON
  4. Toggle "Destroy environment after merge or close pull request" to ON
  5. Select the target Kubernetes cluster for ephemeral environments

Step 3: Verify the Webhook

After enabling, Bunnyshell automatically adds a webhook to your Bitbucket repository:

  1. In Bitbucket, go to Repository Settings > Webhooks
  2. You should see a Bunnyshell webhook with these events:
    • pullrequest:created
    • pullrequest:updated
    • pullrequest:fulfilled (merged)
    • pullrequest:rejected (declined)
    • repo:push

Step 4: Test It

  1. Create a new branch and make a change
  2. Open a pull request in Bitbucket
  3. Bunnyshell automatically creates an ephemeral environment cloned from your primary environment, using the PR branch
  4. Within a few minutes, Bunnyshell posts a comment on the PR with the preview URL
  5. 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

  1. In Bitbucket, go to Repository Settings > Repository variables
  2. Add a new variable:
    • Name: BUNNYSHELL_TOKEN
    • Value: Your Bunnyshell API token
    • Secured: Check this box (the value will be masked in logs)
  3. Add additional variables for your Bunnyshell resource IDs:
    • BUNNYSHELL_ORG_ID — Your organization ID
    • BUNNYSHELL_PROJECT_ID — Your project ID
    • BUNNYSHELL_CLUSTER_ID — Target Kubernetes cluster ID
    • BUNNYSHELL_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:

YAML
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-preview

Understanding the Pipeline Configuration

Let's break down the key parts:

Pull request trigger:

YAML
1pull-requests:
2  '**':
3    - step: *deploy-preview
4    - step: *post-deploy

The 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:

YAML
1branches:
2  main:
3    - step: *destroy-preview
4  master:
5    - step: *destroy-preview

When 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:

YAML
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:

YAML
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 script

Secrets and Variables

Store sensitive values as repository variables in Bitbucket:

VariableSecuredDescription
BUNNYSHELL_TOKENYesBunnyshell API token
BUNNYSHELL_ORG_IDNoOrganization ID
BUNNYSHELL_PROJECT_IDNoProject ID
BUNNYSHELL_CLUSTER_IDNoTarget K8s cluster ID
BUNNYSHELL_ENV_IDNoPrimary environment ID to clone from
BITBUCKET_APP_PASSWORDYesApp 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:

  1. Go to Workspace Settings > Pipelines > Workspace variables
  2. Add BUNNYSHELL_TOKEN and BUNNYSHELL_ORG_ID here — they apply to all repositories in the workspace

Deployment Variables

For environment-specific overrides, use deployment variables:

  1. Go to Repository Settings > Pipelines > Deployments
  2. Create environments like "Preview" and "Production"
  3. 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:

Bash
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 with pullrequest:write scope
  • BITBUCKET_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:

YAML
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 900

Destroy on Merge

YAML
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              done

Deploy with Custom Build Validation

YAML
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 900

This runs tests and linting in parallel, then deploys the preview environment only if both pass.


Troubleshooting

IssueSolution
Pipeline can't find bns commandEnsure 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 emptyThis 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 --waitIncrease 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 postedVerify 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 mergeIn 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 PRsUse destination branch filtering or changeset includePaths to narrow the trigger scope.
bns environments list returns emptyVerify 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 debugThis 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' parallel step 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)

Ship faster starting today.

14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.