Est.

How to Get Per-PR Preview Environments on Kubernetes Without DevOps

Every pull request gets its own isolated test environment without hiring DevOps.

· 11 min read · Updated
Cloud Provider Strategy · September 11, 2026 · 11 min read · 2,474 words

Picture two engineers who both need the shared staging server this week. One is testing a payment flow change, the other is testing a new onboarding screen, and neither change is live yet. One of them deploys first, the other's test data gets overwritten, and a third engineer gets pulled into a bug report that turns out to belong to neither of them, because nobody can tell which commit staging is actually running. That mess is what per-PR preview environments fix. Every pull request gets its own namespace, running against real infrastructure, so reviewers and QA can test the actual change instead of guessing at what's live on a shared box.

Without that setup, teams fall back on one staging environment that everyone shares, and sharing creates a line. Merges queue up behind whoever's using staging. Test data from one feature pollutes another. A failure appears and nobody's sure if it's a real bug or just leftover state from somebody else's branch. None of this requires a large team or a complicated system to become painful. The conflict occurs the moment two people need the same environment at the same time, which happens early, long before anyone has hired a dedicated DevOps engineer.

The specific win for a small team is timing. Preview environments move integration testing earlier, into the pull request itself, where a reviewer who already has full context on the change can catch a problem before it merges. Compare that to the alternative: the same bug reaching production and getting caught by whoever's on call at 2am, with none of the context the original reviewer had. The cost of a bug doesn't change much, but the cost of finding it drops sharply when it's caught by someone who already understands what the code was supposed to do.

What a per-PR preview environment on Kubernetes consists of

A preview environment is a deployment that exists because a pull request exists, and nothing more. On Kubernetes, that usually means a namespace scoped to that PR's lifetime, holding the image built from that PR's branch, any services that image depends on, a hostname someone can actually open in a browser, and a way to tear the whole thing down once the PR closes. A preview environment worth having is ephemeral, so its lifecycle matches the PR's; production-like, running real services against real data instead of mocks; isolated, so one PR's testing can't touch another PR's state; and automated from end to end, so no one has to click a button to make it happen.

Every stage of the lifecycle is a place where a DIY system can break later. A PR opens. CI picks up the push to that branch and builds an image from it. A namespace gets created, named something like preview-pr-142. Kubernetes manifests get rendered with values specific to that PR, things like the namespace name, the image tag, and the hostname. Those manifests get applied, which brings up the workload and any dependent services it needs. A hostname gets registered, usually through a wildcard DNS entry and an ingress rule, so the environment is reachable at something like pr-142.preview.example.com. The PR gets reviewed, tested, and eventually merged or closed. At that point, a cleanup job tears down the namespace, and the environment disappears.

Each of those steps sounds simple in isolation. Handling secrets, external service connections, and database seeds per environment, not just per application, decides whether the whole thing actually works. A preview environment that skips this and points at mocked dependencies fails to test integration. One that points every PR at a shared dev database just carries the same stale, inconsistent state from staging into the preview system, without solving it. Getting this right, consistently, across every PR, is what separates a preview system that earns reviewers' trust from one they learn to ignore.

The DIY path: ArgoCD ApplicationSets, custom pipelines, and the glue between them

Teams building this themselves tend to land on the same architecture, and it works. CI (GitHub Actions, GitLab CI, or similar) handles image builds on push to a PR branch. ArgoCD, using an ApplicationSet with the Pull Request generator, handles provisioning. The Pull Request generator polls the Git provider's API for open PRs and, for each one, stamps out a full ArgoCD Application from a template, complete with the PR number, branch, and commit SHA available as variables. This is about as close as the Kubernetes ecosystem comes to a built-in primitive for per-PR environments, and plenty of teams run it in production successfully.

The templating layer underneath is usually Helm or Kustomize. A base chart defines what the workload looks like, and the ApplicationSet injects PR-specific values at render time: the image tag, the namespace, the hostname. Routing needs two more pieces to work: a wildcard DNS record pointed at the cluster's load balancer, and an ingress controller (commonly NGINX) that can accept a new host rule per PR. The ApplicationSet template can generate that ingress manifest automatically. The DNS record itself is a one-time setup, which makes it one of the easier parts of the whole system.

Cleanup rides on ArgoCD's normal sync behavior. When a PR closes, the Pull Request generator simply stops producing an Application for it. ArgoCD notices the resource is gone and prunes the namespace, as long as the Application's finalizer is set up correctly. That finalizer configuration is a known trap: miss it, and namespaces that should have been deleted just sit there, and nobody notices until someone asks why the cluster is full of old PR numbers.

Database and secret handling gets left almost entirely to whoever builds the system, and there's no clean default answer. Spinning up a fresh database inside each preview environment gives good isolation but adds cost and slows down every preview with extra image-pull time. Pointing every preview at one shared dev database is cheap and fast but reintroduces the exact environment pollution previews were supposed to fix. Mocking out dependencies sidesteps both problems but produces tests that no longer catch integration failures, which is the reason previews were built. None of these is wrong. Each is a trade-off someone has to choose and then live with.

The hidden maintenance tax that compounds as PR volume and service count grow

The cost of this system doesn't grow in a straight line alongside the team. It grows with the product of two things: how many PRs are open at once, and how many services each preview needs to stand up. A system that works fine with ten PRs a week and three services can turn into something closer to a part-time job once PR volume doubles and the service count climbs into double digits, because the number of edge cases each dimension introduces multiplies rather than adds.

