Preview Environments with CircleCI: Automated Per-PR Deployments with Bunnyshell
Why Integrate Bunnyshell with CircleCI?
CircleCI is built for speed — fast builds, smart caching, parallelism out of the box. Adding preview environments to your CircleCI workflows means every pull request gets a live, isolated deployment that reviewers can test immediately. No shared staging bottleneck, no "let me deploy my branch real quick" Slack messages.
With Bunnyshell + CircleCI, you get:
- Automatic deployment — A preview environment spins up for every pull request
- Workflow visibility — Environment status appears in your CircleCI workflow graph
- GitHub status checks — Preview URL posted directly on the PR
- Automatic cleanup — Environments are destroyed when the PR is merged or closed
- Zero infrastructure management — Bunnyshell handles Kubernetes, DNS, and TLS certificates
The integration is lightweight. You either let Bunnyshell handle everything via webhooks (zero workflow config), or add a few jobs to your .circleci/config.yml for full control.
How It Works
There are two models for connecting Bunnyshell to CircleCI:
| Model | How it triggers | Workflow changes | Best for |
|---|---|---|---|
| Webhook-Only | Bunnyshell listens to GitHub/GitLab PR events directly | None | Teams that want zero CI/CD maintenance |
| Pipeline Integration | CircleCI jobs call the Bunnyshell CLI | Add deploy/cleanup jobs | Teams that want full workflow control and visibility |
Webhook-Only: Bunnyshell adds a webhook to your Git provider (GitHub, GitLab, or Bitbucket). When a pull request is opened, Bunnyshell automatically creates and deploys a preview environment. Your .circleci/config.yml stays untouched.
Pipeline Integration: You add jobs to your CircleCI config that call the Bunnyshell CLI (bns). This gives you full control over when environments are created, deployed, and destroyed — and lets you run tests against the live preview URL within the same workflow.
Prerequisites
Before you begin, make sure you have:
- A Bunnyshell account with at least one project and a primary environment already deployed (the primary environment serves as the template for preview environments)
- A Bunnyshell API token — generate one from Settings > API Tokens in the Bunnyshell dashboard
- A CircleCI account connected to your Git provider (GitHub, GitLab, or Bitbucket)
- A working
bunnyshell.yamlin your repository (see our framework-specific guides for Laravel, Django, Express, etc.) - The Bunnyshell CLI installed locally for testing (optional but recommended):
1# macOS
2brew install bunnyshell/tap/bunnyshell-cli
3
4# Linux
5curl -fsSL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | sh
6
7# Verify
8bns versionIf you already have a primary environment running in Bunnyshell, you can skip straight to Approach A (webhook-only) — no workflow changes needed.
Approach A: Webhook-Only (Zero Pipeline Config)
This is the easiest approach. Bunnyshell manages the entire lifecycle — you just flip a toggle and every pull request automatically gets a preview environment.
Step 1: Connect Your Repository
- Log into Bunnyshell
- Go to Settings > Git Repositories
- Click Connect a Git Provider and select your provider (GitHub, GitLab, or Bitbucket)
- Authorize Bunnyshell to access your repository
Step 2: Enable Automatic Preview Environments
- Navigate to your primary environment (the one you've already deployed)
- 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 Kubernetes cluster for preview environments
That's it. Bunnyshell automatically adds a webhook to your repository.
What Happens Now
- Developer opens a PR — Bunnyshell detects the webhook event, clones the primary environment, swaps the branch to the PR's source branch, and deploys
- Developer pushes to the PR — Bunnyshell redeploys the preview environment with the latest changes
- Bunnyshell posts a comment on the PR with a direct link to the live deployment
- PR is merged or closed — The preview environment is automatically destroyed
The primary environment must be in Running or Stopped status before ephemeral environments can be created from it.
No .circleci/config.yml changes. No workflow jobs. No maintenance. If this is all you need, you're done.
Approach B: CircleCI Pipeline Integration
For teams that want full control — trigger Bunnyshell from within your CircleCI workflow, run tests against the preview URL, and track deployment status.
Step 1: Create a CircleCI Context
CircleCI Contexts let you securely share environment variables across projects and workflows:
- Go to your CircleCI Organization Settings
- Navigate to Contexts
- Click Create Context and name it
bunnyshell - Add the following environment variables:
| Variable | Description | Example |
|---|---|---|
BUNNYSHELL_TOKEN | API authentication token | bns_pat_xxxxxxxxxxxx |
BUNNYSHELL_PROJECT_ID | Your Bunnyshell project ID | proj_abc123 |
BUNNYSHELL_ENVIRONMENT_ID | Primary environment ID (template) | env_def456 |
BUNNYSHELL_CLUSTER_ID | Target Kubernetes cluster | cluster_ghi789 |
Contexts provide security boundaries in CircleCI. You can restrict which projects and branches can use the bunnyshell context via Security Groups. This prevents unauthorized branches from creating or destroying environments.
Step 2: Define Reusable Commands
Add reusable commands to your config that install and configure the Bunnyshell CLI:
1commands:
2 install-bns-cli:
3 description: "Install and configure the Bunnyshell CLI"
4 steps:
5 - run:
6 name: Install Bunnyshell CLI
7 command: |
8 curl -fsSL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | sh
9 echo 'export PATH="$HOME/.bunnyshell/bin:$PATH"' >> "$BASH_ENV"
10 - run:
11 name: Configure Bunnyshell CLI
12 command: bns configure set token "$BUNNYSHELL_TOKEN"
13 - run:
14 name: Verify CLI installation
15 command: bns version.circleci/config.yml Configuration
Here's a complete .circleci/config.yml with preview environment workflows:
1version: 2.1
2
3# ── Reusable Commands ──
4commands:
5 install-bns-cli:
6 description: "Install and configure the Bunnyshell CLI"
7 steps:
8 - run:
9 name: Install Bunnyshell CLI
10 command: |
11 curl -fsSL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | sh
12 echo 'export PATH="$HOME/.bunnyshell/bin:$PATH"' >> "$BASH_ENV"
13 - run:
14 name: Configure Bunnyshell CLI
15 command: bns configure set token "$BUNNYSHELL_TOKEN"
16
17# ── Job Definitions ──
18jobs:
19 build:
20 docker:
21 - image: cimg/base:2024.01
22 steps:
23 - checkout
24 - setup_remote_docker:
25 version: "20.10.24"
26 - run:
27 name: Build Docker image
28 command: |
29 docker build -t $CIRCLE_PROJECT_REPONAME:$CIRCLE_SHA1 .
30 echo "Build complete: $CIRCLE_PROJECT_REPONAME:$CIRCLE_SHA1"
31
32 deploy-preview:
33 docker:
34 - image: cimg/base:2024.01
35 steps:
36 - checkout
37 - install-bns-cli
38 - run:
39 name: Set environment name
40 command: |
41 BNS_ENV_NAME="preview-pr-${CIRCLE_PULL_REQUEST##*/}"
42 echo "export BNS_ENV_NAME=$BNS_ENV_NAME" >> "$BASH_ENV"
43 echo "Environment name: $BNS_ENV_NAME"
44 - run:
45 name: Check for existing environment
46 command: |
47 EXISTING_ENV=$(bns environments list \
48 --project "$BUNNYSHELL_PROJECT_ID" \
49 --name "$BNS_ENV_NAME" \
50 --output json 2>/dev/null | jq -r '._embedded.item[0].id // empty')
51 echo "export EXISTING_ENV=$EXISTING_ENV" >> "$BASH_ENV"
52 echo "Existing environment: ${EXISTING_ENV:-none}"
53 - run:
54 name: Create or update preview environment
55 command: |
56 if [ -z "$EXISTING_ENV" ]; then
57 echo "Creating new preview environment: $BNS_ENV_NAME"
58 ENV_ID=$(bns environments create \
59 --from-environment "$BUNNYSHELL_ENVIRONMENT_ID" \
60 --name "$BNS_ENV_NAME" \
61 --project "$BUNNYSHELL_PROJECT_ID" \
62 --k8s "$BUNNYSHELL_CLUSTER_ID" \
63 --output json | jq -r '.id')
64 else
65 echo "Reusing existing environment: $EXISTING_ENV"
66 ENV_ID="$EXISTING_ENV"
67 fi
68 echo "export BNS_ENV_ID=$ENV_ID" >> "$BASH_ENV"
69 - run:
70 name: Update branch and deploy
71 command: |
72 bns environments update-configuration \
73 --id "$BNS_ENV_ID" \
74 --from-git-branch "$CIRCLE_BRANCH"
75 echo "Deploying environment $BNS_ENV_ID..."
76 bns environments deploy --id "$BNS_ENV_ID" --wait
77 no_output_timeout: 30m
78 - run:
79 name: Extract preview URL
80 command: |
81 PREVIEW_URL=$(bns environments show \
82 --id "$BNS_ENV_ID" \
83 --output json | jq -r '._embedded.components[0].endpoints[0].url // "pending"')
84 echo "export PREVIEW_URL=$PREVIEW_URL" >> "$BASH_ENV"
85 echo "Preview URL: $PREVIEW_URL"
86
87 # Save to workspace for downstream jobs
88 mkdir -p /tmp/bns
89 echo "$PREVIEW_URL" > /tmp/bns/preview-url
90 echo "$BNS_ENV_ID" > /tmp/bns/env-id
91 - persist_to_workspace:
92 root: /tmp/bns
93 paths:
94 - preview-url
95 - env-id
96
97 test-preview:
98 docker:
99 - image: cimg/node:20.11
100 steps:
101 - checkout
102 - attach_workspace:
103 at: /tmp/bns
104 - run:
105 name: Load preview URL
106 command: |
107 PREVIEW_URL=$(cat /tmp/bns/preview-url)
108 echo "export PREVIEW_URL=$PREVIEW_URL" >> "$BASH_ENV"
109 echo "Testing against: $PREVIEW_URL"
110 - run:
111 name: Wait for environment readiness
112 command: |
113 for i in $(seq 1 30); do
114 STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$PREVIEW_URL" || echo "000")
115 if [ "$STATUS" = "200" ]; then
116 echo "Preview environment is ready!"
117 exit 0
118 fi
119 echo "Waiting for preview environment... (attempt $i/30, status: $STATUS)"
120 sleep 10
121 done
122 echo "Timeout waiting for environment" && exit 1
123 - run:
124 name: Run smoke tests
125 command: |
126 echo "Running smoke tests..."
127 curl -sf "$PREVIEW_URL" > /dev/null && echo "Homepage: OK"
128 curl -sf "$PREVIEW_URL/api/health" > /dev/null && echo "Health check: OK"
129 # Uncomment for E2E tests:
130 # - run:
131 # name: Run Cypress tests
132 # command: npx cypress run --config baseUrl=$PREVIEW_URL
133 # - store_artifacts:
134 # path: cypress/screenshots
135 # - store_artifacts:
136 # path: cypress/videos
137
138 cleanup-preview:
139 docker:
140 - image: cimg/base:2024.01
141 steps:
142 - install-bns-cli
143 - run:
144 name: Destroy preview environment
145 command: |
146 # Extract PR number from the branch or commit message
147 # On merge to main, we clean up all preview environments
148 # matching the merged branch
149 echo "Checking for orphaned preview environments..."
150
151 # List all preview environments
152 ENVS=$(bns environments list \
153 --project "$BUNNYSHELL_PROJECT_ID" \
154 --output json 2>/dev/null | jq -r '._embedded.item[] | select(.name | startswith("preview-pr-")) | .id')
155
156 for ENV_ID in $ENVS; do
157 ENV_STATUS=$(bns environments show \
158 --id "$ENV_ID" \
159 --output json | jq -r '.status')
160 ENV_NAME=$(bns environments show \
161 --id "$ENV_ID" \
162 --output json | jq -r '.name')
163
164 echo "Found: $ENV_NAME (status: $ENV_STATUS)"
165
166 # Check if the corresponding PR is still open
167 PR_NUM=$(echo "$ENV_NAME" | grep -oP '\d+$')
168 if [ -n "$PR_NUM" ]; then
169 # If running on main branch merge, destroy the environment
170 echo "Destroying environment: $ENV_NAME ($ENV_ID)"
171 bns environments delete --id "$ENV_ID" --wait || true
172 fi
173 done
174
175 cleanup-specific:
176 docker:
177 - image: cimg/base:2024.01
178 parameters:
179 pr-number:
180 type: string
181 default: ""
182 steps:
183 - install-bns-cli
184 - run:
185 name: Destroy specific preview environment
186 command: |
187 BNS_ENV_NAME="preview-pr-<< parameters.pr-number >>"
188 EXISTING_ENV=$(bns environments list \
189 --project "$BUNNYSHELL_PROJECT_ID" \
190 --name "$BNS_ENV_NAME" \
191 --output json 2>/dev/null | jq -r '._embedded.item[0].id // empty')
192
193 if [ -n "$EXISTING_ENV" ]; then
194 echo "Destroying preview environment: $BNS_ENV_NAME ($EXISTING_ENV)"
195 bns environments delete --id "$EXISTING_ENV" --wait
196 echo "Environment destroyed."
197 else
198 echo "No environment found for $BNS_ENV_NAME — nothing to clean up."
199 fi
200
201# ── Workflows ──
202workflows:
203 # Preview environment workflow — runs on PRs
204 preview-deploy:
205 jobs:
206 - build:
207 filters:
208 branches:
209 ignore: main
210 - deploy-preview:
211 context: bunnyshell
212 requires:
213 - build
214 filters:
215 branches:
216 ignore: main
217 - test-preview:
218 requires:
219 - deploy-preview
220 filters:
221 branches:
222 ignore: main
223
224 # Cleanup workflow — runs on merge to main
225 preview-cleanup:
226 jobs:
227 - cleanup-preview:
228 context: bunnyshell
229 filters:
230 branches:
231 only: mainCircleCI doesn't have a native "on PR close" trigger like GitHub Actions. The cleanup workflow runs on main branch pushes (which happen after a PR merge) and cleans up orphaned preview environments. For immediate cleanup on PR close, use Bunnyshell's webhook approach (Approach A) alongside the pipeline integration.
Workflow Examples
Deploy on Pull Request
When a developer pushes to a branch with an open PR, CircleCI triggers the preview workflow:
1Branch: feature/add-user-profile
2 preview-deploy workflow:
3 ├── build .............. Build Docker image
4 ├── deploy-preview ..... Create Bunnyshell environment "preview-pr-42"
5 │ └── Persists preview URL to workspace
6 └── test-preview ....... Run smoke tests against the live URLThe preview URL is available in the CircleCI job output and can be posted to the PR via GitHub API.
Destroy on Merge to Main
When a PR is merged, CircleCI runs the cleanup workflow on the main branch:
1Branch: main (after merge of PR #42)
2 preview-cleanup workflow:
3 └── cleanup-preview .... Scan and destroy orphaned preview environmentsWorkflow Graph
In the CircleCI dashboard, the workflow appears as:
build ──► deploy-preview ──► test-previewEach job shows its status, duration, and logs — giving full visibility into the preview environment lifecycle.
CircleCI Contexts and Environment Variables
Context Security
CircleCI Contexts provide a security boundary for sensitive variables. Best practices:
1# Only the deploy and cleanup jobs need Bunnyshell credentials
2deploy-preview:
3 context: bunnyshell # Has access to BUNNYSHELL_TOKEN
4 # ...
5
6build:
7 # No context — doesn't need Bunnyshell credentials
8 # ...Available CircleCI Variables
These built-in CircleCI variables are useful for Bunnyshell integration:
| CircleCI Variable | Description | Example Value |
|---|---|---|
CIRCLE_PULL_REQUEST | Full URL of the PR | https://github.com/org/repo/pull/42 |
CIRCLE_BRANCH | Current branch name | feature/add-profile |
CIRCLE_SHA1 | Full commit hash | a1b2c3d4e5f6... |
CIRCLE_PROJECT_REPONAME | Repository name | my-app |
CIRCLE_PROJECT_USERNAME | Organization/user name | my-org |
CIRCLE_BUILD_URL | URL to the CircleCI build | https://circleci.com/gh/org/repo/123 |
CIRCLE_WORKFLOW_ID | Current workflow ID | 12345-abcde-... |
Extracting PR Number from CircleCI
CircleCI doesn't provide a CIRCLE_PR_NUMBER variable directly. Extract it from CIRCLE_PULL_REQUEST:
1- run:
2 name: Extract PR number
3 command: |
4 PR_NUMBER="${CIRCLE_PULL_REQUEST##*/}"
5 echo "export PR_NUMBER=$PR_NUMBER" >> "$BASH_ENV"
6 echo "PR number: $PR_NUMBER"Using Multiple Contexts
For organizations with multiple environments (staging, production), use separate contexts:
1workflows:
2 preview-deploy:
3 jobs:
4 - deploy-preview:
5 context:
6 - bunnyshell # Bunnyshell credentials
7 - docker-registry # Container registry credentialsPR Status Checks with Preview URL
Posting Preview URL to GitHub PR
Add a step to your deploy job to post the preview URL as a PR comment:
1- run:
2 name: Post preview URL to PR
3 command: |
4 if [ -n "$PREVIEW_URL" ] && [ "$PREVIEW_URL" != "pending" ]; then
5 PR_NUMBER="${CIRCLE_PULL_REQUEST##*/}"
6 curl -s -X POST \
7 -H "Authorization: token $GITHUB_TOKEN" \
8 -H "Content-Type: application/json" \
9 -d "{\"body\": \"## Preview Environment Ready\\n\\nYour preview environment is live:\\n\\n**URL:** $PREVIEW_URL\\n\\n**Environment:** \`preview-pr-$PR_NUMBER\`\\n**Commit:** \`${CIRCLE_SHA1:0:8}\`\\n\\n---\\n_Deployed by Bunnyshell via CircleCI_\"}" \
10 "https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/issues/$PR_NUMBER/comments"
11 fiAdd a GITHUB_TOKEN with repo scope to your CircleCI context for posting PR comments. If you're using GitLab or Bitbucket, adjust the API endpoint accordingly.
GitHub Status Check
Instead of (or in addition to) a comment, set a commit status with the preview URL:
1- run:
2 name: Set GitHub commit status
3 command: |
4 curl -s -X POST \
5 -H "Authorization: token $GITHUB_TOKEN" \
6 -H "Content-Type: application/json" \
7 -d "{\"state\": \"success\", \"target_url\": \"$PREVIEW_URL\", \"description\": \"Preview environment ready\", \"context\": \"bunnyshell/preview\"}" \
8 "https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/statuses/$CIRCLE_SHA1"This adds a green check with a clickable link to the preview environment in the PR's status checks section.
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
BUNNYSHELL_TOKEN not found | Context not attached to job | Add context: bunnyshell to the job definition in the workflow |
bns: command not found | CLI not in PATH after install | Use echo 'export PATH="$HOME/.bunnyshell/bin:$PATH"' >> "$BASH_ENV" and source it |
| Environment creation fails | Primary environment not deployed | Deploy the primary environment first. It must be in Running or Stopped status |
| Workflow hangs on deploy | Deployment is slow or stuck | Add no_output_timeout: 30m to the deploy step |
| PR number is empty | No PR associated with branch push | CIRCLE_PULL_REQUEST is only set when a PR exists. Check if the variable is non-empty before proceeding |
| Cleanup doesn't run on PR close | CircleCI lacks "PR close" trigger | Use Bunnyshell webhooks (Approach A) for automatic cleanup on PR close. The pipeline cleanup handles merge-to-main scenarios |
| Preview URL returns 502 | Application not fully started | Add a readiness check loop in your test job. The app may need 30-60 seconds after deploy completes |
| Duplicate environments | Job retried without checking existence | Always check for existing environments by name before creating (see the EXISTING_ENV check) |
| Workspace data missing | persist_to_workspace / attach_workspace mismatch | Verify the root and paths match between persist and attach steps |
| Context access denied | Security group restriction | Check that your project is included in the context's security group in CircleCI Organization Settings |
What's Next?
- Add E2E tests — Run Cypress, Playwright, or Selenium tests against the preview URL in your workflow
- CircleCI orbs — Package the Bunnyshell integration as a reusable CircleCI orb for your organization
- Parallelism — Run multiple test suites in parallel against the same preview environment
- Scheduled cleanup — Add a scheduled workflow (
triggers: - schedule) to clean up stale preview environments nightly - Slack notifications — Use the CircleCI Slack orb to notify your team when preview environments are ready
- Branch-specific configs — Use CircleCI's
parametersandwhenconditions to customize preview environments per branch pattern - Resource classes — Use larger CircleCI resource classes for faster builds if preview deployments are in the critical path
Ship faster starting today.
14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.