How to Deploy Sandbox Environments on Kubernetes: 5 Approaches, Ranked by Isolation and Cost
A practitioner's guide to deploying sandbox environments on Kubernetes: namespace-per-sandbox, GitOps per-PR environments, virtual clusters, ephemeral clusters, and hardened runtimes for untrusted AI agent code - with comparison tables of isolation boundary, creation time, cost driver, and cleanup for each.
There are five practical ways to deploy a sandbox environment on Kubernetes, in rising order of isolation and cost: namespace-per-sandbox (created in seconds, weakest isolation), GitOps per-PR environments via the Argo CD ApplicationSet pull request generator, virtual clusters such as vCluster (own API server, shared nodes), ephemeral real clusters via Terraform, Pulumi, or kind/k3d in CI, and pod-level sandboxes on a hardened runtime like gVisor or Kata Containers.
The minimum viable Kubernetes sandbox is six objects: a Namespace labelled with owner and TTL, a ResourceQuota, a LimitRange, a default-deny ingress and egress NetworkPolicy with a DNS allow-list, the pod-security.kubernetes.io/enforce=restricted label, and a TTL reaper that deletes the namespace when the pull request closes. You can ship all six in an afternoon with kubectl plus Helm or Kustomize.
Choose the boundary by the trust level of the code, not by convenience. Trusted internal code is fine in a hardened namespace. Untrusted or LLM-generated code needs a kernel boundary: a RuntimeClass backed by gVisor or Kata Containers, or a separate cluster.
A Kubernetes namespace is not a security boundary. Pods in different namespaces share the node kernel, and all pod-to-pod traffic is allowed until a NetworkPolicy selects the pod (Kubernetes docs).
For AI agent code execution, Agent Sandbox is a young Kubernetes SIG Apps project (github.com/kubernetes-sigs/agent-sandbox) that ships a Sandbox custom resource for fast-starting, pausable, isolated sandboxes and delegates the actual isolation to gVisor or Kata Containers. It solves a different problem than preview environments.
Sandboxes are cheap to create and expensive to forget: the cost is TTL and auto-stop, not compute. If you want a full application environment per pull request without maintaining namespace templating, wildcard DNS, TLS renewal, database seeding, and cleanup controllers, an internal developer platform is the shortest path. Qovery does this inside your own AWS, GCP, Azure, or Scaleway account, or on your existing Kubernetes cluster, with auto-stop on idle.
What is a sandbox environment on Kubernetes, and what are you actually isolating?
A Kubernetes sandbox environment is a short-lived, isolated slice of compute where code runs without touching production, and there are exactly four boundaries you can choose from: namespace (shared control plane and shared kernel), virtual cluster (own API server, shared nodes), separate cluster or dedicated node pool, and sandboxed container runtime (kernel boundary). Everything else in this article is a consequence of that one choice.
Before anything else, split the word "sandbox," because it means two different things and every other article conflates them. An application sandbox is a preview or test environment per branch or pull request: your app, its dependencies, a URL a reviewer can click. A code-execution sandbox is a place to run untrusted or AI-agent-generated code safely. They look similar on a slide and they need completely different tools. I will keep them split for the rest of this piece.
The three real use cases fall out of that split: developer and PR previews (application sandbox), CI and integration tests (application sandbox), and untrusted code execution for AI agents, user-submitted code, or sales demos (code-execution sandbox).
Here is the isolation ladder, weakest to strongest:
Namespace - shared control plane, shared node kernel.
Virtual cluster - its own API server and CRDs, workloads still land on shared nodes.
Separate cluster or dedicated node pool - separate control plane or separate machines.
Sandboxed container runtime - a kernel boundary per pod via gVisor or Kata Containers.
Now the opinion I will defend for the rest of the article: a Kubernetes namespace is not a security boundary, and treating it like one is how untrusted code turns into a cloud incident. Two facts back this up. First, Kubernetes networking is allow-by-default: all pod-to-pod traffic, ingress and egress, is permitted until a NetworkPolicy selects the pod, and policies are additive and opt-in. Second, a namespace is a scope for names, not a kernel boundary: pods in different namespaces run on the same node and share that node's Linux kernel.
A namespace does not isolate: the node kernel, node resources unless you set a ResourceQuota and LimitRange, cluster-scoped objects (CRDs, ClusterRoles, validating and mutating admission webhooks), or network egress, including the cloud instance metadata endpoint at 169.254.169.254.
This is the default question, not a niche one. In the CNCF 2025 Annual Cloud Native Survey, published January 2026, 82% of surveyed container users run Kubernetes in production. If your team runs on Kubernetes, sandboxing on Kubernetes is where this lands.
One line to carry with you: sandboxes are cheap to create and expensive to forget.
How do you deploy a sandbox environment on Kubernetes step by step?
For trusted internal code, deploying a sandbox environment on Kubernetes takes seven steps: create a labelled Namespace, apply a ResourceQuota and LimitRange, apply a default-deny ingress and egress NetworkPolicy with a kube-dns allow-list, set pod-security.kubernetes.io/enforce=restricted, deploy the app as a per-sandbox Helm release or Kustomize overlay, expose it on wildcard DNS with an Ingress or Gateway API route and a wildcard TLS certificate, then register a TTL that deletes it.
The seven steps, with the exact object names and label keys so each is actionable:
Create a Namespace labelled owner, ttl, git-branch, and pr-number. Those labels are what your cleanup and cost tooling will key on later.
Apply a ResourceQuota and a LimitRange so one runaway sandbox cannot starve the cluster.
Apply a default-deny NetworkPolicy for both ingress and egress, then add back only what the app needs, starting with kube-dns on port 53.
Set pod-security.kubernetes.io/enforce=restricted on the namespace.
Deploy the app as a per-sandbox Helm release or Kustomize overlay with a generated release name.
Expose it on wildcard DNS with an Ingress or Gateway API route plus a wildcard or per-environment TLS certificate.
Register a TTL so the namespace is deleted when the pull request closes.
Here is the Namespace, ResourceQuota, and default-deny NetworkPolicy in one manifest:
The restricted Pod Security Admission level is doing real work here, and most people paste the label without knowing what it enforces. Per the Pod Security Standards, restricted requires runAsNonRoot: true, a seccomp profile of RuntimeDefault, allowPrivilegeEscalation: false, and dropping ALL capabilities (only NET_BIND_SERVICE may be added back). That single label removes a large class of container-escape footguns for free.
Watch the ResourceQuota gotcha. Per the ResourceQuota docs, once a namespace has a CPU or memory quota, every new pod must specify requests or limits for that resource or the control plane can reject it. That is exactly why the LimitRange matters: it supplies defaults so developers do not hand-write requests on every pod just to get scheduled.
DNS and TLS are the underestimated part. You need one wildcard DNS record (*.preview.example.com), a wildcard or per-environment certificate from cert-manager, and an eye on ACME rate limits. Let's Encrypt currently allows up to 50 certificates per registered domain every 7 days and up to 5 certificates per exact set of identifiers every 7 days. Spin up dozens of uniquely named preview environments a day and you will hit those limits, which is the argument for a single wildcard certificate over a fresh cert per sandbox.
Data seeding is where sandboxes get slow or flaky. Three options, with the honest tradeoff each time: restore from a snapshot (accurate, slow), load shared read-only fixtures (fast, limited realism), or use an anonymized production dump (safest realistic data, more work to build). Do not point every sandbox at one shared database. The moment two pull requests write to the same rows, you get cross-talk and flaky tests, and nobody can tell whether the failure is the code or the neighbor.
Treat cleanup as step seven, not as a someday. Three patterns that work: a label-based TTL reaper running as a CronJob that deletes namespaces past their ttl, the Argo CD ApplicationSet pull request generator (which removes the generated Application automatically once the pull request no longer matches, meaning it is closed or merged), or a GitHub Actions job on the pull_requestclosed event. Whatever you pick, it also has to sweep the orphans a namespace delete leaves behind: PersistentVolumes, cloud load balancers, DNS records, and container images.
Be honest about the ceiling of this approach. A namespace gives you no CRD or cluster-scoped isolation, a shared node kernel, and collisions the moment two sandboxes install an operator or admission webhook that assumes cluster-wide ownership. That is when you climb the ladder.
When should you use a virtual cluster or an ephemeral real cluster instead of a namespace?
Use a virtual cluster such as vCluster when sandboxes need their own API server, CRDs, or cluster-scoped resources but you do not want to pay for a real control plane, and use a full ephemeral cluster only when you must validate node-level, CNI, operator, or Kubernetes upgrade behaviour - which is a small minority of sandboxes.
vCluster from Loft Labs runs a virtual control plane (its own API server, controller manager, and data store) inside a namespace of a host cluster, while a syncer schedules the actual workloads onto the host cluster's nodes. Each tenant gets its own API server, CRDs, and RBAC without operating a real control plane. It is the right tool when platform teams need to test operators and CRDs, or hand a team cluster-admin without handing them your production cluster. Loft describes vclusters as starting in seconds; I would confirm any density or startup number against their docs rather than quoting a figure that moves between releases.
Ephemeral real clusters come in two flavors. Managed EKS, GKE, or AKS clusters created per run with Terraform, Pulumi, or Crossplane give you a genuinely separate control plane. kind or k3d inside a CI runner give you a throwaway cluster at near-zero cloud cost, which is perfect for integration tests that need real cluster behaviour but not real cloud infrastructure. For managed clusters, the cost driver is control-plane hours plus node warm-up, not the workload itself.
That control-plane cost is real and easy to underestimate. As of publication, at list price: Amazon EKS charges $0.10 per cluster per hour, GKE charges a $0.10 per cluster per hour management fee (with a monthly credit that effectively covers one zonal or Autopilot cluster per billing account), and AKS offers a Free control-plane tier and a Standard tier at $0.10 per cluster per hour for the uptime SLA. Work one example: 20 ephemeral clusters, 8 hours a day, 20 working days a month, at $0.10 per hour, is 20 x 8 x 20 x $0.10 = $320 a month in control-plane fees alone, before a single node, load balancer, or NAT gateway.
On creation latency, use the ordering rather than an invented number: a namespace comes up in seconds, a vCluster in tens of seconds, kind or k3d in about a minute, and a managed cloud cluster in many minutes. Seconds versus minutes is the decision, and it usually decides itself.
For running the test suites against these sandboxes, Testkube is the common CI-on-Kubernetes pattern: it runs your tests as native workloads inside the cluster rather than shelling out from an external runner.
The per-sandbox line items teams forget, each with its price page: the control-plane fee, one load balancer and public IP per environment (an AWS Application Load Balancer starts around $0.0225 per hour plus capacity units), a NAT gateway at roughly $0.045 per hour plus $0.045 per GB processed in US East, orphaned EBS or Persistent Disk volumes, and registry egress. None of these show up in a "how much does a pod cost" estimate, and together they are usually larger than the compute.
How do you sandbox untrusted or AI-agent-generated code on Kubernetes?
Untrusted code needs a kernel boundary, and on Kubernetes that means a RuntimeClass backed by gVisor or Kata Containers on a dedicated, tainted node pool, with default-deny egress and the instance metadata endpoint blocked. For AI agent workloads specifically, the Kubernetes SIG Agent Sandbox project provides a Sandbox custom resource built for exactly this shape of work.
Start with the runtime layer. gVisor runs a user-space kernel called the Sentry that intercepts application syscalls, which is the mechanism behind GKE Sandbox. Kata Containers takes a different route and runs each pod inside a lightweight virtual machine. Both are selected per pod through RuntimeClass. Be honest about the cost: syscall interception adds runtime overhead and some syscalls are unimplemented or partial, so test your workload before you commit. A kernel boundary is not free, but for untrusted code it is the point.
The pod-hardening checklist for untrusted workloads:
Egress is the real attack surface, not the container image. Default-deny egress, put an allow-list forward proxy in front of the few domains the agent legitimately needs, and explicitly block 169.254.169.254 so a compromised sandbox cannot reach the cloud metadata service and mint node credentials. On AWS, enforce IMDSv2 (session tokens, and a hop limit that stops pods from reaching the host's metadata) and on GCP the metadata server requires the Metadata-Flavor: Google header, but neither replaces blocking the endpoint at the network layer for untrusted pods.
For AI agents specifically, look at Agent Sandbox (github.com/kubernetes-sigs/agent-sandbox), a Kubernetes SIG Apps project. Its core custom resource is Sandbox (with optional SandboxTemplate, SandboxClaim, and SandboxWarmPool extensions for reusable templates and pre-warmed pools), and it gives sandboxes a stable network identity, persistent storage that survives restarts, and lifecycle management including pausing and resuming. Its stated use cases are AI agent runtimes, executing untrusted or LLM-generated code, development environments, and reinforcement-learning evaluation loops. Two honest notes: it is a young, pre-1.0 project (the latest release is v0.5.6 as of publication, with the API having recently moved from v1alpha1 to v1beta1), so verify the API surface against the repo before you build on it. And it is an orchestrator, not an isolation mechanism: by its own README it delegates the low-level isolation to gVisor or Kata Containers via RuntimeClass.
If you would rather buy than build, the managed code-execution services are a fair option and each has a focus: E2B and Daytona target agent and dev sandboxes, Modal targets serverless code and GPU execution, and Northflank runs both apps and untrusted workloads. Buying beats building when volume is low, you have no platform team, or you have a hard deadline on time-to-first-sandbox.
One boundary stated plainly: Qovery is an internal developer platform for application environments, not an untrusted code-execution sandbox. For agent code, reach for Agent Sandbox, gVisor, or Kata Containers.
If you only do three things for untrusted code: run it on a hardened runtime on a tainted node pool, default-deny egress with no metadata access, and set hard resource limits plus a TTL.
Ship faster on infrastructure you control.
Qovery gives your team a preview environment per pull request on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster - with auto-stop on idle. Start deploying in under 10 minutes.
Which Kubernetes sandbox approach should you choose?
Choose namespace-per-sandbox for trusted application previews, the Argo CD ApplicationSet pull request generator if you already run GitOps, vCluster when sandboxes need their own CRDs, ephemeral clusters for infrastructure-level validation, gVisor, Kata Containers, or Agent Sandbox for untrusted code, and an internal developer platform when you want developers to get environments without learning any of the above.
Approach
Isolation boundary
Typical creation time
Cost driver per sandbox
Cleanup mechanism
Who operates it
Best for
Main limitation
Namespace per sandbox (kubectl + Helm/Kustomize)
Namespace only: shared control plane, shared node kernel
Seconds
Pod compute plus one load balancer and DNS record per environment
Label-based TTL reaper (CronJob) that deletes the namespace
Your platform team on an existing cluster
Trusted internal PR and branch previews
Not a security boundary; no CRD or cluster-scoped isolation
Argo CD ApplicationSet pull request generator
Namespace per PR, driven by GitOps
Seconds to a minute after sync
Pod compute plus per-PR load balancer and DNS
Application auto-removed when the PR closes or no longer matches
Your platform team, already running Argo CD
Teams already doing GitOps who want per-PR previews for free
Inherits namespace isolation limits; needs Argo CD in place
vCluster (Loft Labs)
Virtual control plane: own API server, CRDs, RBAC; workloads on shared host nodes
Tens of seconds
Host-node compute plus vCluster control-plane pods
Delete the vCluster (and its host namespace)
Your platform team on a host cluster
Operator, CRD, and multi-tenant testing without a real control plane
Shared host kernel; not a hardware or kernel boundary
Everything above, plus default-deny ingress and egress NetworkPolicy and no metadata access
Untrusted or LLM-generated code
Kernel boundary or separate cluster
RuntimeClass (gVisor or Kata) on a tainted node pool, default-deny egress with 169.254.169.254 blocked, hard limits, activeDeadlineSeconds
The decision rule in one line: pick the cheapest boundary that matches the trust level of the code, then add a TTL.
How do you stop sandbox environments from blowing up your cloud bill?
Sandbox sprawl, not sandbox creation, is what costs money, so give every sandbox four things it cannot opt out of: an owner label enforced at admission, a hard ResourceQuota, auto-stop on idle, and a TTL that deletes it without a human deciding to.
The four enforced controls:
Owner and TTL labels enforced at admission with Kyverno or a Kubernetes ValidatingAdmissionPolicy, so an untagged sandbox simply cannot be created. If you cannot tell who owns it, you will never delete it.
A ResourceQuota and LimitRange per namespace, so a single sandbox cannot consume the cluster.
Auto-stop on idle, including nights and weekends, because most sandboxes sit doing nothing for the majority of their life.
TTL deletion plus an orphan sweep for volumes, load balancers, DNS records, and images.
Right-size aggressively. Sandboxes almost never need production replica counts, HPA minimums, or production instance types. Set requests without inflated limits, run them on a shared node pool with cluster autoscaler or Karpenter for scale-down and consolidation, and put them on interruptible capacity. As of publication, at list price, spot and preemptible instances run up to 90% off on-demand on AWS, up to 91% on GCP, and up to 90% on Azure. Non-production sandboxes are the textbook workload for it.
The waste is measurable. The CAST AI 2026 State of Kubernetes Resource Optimization Report found average CPU utilization of 8% and memory utilization of 20% across the production clusters it analyzed, so most provisioned capacity is paid for and idle. Zoom out to the whole cloud bill and the Flexera 2025 State of the Cloud Report found 84% of organizations name managing cloud spend as their top cloud challenge, and the FinOps Foundation's State of FinOps 2025 ranks workload optimization and waste reduction as the top practitioner priority. Sandboxes are exactly the kind of low-attention, high-churn workload where that waste accumulates.
The single biggest line item is usually dependencies, not compute. Do not spin up an RDS or Cloud SQL instance per sandbox. Run one shared managed database instance and give each sandbox its own schema or logical database. Same pattern for caches and message brokers. The compute for the app is often the cheapest thing in the environment; the per-sandbox managed data services are what quietly triple the bill.
Make the cost visible. Allocate spend by namespace label so every team sees its own sandbox bill, using OpenCost, the CNCF project for Kubernetes cost allocation, or Kubecost, which builds on it. A bill nobody can attribute is a bill nobody will cut.
The anti-pattern to name and avoid: the "we'll clean it up later" long-lived shared staging namespace. It starts as a convenience, and a year later it is an undocumented, permanent production dependency that everyone is afraid to touch. A sandbox with no TTL is not a sandbox, it is future tech debt.
How does Qovery fit in if you want a sandbox per pull request without building it?
Qovery is worth considering when the sandbox you want is a full application environment per pull request and you would rather not maintain namespace templating, wildcard DNS, TLS renewal, database seeding, quota policy, and cleanup controllers yourself. It runs inside your own cloud account or your existing Kubernetes cluster, so the cluster, the data, and the cloud bill stay in your name.
The capabilities I will stand behind: a preview and ephemeral environment per pull request, git-push deployments, per-environment RBAC, environment auto-stop for non-production, managed cluster upgrades, and databases backed by managed cloud services. That is the exact list of things you would otherwise wire together by hand from the sections above.
On where it runs, be precise: Qovery is bring-your-own-cloud on AWS, GCP, Azure, Scaleway, or your own existing Kubernetes cluster (self-managed, on-prem, any distribution). Your committed-use discounts and Savings Plans stay in your name because the workloads run in your account. It is multi-cloud, not AWS-only.
Here is the field observation that made me care about this. After talking with hundreds of CTOs, the pattern repeats almost word for word: a team builds namespace-per-PR in about a week, ships it, feels great, and then spends the next two years maintaining DNS, TLS renewal, quotas, data seeding, and cleanup as the app changes underneath it. The build is the easy week. The maintenance is the cost, and it never shows up in the original estimate.
The honest boundary, for the second time: Qovery is not a replacement for Agent Sandbox, gVisor, or Kata Containers when the job is executing untrusted or AI-generated code. That is a code-execution sandbox and a kernel-boundary problem. Qovery is an application-environment platform. Different problem, different tool.
What you keep versus what you stop maintaining:
You keep: your cloud account, your Kubernetes cluster, your data, your committed-use discounts.
You stop maintaining: namespace templating, wildcard DNS and TLS, quota policy, seeding jobs, TTL reapers, and orphan cleanup.
To be fair about the category, Northflank is a reasonable alternative that also does preview environments and, unlike Qovery, can run untrusted workloads. The concrete difference is the deployment model, which is to say where the workloads actually run: Qovery deploys into your own cloud account or your existing cluster, so the infrastructure stays yours. Pick based on whether owning the underlying cluster and bill matters to you.
How do you deploy a sandbox environment on Kubernetes step by step?
For trusted internal code on Kubernetes, deploy a sandbox in seven steps: create a labelled Namespace, apply a ResourceQuota and LimitRange, apply a default-deny ingress and egress NetworkPolicy with a kube-dns allow-list, set pod-security.kubernetes.io/enforce=restricted, deploy the app as a per-sandbox Helm release or Kustomize overlay, expose it on wildcard DNS with a TLS certificate from cert-manager, and register a TTL that deletes it when the pull request closes. You can build all of it with kubectl plus Helm or Kustomize in an afternoon.
Is a Kubernetes namespace enough isolation for a sandbox environment?
A Kubernetes namespace is enough for trusted internal code and not enough for untrusted code. A namespace is a scope for names, not a security boundary: pods in different namespaces share the node kernel, and all pod-to-pod traffic is allowed until a NetworkPolicy selects the pod. For untrusted or AI-generated code you need a kernel boundary from gVisor or Kata Containers, or a separate cluster.
What is Agent Sandbox in Kubernetes and when should you use it?
Agent Sandbox is a young Kubernetes SIG Apps project (github.com/kubernetes-sigs/agent-sandbox) that ships a Sandbox custom resource for fast-starting, pausable, stateful sandboxes aimed at AI agent code execution. It is an orchestrator that delegates the actual isolation to gVisor or Kata Containers via RuntimeClass. Use it when you are running untrusted or LLM-generated agent code and want a Kubernetes-native way to manage those sandboxes, but treat it as pre-1.0 (the latest release is v0.5.6 as of publication) and verify its API before you build on it.
How is a virtual cluster like vCluster different from a namespace-based sandbox?
A vCluster gives each sandbox its own Kubernetes API server, CRDs, and RBAC, running inside a namespace of a host cluster while syncing workloads onto the host's nodes (vcluster.com/docs). A namespace-based sandbox shares one API server and one set of cluster-scoped objects across all sandboxes. Use vCluster when sandboxes need to install their own operators or CRDs without colliding; use a plain namespace when they do not, because it is cheaper and simpler.
How do you automatically delete or auto-stop Kubernetes sandbox environments?
Automate deletion with a label-based TTL reaper running as a CronJob, the Argo CD ApplicationSet pull request generator (which removes the environment when the PR closes), or a GitHub Actions job on the pull_request closed event. Pair deletion with auto-stop on idle so environments do not run overnight and on weekends, and sweep the orphans a namespace delete leaves behind: PersistentVolumes, load balancers, DNS records, and container images.
How much does it cost to run one sandbox environment per pull request on Kubernetes?
On an existing cluster, one sandbox as a namespace is mostly pod compute plus one load balancer and a DNS record. The costs stack up when each sandbox gets its own cluster or managed database: at list price, EKS and GKE both charge about $0.10 per cluster per hour, and a NAT gateway adds roughly $0.045 per hour plus $0.045 per GB in US East. The real driver is time-to-delete: idle sandboxes with no TTL, not the price of creating them.
Can you run a sandbox environment per pull request on your own existing Kubernetes cluster?
Yes. You can build namespace-per-PR sandboxes on any existing Kubernetes cluster with kubectl, Helm or Kustomize, cert-manager, and a TTL reaper, or drive them through the Argo CD ApplicationSet pull request generator if you already run GitOps. If you would rather not maintain the DNS, TLS, quota, seeding, and cleanup plumbing yourself, Qovery runs preview environments per pull request on your own AWS, GCP, Azure, or Scaleway account, or your existing self-managed Kubernetes cluster, with environment auto-stop on idle.
Romaric founded Qovery to make Kubernetes accessible to every engineering team. He writes about platform strategy, developer experience, and the future of cloud infrastructure.
Next step
Ship faster on infrastructure you control.
Qovery gives your team a preview environment per pull request on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster - with auto-stop on idle. Start deploying in under 10 minutes.