How to Get Per-PR Preview Environments on Kubernetes Without DevOps
Four patterns let teams run per-PR preview environments on Kubernetes.

Trunk-based development only works if code merges to main constantly, ideally daily, with no branch living long enough to rot. Shared staging environments break that model. Engineers queue for access, unrelated features collide in the same namespace, and one bad deploy blocks the whole team from testing anything.
What a preview environment actually is and what properties make one worth building
A preview environment is a deployment that exists because a pull request exists, nothing more. It spins up when the PR opens, gets a shareable URL, and dies when the PR closes or merges. No manual request, no ticket to infrastructure, no waiting.
Vercel made this the default expectation for frontend work years ago: push a commit, get a URL. What's changed is that the expectation has spread well past frontend deploys. Engineers now expect the same thing for full-stack changes, backend services, anything running on Kubernetes. Industry research found roughly 70% of engineers consider preview environments important to their workflow, and about a quarter call them extremely important. That's not a niche preference anymore. It's closer to table stakes, and the audit trail a per-PR environment produces is increasingly cited as a secondary benefit alongside developer productivity.
A preview environment worth building has four properties. It's ephemeral, meaning its lifecycle is tied to the PR and nothing outlives its purpose. It's production-like, running real services against realistic data instead of mocks. It's isolated, so one PR's tests can't touch another PR's state. And it's automated end to end, responding to GitHub events without a human clicking anything.
Underneath those four properties sit five technical problems every implementation has to solve regardless of approach: how to draw the isolation boundary, how to build and deploy the image, how to route traffic to a unique URL, how to isolate stateful services like databases, and how to clean everything up when the PR closes.
The four technical approaches to preview environments on Kubernetes and when each fits
Four patterns cover almost every team running previews on Kubernetes today, and they trade off cost, isolation strength, and setup complexity in different directions.
Namespace-per-PR is the simplest mental model. CI creates a namespace called something like preview-pr-142, deploys the full stack into it with Helm or Kustomize, and a wildcard DNS record routes traffic in. Close the PR, delete the namespace. The catch is that infrastructure cost scales linearly with both service count and open PR volume, so a team running 40 microservices with 20 open PRs is running 800 service instances just for previews.
ArgoCD ApplicationSets with a pull request generator take the GitOps route: an Application object gets created and destroyed automatically for each open PR, fully declarative and version-controlled. It's the most auditable option and gives the most control, but it also carries the highest ongoing maintenance load. Scaling and performance are among the top challenges teams hit once they run this pattern at real volume.
vCluster goes a layer deeper, giving each PR its own virtual Kubernetes API surface inside a shared physical cluster. This matters when a PR touches an operator, adds a new CRD, or needs cluster-scoped config that would otherwise step on other tenants. Isolation is stronger than plain namespaces, but the control-plane overhead adds up: one load test running 1,000 virtual clusters on STACKIT infrastructure landed in the €20,000 to €40,000 range, which is a useful ceiling to keep in mind before committing to this path at scale.
Request-level isolation, the model Signadot documents, flips the cost curve entirely. Instead of deploying the full stack per PR, it deploys only the services a PR actually changed and routes everything else to shared, real dependencies using a routing key carried in the request header. Previews come up in seconds because there's far less to build. Cost tracks the number of changed services, not the total service count, so the math gets better, not worse, as the microservice graph grows. Bitso ran into this directly: its homegrown preview environment approach got too expensive and too unreliable to keep running, and switching to Signadot's request-level model gave its 250-plus engineers across 200-plus microservices a lighter preview for every change.
Picking between them comes down to shape. Small-to-medium teams with a moderate service count do fine with namespace-per-PR. Teams whose PRs regularly touch operators or CRDs need vCluster. Teams that prioritize audit trails and already have platform engineering capacity lean toward ArgoCD ApplicationSets. And teams with a large service graph or high PR volume are the ones request-level isolation was built for.
What building namespace-per-PR yourself actually costs before it is production-ready
A prototype comes together fast. Getting it solid enough to trust does not.
The early version usually works for a week or two before it starts leaking: orphaned namespaces nobody cleans up, DNS entries that don't resolve, database state bleeding between environments because nobody thought through isolation carefully. None of these are exotic failures. They're the predictable cost of skipping the boring parts.
Getting the full system production-ready, meaning ApplicationSets wired up, DNS and wildcard certs working, database branching sorted, cleanup jobs running, resource quotas set, and a GitHub bot posting preview URLs on each PR, takes roughly two weeks of dedicated platform engineering time, according to cloudrps.com. That's not a guess pulled from a whiteboard estimate. That's real calendar time from a team that's built it.
Cleanup is where corners get cut most often. Orphaned environments don't announce themselves, they just sit there running and billing until someone notices the cloud invoice ballooned at month-end. TTLs and automated pruning aren't a nice-to-have here, they're the difference between a system that works and one that quietly bankrupts itself.
Database isolation is the other place teams underestimate the work. A dedicated database instance per PR gives full isolation but costs real money and takes time to provision. Schema isolation is cheaper but migrations can leak across environments if they're not scoped tightly. Snapshot-based approaches give realistic data at a moderate cost, but someone has to build and maintain the snapshot pipeline. None of these is free, and picking wrong means rebuilding later.
Two weeks of platform engineering is two weeks not spent shipping product. That trade-off is exactly what pushes many teams toward a managed layer instead of building this in-house.
Step-by-step: setting up namespace-per-PR preview environments with GitHub Actions and Kubernetes
The mechanics, laid out in order:
A GitHub Actions workflow triggers on pull_request events, specifically opened, synchronize, and closed. Three event types, one workflow file, no need to split logic across multiple triggers.
On open or synchronize, the pipeline creates a namespace named something like preview-pr-142 and applies resource quotas immediately, before any deploy happens. Capping CPU and memory at creation time, not after the fact, is what keeps one runaway preview from eating cluster capacity meant for everyone else.
Next comes the image build: tag the container with the PR number or commit SHA, not latest, and push it to a registry the cluster can pull from. Tagging by PR number matters later, because cleanup needs to know exactly which image to remove.
Deployment runs through Helm or Kustomize, parameterized by namespace and image tag. No need for a separate chart per PR, just a values override or patch file that gets injected at deploy time.
Routing rides on a single wildcard DNS record, something like *.preview.yourdomain.com, pointed at the ingress controller. That one record covers every PR going forward, so the deploy step just creates an Ingress resource for pr-142.preview.yourdomain.com and traffic finds it automatically. TLS works the same way: a wildcard cert issued once through cert-manager covers every subdomain, so there's no per-PR certificate request to manage.
Cleanup runs on the closed event, deleting the namespace outright. But webhooks miss sometimes, so pair that with a TTL-based fallback, a CronJob or controller rule that prunes anything that's overstayed its welcome regardless of whether the close event fired.
Last step: post the preview URL as a comment on the PR through the GitHub API. Reviewers shouldn't have to guess or construct the URL by hand.
Database isolation options at the per-PR level and how to pick one without overbuilding
Three real options exist here, and each one is a genuine tradeoff, not a strictly better or worse choice.
A separate database instance per PR gives full isolation. Destructive migrations can't hurt anyone else's environment. It's also the most expensive option and the slowest to provision when PR volume climbs. Schema isolation, running one database server with a separate schema or table prefix per PR, cuts the cost significantly, but migrations need to be scoped carefully or they bleed across environments. Snapshot-based isolation clones from a recent production snapshot, which gives reviewers realistic data to test against at a moderate cost, assuming the snapshot pipeline itself is maintained.
Neon and PlanetScale both support database branching per PR, and that maps cleanly onto the namespace-per-PR pattern: branch the database the same moment the namespace gets created, no separate snapshot pipeline to babysit.
For most teams running a moderate volume of PRs, schema isolation hits the right balance between cost and fidelity. Snapshot-based makes more sense once reviewers actually need production-realistic data to sign off on a change with confidence.
Sharing a single dev database across all preview environments is viable, but only under one condition: no PR runs a destructive migration. That has to be a team rule set before the first preview environment ships, not a lesson learned after someone truncates a shared table by accident. Whatever approach gets chosen, credentials belong in Kubernetes Secrets injected at deploy time, never hardcoded into a Helm values file sitting in version control.
Managed platforms that handle the namespace, routing, and cleanup layer for you
Several platforms exist specifically to absorb the two weeks of plumbing described above.
Bunnyshell deploys a full-stack preview environment per pull request directly into a customer's own Kubernetes cluster, supporting Helm, Docker Compose, K8s manifests, and Terraform. Connect an EKS, GKE, or AKS cluster, and a first preview environment can be running in under 30 minutes. Cost tracking comes built in through a Kubecost integration, and auto-sleep along with auto-destroy keep the bill from creeping upward unnoticed.
Okteto provisions preview environments per PR and integrates with CI/CD pipelines. It fits teams looking for a straightforward per-PR environment workflow without heavy platform engineering overhead.
Uffizzi takes an open-source-first approach, which matters for teams that want the convenience of a managed platform but also want to inspect, modify, or self-host the orchestration layer rather than hand it off entirely.
Vercel and Netlify are worth naming for completeness, since they popularized the preview URL pattern in the first place, but they're built for frontend projects and don't address backend or full-stack Kubernetes workloads. PaaS-native platforms like Render or Railway offer per-PR environments as a built-in feature with minimal setup (Render's version requires a Professional workspace or higher), as does Porter, a bring-your-own-cloud PaaS that provisions CI/CD and preview environments inside a team's own AWS, GCP, or Azure account, which suits teams that aren't running Kubernetes at all yet.
For teams where data residency and cost transparency matter — particularly under SOC 2 or HIPAA — running previews inside a customer-owned cloud account rather than shared tenant infrastructure becomes a meaningful distinction worth evaluating.
Keeping preview environment costs from outgrowing the value they deliver
Cost creep in preview environments has one root cause: environments that outlive their purpose because nobody capped how long they could live or how much they could consume. That charge doesn't show up as a line item labeled "orphaned preview namespace." It shows up buried inside a single undifferentiated cloud bill at the end of the month, and by then it's hard to trace back to a cause.
Spot instances, running at a 70 to 90% discount versus on-demand pricing, are a good fit for preview node pools specifically because preview environments are short-lived by design. An eviction just restarts a pod. Using node taints to keep preview workloads off production node pools prevents any spillover risk from that tradeoff.
TTL-based auto-destroy isn't optional. A PR sitting open for two weeks with no commits isn't being actively reviewed, it's just quietly accumulating cost, so a maximum lifetime needs to apply even to open PRs, not just closed ones. Pair the webhook-triggered cleanup with a scheduled pruning job as a backstop, since webhooks fail silently sometimes.
Resource quotas belong at namespace creation, full stop, not bolted on afterward as a fix once someone notices a runaway pod. For teams running a managed platform, Bunnyshell's Kubecost integration is one concrete example of what per-environment cost visibility looks like in practice: knowing exactly what each PR's preview costs, not just what the aggregate cluster costs.
Request-level isolation changes the underlying math rather than just optimizing around it. Cost scales with the number of services a PR actually changed, not the total number of services in the system. For a large microservice graph, that's the difference between a preview system that stays affordable as PR volume grows and one that eventually gets shut down because finance flagged it.
Where preview environments fit in a broader shift away from DevOps-heavy infrastructure
Preview environments are one visible piece of a much larger pattern: operational work that used to require a dedicated DevOps hire is increasingly handled by automation layers that get configured once and then run on their own.
The same logic shows up in CI/CD pipelines, cluster upgrades, and CVE patching. None of that work makes the product better directly. It just has to happen, and the goal across all of it is the same: make the undifferentiated overhead disappear into automation so engineering time goes toward features customers actually notice.
There's a safety argument here too, not just a speed one. Research published by alloy.app found an 89% drop in deployment-related incidents at teams that paired feature switches with per-PR preview environments. That's a meaningful number: preview environments aren't just about moving faster, they're about catching problems before they reach anyone who matters.
The infrastructure choices that make preview environments work, owning the cloud account, running workloads inside a private VPC, and using a platform layer that manages namespace lifecycle and cleanup automatically, are the same choices that make SOC 2 or HIPAA compliance achievable without turning it into a multi-month engineering slog. Teams moving off Heroku or other shared-tenant PaaS platforms often discover this the hard way: Heroku shifted to a sustaining engineering model in February 2026 and stopped offering new Enterprise Account contracts, and teams migrating off it frequently realize preview environments were never actually available to them on the old platform in the first place.
The pattern holds regardless of company size. Own the cloud account, automate the parts that don't require a human judgment call, and never let the deployment layer become the thing standing between a PR and a merged, tested, shipped feature.