The failure modes at scale are specific, and they're the kind of thing that's hard to predict until it happens once. A cleanup job fails silently and namespace quotas run out. Two PRs on similarly named branches generate the same hostname and collide. A cluster's node pool wasn't sized for several services building at once, so image pulls slow down across every open preview. ArgoCD's own reconciliation loop for the ApplicationSet competes with production deployments running through the same controller, and both get slower. Each of these is a one-off problem the first time it happens, and a recurring one after that, because the underlying conditions that caused it didn't go away.

Kubernetes itself keeps moving, and every upgrade forces a re-check: do the ApplicationSet CRDs still work, is the ingress controller version still compatible, do the admission webhooks the preview system depends on still behave the way they did. None of that work makes the product better. It just keeps the lights on, and it has to happen on a schedule set by the platform, not by the team's own priorities.

Secrets add a second axis of growth entirely separate from PR volume. As services multiply, so does the number of secrets that need to be scoped correctly per preview environment, and that scoping is usually manual. Teams often find out during a security audit that preview environments have been running with production credentials for months, because nobody updated the scoping when a new service got added. And the person who understands how all of this fits together is usually just one engineer. When that engineer moves to a different project or leaves the company, the system doesn't get easier to maintain, it gets harder, because the knowledge of why it was built that way leaves with them.

Measuring whether the DIY tax is already costing your team more than you think

A preview environment is supposed to be invisible. Reviewers open a URL, test the change, and move on. The moment people start spending time debugging the preview infrastructure itself rather than the code it was built to test, the system has flipped from something that saves time to something that costs it, and that's worth checking for directly rather than guessing at.

A few numbers make this concrete. Look at how many "the preview environment is broken" issues come up relative to total PR review volume. Check how long it takes from opening a PR to having a working preview URL: if that number consistently runs past a few minutes, the team has probably just gotten used to a delay that shouldn't be there. Count how often someone has to manually clean up a stuck namespace, because that's a direct measure of how often the automated cleanup is failing. And ask whether adding a new service to the product requires a real, nontrivial change to the infrastructure repo before it can even be previewed, since that's a sign the system's design hasn't kept pace with the product.

A simple onboarding test tells the same story from a different angle. Hand a new engineer the task of adding a new service to the preview system without help from whoever built it. If they can't do it, the system's complexity has already outgrown its documentation, whether or not anyone has said so out loud.

Cost tends to go unmeasured. Preview namespaces that sit running all weekend because nobody tore them down. Failed cleanups that leave full environments up for days at a time. Node pools sized for the busiest PR day of the month, running at that size every single day. All of it adds up on the cloud bill, and almost none of it is visible on a line labeled "preview environments," so it's easy to miss even when it's substantial.

What a Managed Preview Environment Layer Handles

A managed preview environment platform takes on the full lifecycle described above as a product feature: namespace provisioning, image builds, routing, secret injection, teardown, and cost accounting, all under one contract, so the engineering team isn't the one maintaining the glue code between each piece.

The real value isn't in handling the happy path. Any DIY system handles the happy path; that's not where it breaks. The value is in the edge cases that came up in the maintenance section: handling branch-name collisions so two PRs never fight over the same hostname, confirming that a namespace and its attached storage are actually gone after cleanup instead of silently lingering, and giving preview environments the same secrets management posture as production instead of a separate manual process that someone has to remember to keep updated.

For applications with several services, a managed layer that understands the service graph can bring up only the services a given PR actually touches, and route everything else to a shared baseline. Building that kind of dependency resolution inside a DIY system is a real engineering project on its own, which is part of why so few homegrown systems attempt it.

Where the infrastructure runs matters too. A platform that deploys into the customer's own cloud account, rather than shared infrastructure it operates on the vendor's side, keeps the preview environment inside the same VPC and IAM perimeter as the rest of the production stack. A secret exposed in a preview environment under that model stays a problem inside the team's own infrastructure, not an exposure to a third party's shared systems.

Porter's fit for teams running on AWS, GCP, or Azure

Porter deploys preview environments directly into a team's own AWS, GCP, or Azure account. That means the preview infrastructure runs inside the same VPC and IAM boundary as everything else the team already has running in production, rather than on infrastructure shared across Porter's customers. For teams with compliance requirements, or services that touch sensitive data, that detail decides whether a managed layer is even an option worth considering, since it keeps the compliance boundary exactly where it already is.

Teams that want the lifecycle handled, namespace provisioning, image builds, DNS routing, and cleanup, without owning the ApplicationSet configuration, the ingress wiring, and the finalizer logic that make it all work, get that as a built-in, declarative feature rather than a system they have to assemble and then keep patched through every Kubernetes upgrade. It's one option among the ones already described here, and it fits specifically into the part of the decision that's about who owns the maintenance going forward.

Making the build-vs-manage call given your team's current size and trajectory

The decision comes down to two variables. First, how much of the team's time is the team willing to spend on infrastructure that doesn't move the product forward. Second, how much does preview reliability actually matter to review speed and release velocity right now, today, not in some hypothetical future state.

Building the DIY system is the right call when someone on the team genuinely wants to own it, when the service count is low and not likely to grow fast, and when the Kubernetes expertise to run ArgoCD and an ingress controller already exists in-house. In that situation, building the system doubles as learning the platform, and the time spent isn't wasted, it's an investment that pays off in platform knowledge the team keeps either way.

The inflection point comes from service count multiplied by PR velocity. A two-person team shipping a fast-moving microservices product will hit the maintenance wall sooner than a ten-person team running a monolith on a slow release cadence, because the first team's two variables are both climbing at once while the second team's aren't.

The question that actually matters isn't whether a team can build this system. Almost any team can build it; the real question is whether the team wants to still be maintaining it in six months, with three more services running and twice the PR volume moving through it. Whichever answer that question gets is the one that tells a team which path costs less, not just this month, but on the trajectory it's actually on.

More in Cloud Provider Strategy