Pull request environments engineers run themselves: a chart, an operator, and a dashboard

Over 1,470 pull request environments deployed since February across 13 repositories, a median six minutes from push to URL, with dev stacks and AI prototyping sandboxes running on the same operator and dashboard and no platform ticket to see, restart, or tear one down.

Industry: Media and events
Company size: 200-500 employees
Services: Platform engineering, developer experience, Kubernetes operators, CI/CD


The Challenge

The client's engineers ship Laravel applications from a dozen repositories, and before a change merges someone needs to see it running: a reviewer, a product manager, QA, the marketing team checking a landing page. Shared staging environments cannot do that for twenty open pull requests at once, and the old answer was to ask the platform team for something ad hoc.

Once per-pull-request environments existed, the second problem appeared. The lifecycle was platform-only. An engineer without kubectl or AWS access could not tell whether their stack was healthy, tail its logs, restart it, or tear it down. Every one of those was a message to a platform engineer. Nobody could answer "how many stacks are live right now?" without checking Flux, kubectl, CloudFormation, and GitHub separately, and the only cleanup was a Friday-night cron that unlabeled pull requests idle for two weeks.

Our Approach

Three pieces, built in the order that let each one stay small: a Helm chart that defines what a stack is, an operator that makes every stack a first-class Kubernetes object, and a dashboard that is a thin interface over the operator. Two more kinds of environment, dev stacks and prototyping sandboxes, were added on the same three pieces rather than as new systems.

The chart

Adding one label to a pull request builds a multi-architecture image, deploys a small CloudFormation stack for the per-PR IAM role and queues, applies sealed secrets, runs the chart, and posts the URL back on the pull request. The chart, now at its fourteenth minor release since December 2025, does the rest.

  • One pod, localhost networking. Web container, queue workers, scheduler, and optional MySQL and Redis sidecars share a pod, so nothing crosses a network boundary and nothing needs a service mesh. Every connection string is derived by the chart from the mode you pick, and the chart refuses to render if you pick two.
  • Four database modes. A throwaway MySQL sidecar with a small persistent volume, a pointer at the shared non-production database, a copy-on-write clone of a production snapshot on FSx for OpenZFS that provisions in about a minute and stores only the blocks the pull request changes, and a Postgres clone cut from a streaming standby for the data platform. Migrations run automatically before the web server starts, and workers wait for migrations to finish before they start.
  • Queues without cloud resources. An SQS-compatible sidecar with dead-letter queues, visibility timeouts, rate limits, and a queue dashboard at /queues on the stack's own hostname. Workers run consolidated or one per queue, with per-PR queue prefixes handled automatically. Two stacks can share one sidecar for cross-application flows.
  • Sidecar services and their UIs. Any dependent microservice can run as an additional container in the pod, and when it is enabled the chart switches the app's endpoint variables to localhost automatically. Sidecar UIs mount as path prefixes on the same hostname behind single sign-on. When CloudBeaver is on, the chart registers the stack's database connection on install and removes it on uninstall.
  • Cost controls in the template. Stacks scale to zero at 8 pm Friday and back at 6 am Monday through KEDA, prefer spot capacity, and spread across zones. Init containers, custom volumes, extra secrets and config maps, and web-container overrides are all values, so no repository needs a fork of the chart.

The operator

The operator, shipped in June 2026, watches Helm's own storage secrets and emits a PRStack object for every release it finds. Onboarding a repository needs zero operator configuration. It never calls Helm and has no AWS credentials, so uninstalling it leaves every stack running.

  • Status in one object. Phase, replica readiness, chart and revision, last deploy time, ingress URL discovered from either an Ingress or a Traefik IngressRoute, the pull request's state and label on GitHub, and Kubernetes events for every transition, so kubectl describe tells the story without operator logs.
  • Actions by annotation. A restart is one annotation on the PRStack; the operator rolls the Deployment. Secret overrides go through the operator, which owns the write. Bootstrap commands listed on the object run once in the web container after the stack is Ready, idempotent by name and deliberately never retried, because a blindly retried migration can corrupt data. They replaced the exec-into-a-pod setup steps that used to live in CI.
  • Cleanup that uses the repository's own teardown. Orphaned stacks, where the pull request closed, merged, or lost its label, are reaped by dispatching the repository's close workflow. Stacks whose release never came up are garbage-collected after a grace period. A teardown that keeps failing is marked stuck after three attempts and backed off to every six hours instead of hammering Actions. A TTL pass that retires idle stacks after fourteen days of pull request inactivity, with a protected label to exempt QA work, is built and deployed, and switched off until the last GitHub permission is confirmed.

