How to Set Up Preview Environments for Every Pull Request on Kubernetes (2026)
The Short Version
Setting up preview environments for every pull request on Kubernetes requires three things: a way to connect to your cluster, a definition of what your application's stack looks like, and a trigger that creates/updates/destroys a deployment as pull requests open, get pushed to, and close. This guide covers both paths — building it yourself with GitHub Actions and raw Kubernetes tooling, and using a managed platform — so you can make an informed choice, not just follow one vendor's tutorial.
Step 1: Connect Your Kubernetes Cluster
Whether you build this yourself or use a platform, the starting point is the same: your CI/CD system (or the platform) needs credentials to deploy into your cluster.
DIY path: Create a service account with namespace-creation and deployment permissions, export its kubeconfig, and store it as a CI secret:
1kubectl create serviceaccount preview-deployer -n default
2kubectl create clusterrolebinding preview-deployer-binding \
3 --clusterrole=edit --serviceaccount=default:preview-deployerYou'll reference this credential in every workflow that needs to create or destroy a preview namespace.
Managed platform path (Bunnyshell): connect the cluster once through the CLI or UI:
bns k8s-clusters create --name my-cluster --kubeconfig ./kubeconfig.yamlThis is a one-time step — every subsequent environment created against this cluster reuses the connection.
Step 2: Define Your Application Stack
This is the step that determines how much ongoing maintenance you're signing up for.
DIY path: You write raw Kubernetes manifests (or Helm charts) for every component — frontend, backend, database — parameterized so a unique namespace/hostname can be injected per PR. You also write the logic that decides what changed and needs rebuilding versus what can be reused.
Managed platform path: A single bunnyshell.yaml describes the whole stack, and it accepts Docker Compose, Helm charts, or raw manifests directly — you don't rewrite existing infrastructure-as-code to adopt this:
1kind: Environment
2name: my-app-staging
3type: primary
4
5components:
6 - kind: Application
7 name: api
8 gitRepo: 'https://github.com/my-org/my-app.git'
9 gitApplicationPath: /api
10 dockerCompose:
11 build:
12 context: .
13 hosts:
14 - hostname: 'api-{{ env.base_domain }}'
15 servicePort: 8080
16
17 - kind: Database
18 name: dbStep 3: Wire Up the Git Provider Trigger
The mechanism that actually makes this "automatic" is a webhook: your Git provider needs to notify something when a PR opens, updates, or closes.
DIY path: A GitHub Actions workflow triggered on pull_request events (opened, synchronize, closed), calling kubectl apply on open/push and kubectl delete namespace on close:
1on:
2 pull_request:
3 types: [opened, synchronize, closed]
4jobs:
5 preview:
6 runs-on: ubuntu-latest
7 steps:
8 - name: Deploy or destroy preview namespace
9 run: |
10 if [ "${{ github.event.action }}" == "closed" ]; then
11 kubectl delete namespace "pr-${{ github.event.number }}" --ignore-not-found
12 else
13 kubectl create namespace "pr-${{ github.event.number }}" --dry-run=client -o yaml | kubectl apply -f -
14 # ... apply your manifests into that namespace, inject PR-specific values
15 fiThis works, but every edge case (a force-push, a re-opened PR, a stuck namespace deletion) is something you now own.
Managed platform path: connect the repository and enable ephemeral settings on the primary environment — no workflow file to write or maintain:
1bns environments update-settings --id <PRIMARY_ENV_ID> \
2 --create-ephemeral-on-pr \
3 --auto-deploy-ephemerals \
4 --destroy-ephemeral-on-pr-closeSee Connect Your Git Provider for the exact steps across GitHub, GitLab, Bitbucket, and Azure DevOps.
Step 4: Confirm the Round Trip Works
Regardless of which path you took, verify it end to end before relying on it:
- Open a real test PR against the connected repository.
- Confirm a new namespace/environment appears within a minute or two.
- Confirm it has a reachable URL (DIY: check ingress; platform:
bns environments endpoints --id <ENV_ID>). - Push a new commit and confirm it updates rather than creating a duplicate.
- Merge or close the PR and confirm the namespace/environment is actually removed — this is the step DIY setups most often get wrong, since nothing blocks you if teardown silently fails.
DIY vs. Managed Platform: The Honest Tradeoff
| DIY (Actions + kubectl) | Managed platform | |
|---|---|---|
| Upfront cost | Low — no new tool | Low — connect cluster + repo |
| Ongoing maintenance | You own every edge case | Platform owns lifecycle correctness |
| Teardown reliability | Depends on your own scripts | Built-in, guaranteed setting |
| Multi-format stack support (Compose/Helm/Terraform) | You write conversion logic per format | Native support, no rewrite |
| ChatOps from PR comments | You build it | Built-in (/bns:deploy, /bns:stop) |
| Cost visibility | You build it | Built-in per-environment tracking |
| Right for... | Teams with strong platform engineering capacity and a narrow, stable stack | Teams that want this working reliably without becoming its full-time maintainer |
Neither is universally correct. A team with one simple service and a platform engineer who enjoys maintaining this exact kind of tooling can reasonably choose DIY. A team whose actual job is shipping product features, with a stack that spans multiple formats (Compose locally, Helm in production, some Terraform-provisioned cloud resources), gets a much faster and more reliable setup from a platform that already handles all three natively.
Common Setup Mistakes
Forgetting to scope down cluster permissions. A CI service account with full cluster-admin is a real security exposure — scope it to exactly namespace-creation and deployment within the intended namespace pattern.
No teardown safety net for PRs that never close. Both DIY and platform setups need a stale-environment policy for PRs that sit open for weeks — see Auto-Destroy After Merge for the platform-side mechanism.
Assuming one namespace per PR is enough isolation. If components share a database or an external resource by mistake, one PR's test data can corrupt another's — verify isolation explicitly, don't assume it.
No verification step before declaring it done. Skipping step 4 above is how teams discover, weeks later, that teardown has been silently failing the whole time.
Where to Go Next
If you're evaluating whether to build this yourself, Replacing Homegrown Environment Tooling covers the maintenance-cost side of that decision in more depth. If you've already decided on a managed platform, Automated PR Environments for E2E Testing covers what to do once the environment exists — wiring it into your test suite.
Get preview environments working without maintaining the plumbing.
14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.
Frequently Asked Questions
What is a preview environment on Kubernetes?
A full-stack, isolated deployment of your application — frontend, backend, database, and dependencies — created automatically for a pull request and destroyed when it merges or closes. It runs as a real set of Kubernetes resources in its own namespace, not a mocked or partial environment.
Do I need to write my own Kubernetes manifests to get this working?
No, if you use a platform like Bunnyshell — you define your stack once in a bunnyshell.yaml file (from Docker Compose, Helm charts, or raw manifests) and the platform handles namespace creation, networking, and lifecycle. If you build this yourself with raw GitHub Actions and kubectl, yes, you write and maintain all of that.
How long does it take to get the first preview environment working?
With an existing bunnyshell.yaml and a connected cluster, a first deploy typically completes in under two minutes for a small-to-moderate application. The one-time setup (connecting the cluster and Git provider) takes longer the first time, but is not repeated per PR.
Can I do this with just GitHub Actions and kubectl?
Yes, and many teams start here — but be honest about the maintenance cost: you will be writing and maintaining namespace creation, ingress/DNS wiring, teardown-on-merge logic, and secret handling yourself, and re-fixing it every time your cluster or Actions runner changes. See the DIY-vs-platform section below for the actual tradeoff.
Does this work with an existing Kubernetes cluster, or do I need a new one?
It works with an existing cluster — EKS, GKE, AKS, or self-managed. The setup connects to your current cluster rather than requiring a dedicated new one, so existing workloads and networking are unaffected.