Text size
Tried & Tested6 min read

A fresh environment for every pull request

Shared staging slows everyone down. Preview environments fix that, if you control the cost.

A fresh environment for every pull request
AI-generated editorial illustration

Almost every team I've worked with has had a "staging" environment. And almost every team has had the same conversation in Slack: "Who deployed to staging? My changes are gone." "Can you not deploy for an hour, I'm testing something?" "Staging is broken again, does anyone know why?"

One shared environment for many developers creates a bottleneck. Preview environments eliminate that bottleneck.

Why shared staging hurts

  • Changes collide. Two feature branches deployed to the same place overwrite each other.
  • It's always slightly broken. Someone's half-finished change is always there.
  • Feedback is slow. Reviewers, testers and product managers can't see a change until it's merged.
  • Nobody trusts it. And an environment nobody trusts is an environment nobody tests in properly.

How preview environments work

The idea is simple. When a pull request is opened, the pipeline creates a temporary copy of the application just for that branch, with its own URL like pr-142.preview.example.com. When the pull request is merged or closed, the environment is deleted.

On Kubernetes, there are a few common ways to build this:

A namespace per pull request. The simplest approach. The pipeline creates a namespace, deploys the Helm chart with the branch's image tag, and sets up an ingress or HTTPRoute for the preview hostname.

Argo CD ApplicationSets with the pull request generator. Argo CD watches your repository and automatically creates an Application for every open pull request, and removes it when the PR closes. No custom scripts.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: shop-previews
spec:
  generators:
  - pullRequest:
      github:
        owner: my-org
        repo: shop
      requeueAfterSeconds: 120
  template:
    metadata:
      name: 'shop-pr-{{number}}'
    spec:
      project: previews
      source:
        repoURL: https://github.com/my-org/shop
        targetRevision: '{{head_sha}}'
        path: deploy/chart
        helm:
          parameters:
          - name: image.tag
            value: '{{head_sha}}'
          - name: ingress.host
            value: 'pr-{{number}}.preview.example.com'
      destination:
        server: https://kubernetes.default.svc
        namespace: 'shop-pr-{{number}}'
      syncPolicy:
        automated: { prune: true }
        syncOptions: [CreateNamespace=true]

Virtual clusters (vCluster). When teams need cluster-level resources like CRDs, a virtual cluster per pull request gives full isolation without paying for a real cluster each time.

The best review comment is "I clicked through it and it works", and preview environments make that possible before merging.

Data and secrets: the hard part

Deploying the app is the easy bit. The tricky questions:

  • Databases. Options range from a small database container seeded with test data, to a shared database with a separate schema per PR, to snapshots of an anonymised dataset. Never copy production data with personal information into previews.
  • External services. Payment providers, email, third-party APIs. Use sandbox accounts or mocks.
  • Secrets. Previews should get their own limited credentials, ideally through External Secrets or your secret manager, not copies of staging secrets.
  • Wildcard DNS and TLS. A wildcard record like *.preview.example.com and a wildcard certificate (cert-manager handles this) make new hostnames work instantly.

Cleaning up so the bill stays small

Preview environments can quietly become expensive. Fifty open pull requests means fifty copies of your app.

What helps:

  • Delete on close, always. ApplicationSets do this automatically. With scripts, make sure the cleanup job runs even when the pipeline fails.
  • Time-to-live. Delete previews that haven't been updated for a few days, even if the PR is still open.
  • Small resources. Previews don't need production sizing. One replica, low requests.
  • Spot or preemptible nodes for the preview node pool.
  • Scale to zero at night with a simple scheduled job, or with tools that wake environments up on the first request.

So, is it worth it?

For teams with more than a few developers working in parallel, yes. Reviews get better, testing happens earlier, and the "who broke staging" messages disappear. Start with a namespace per PR, add data and cleanup properly, and grow from there.

My take: give every change its own little world. It's cheaper than the time your team loses waiting for staging.

— Desly