The dashboard

About five thousand lines of Go behind single sign-on, deployed on merge, and a thin interface over the operator by design. It was first scaffolded to read Helm releases directly; we reversed that, wrote the operator first, and swapped the dashboard's data layer to the PRStack object without a rewrite.

  • Every live stack across every repository, with phase, replicas, chart, URL, and bootstrap results from the operator's status.
  • Live pod logs streamed over server-sent events, which is why it is Go: one goroutine per tail instead of a PHP worker pinned per open connection.
  • Restart, run a command, set or clear secret overrides, all through the operator. Teardown removes the label for an open pull request, dispatches the close workflow for orphans and dev stacks, and offers force-delete for stuck ones.
  • Orphan detection from two signals, the operator's view of the pull request and GitHub's, after a single swallowed search error once flagged every stack at once.
  • A read-only audit log of every action anyone took.

Dev stacks

Not every environment belongs to a pull request. A dependency service that several people's stacks call, or a long-running replica of main for password-reset testing, needs a stable address that outlives any one branch. Dev stacks reuse the chart and the same deploy action with a stack slug in place of a pull request number, so they get the same hostname convention, IAM role, secrets, and sidecars. Nine repositories can create them from a repository picker in the dashboard, the operator tracks them as their own kind, and the dashboard flags stale ones for cleanup. Six are live today, including a main-branch replica of the flagship site and a dependency stack that two other stacks point at.

Prototyping sandboxes

When product staff and their coding agents needed somewhere to run prototypes, the operator grew a second kind rather than a second system. A Sandbox object describes one or two pods of already-published images, an app plus its database and queue, with no repository, no build, and no chart. The operator renders it into a Deployment, Service, and authenticated ingress, gives it a 72-hour default lifetime, hardens the pod by default with no API token, no raw sockets, and no privilege escalation, injects secrets by reference, and offers an opt-in service account for object storage. Long-lived sandboxes live in a walled-off GitOps repository with the lifetime set to zero. The dashboard gained a page to create, edit, and delete them and to see the tenant apps sharing the namespace. The full story is in the AI prototyping sandbox case study.

The Results

Counts come from the GitHub Actions history of the thirteen onboarded repositories and the cluster on September 23, 2026.

1,470+stacks deployed since February, on an export that caps at 1,000 runs per repository, so the true count is higher
6 minmedian from push to a working URL, 11 minutes at the 90th percentile
13repositories on the same chart and workflow, 9 of them with dev stacks
0platform-team tickets needed to see, restart, or tear down a stack
  • Volume roughly doubled after the operator and dashboard shipped. Successful deploys ran at 100 to 240 a month through the spring, then 368 in August and 303 in September as the flagship site moved onto the platform and engineers stopped treating stacks as a favor.
  • Eighteen stacks are live as I write this, twelve on open pull requests and six dev stacks, every one reporting Ready, alongside the prototyping sandboxes in their own namespace.
  • The question that used to take four tools is one command, and the dashboard answers it for people who don't run commands.
  • Three kinds of environment, one operator, one dashboard. Pull request stacks, dev stacks, and sandboxes differ in how they are born and how they die. Everything in between is shared.

What did not go to plan

  • Removing the pull request label only triggers teardown on open pull requests. Closed and merged ones need a direct workflow dispatch, and dev stacks have no pull request at all, so teardown is a three-way branch rather than one call.
  • The dashboard's live log stream reconnected with stale credentials after the single sign-on session lapsed at one hour, producing about 200 authorization cycles an hour against the identity provider and a memory leak in its outpost. Fixed on the dashboard side and documented on the platform side.
  • A pull request with merge conflicts never fires the deploy workflow at all, not even as skipped. Because every merged pull request rewrites the PR values file, this was common, and it looks identical to a broken pipeline until you check mergeability.
  • The operator's first sandbox rollout ran two replicas without leader election, so both reconciled every sandbox and each one churned through about eight pods before settling. Fixed the same day.

Key Takeaway

Ephemeral environments are easy to create and hard to live with. The chart solved creation in a week. The operator and dashboard are what made the environments self-service, observable, and cleaned up, and they only stayed small because the operator was written first and the dashboard was forced to be a view over it. That is also why dev stacks and sandboxes cost days to add instead of months.

Related guide: Preview environments per pull request on Kubernetes: what is actually hard

Related case study: One login for every internal tool: Authentik as the front door to a Kubernetes platform


Want pull request environments your team runs themselves?

We'll show you the chart, the operator, and the dashboard on a live stack, and what it takes to onboard your first repository.

Schedule a call