> ## Documentation Index
> Fetch the complete documentation index at: https://e2b-automation-api-reference-update.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Run Kubernetes with k3s

> Create a disposable Kubernetes cluster for agents, CI, previews, and integration testing.

The `e2b/k3s` template turns an E2B Sandbox into a preconfigured, single-node [k3s](https://k3s.io) cluster. `kubectl`, cluster DNS, networking, storage, and the default k8s components are configured when the sandbox starts.

Use it when an agent or CI job needs a single-node Kubernetes cluster without provisioning or sharing a permanent cluster.

<Warning>
  The k3s template is in preview and is not yet battle-tested across every Kubernetes workload. It is intended for ephemeral development and testing workflows, not as a highly available environment for hosting production traffic.
</Warning>

## Quickstart

Create and connect to a k3s sandbox from the CLI:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
e2b sbx create e2b/k3s
```

`kubectl` is already configured. On a fresh or resumed sandbox, wait for the node and cluster DNS objects to appear and become ready before deploying:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
timeout 120s sh -c 'until kubectl get node -o name | grep -q .; do sleep 1; done'
kubectl wait --for=condition=Ready node --all --timeout=120s
timeout 120s sh -c 'until kubectl get pod --namespace=kube-system --selector=k8s-app=kube-dns -o name | grep -q .; do sleep 1; done'
kubectl wait --for=condition=Ready pod \
  --namespace=kube-system \
  --selector=k8s-app=kube-dns \
  --timeout=120s
kubectl get nodes
kubectl get pods --all-namespaces
```

You can also create the sandbox with the standard E2B SDK:

<CodeGroup>
  ```typescript JavaScript & TypeScript theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
  import { Sandbox } from 'e2b'

  // One timeout for the sandbox and for every command run inside it.
  const TIMEOUT_MS = 15 * 60_000

  const sandbox = await Sandbox.create('e2b/k3s', {
    timeoutMs: TIMEOUT_MS,
  })

  try {
    await sandbox.commands.run(
      `
        set -euo pipefail
        timeout 120s sh -c 'until kubectl get node -o name | grep -q .; do sleep 1; done'
        kubectl wait --for=condition=Ready node --all --timeout=120s
        timeout 120s sh -c 'until kubectl get pod --namespace=kube-system --selector=k8s-app=kube-dns -o name | grep -q .; do sleep 1; done'
        kubectl wait --for=condition=Ready pod \
          --namespace=kube-system \
          --selector=k8s-app=kube-dns \
          --timeout=120s
      `,
      { timeoutMs: TIMEOUT_MS },
    )

    const nodes = await sandbox.commands.run('kubectl get nodes -o wide', {
      timeoutMs: TIMEOUT_MS,
    })
    console.log(nodes.stdout)
  } finally {
    await sandbox.kill()
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
  from e2b import Sandbox

  # One timeout for the sandbox and for every command run inside it.
  TIMEOUT = 15 * 60

  sandbox = Sandbox.create("e2b/k3s", timeout=TIMEOUT)

  try:
      sandbox.commands.run(
          """
          set -euo pipefail
          timeout 120s sh -c 'until kubectl get node -o name | grep -q .; do sleep 1; done'
          kubectl wait --for=condition=Ready node --all --timeout=120s
          timeout 120s sh -c 'until kubectl get pod --namespace=kube-system --selector=k8s-app=kube-dns -o name | grep -q .; do sleep 1; done'
          kubectl wait --for=condition=Ready pod \
            --namespace=kube-system \
            --selector=k8s-app=kube-dns \
            --timeout=120s
          """,
          timeout=TIMEOUT,
      )

      nodes = sandbox.commands.run("kubectl get nodes -o wide", timeout=TIMEOUT)
      print(nodes.stdout)
  finally:
      sandbox.kill()
  ```
</CodeGroup>

## Deploy and preview an application

This example creates an nginx Deployment and Service, forwards the Service to a sandbox port, and prints an E2B URL that you can open in a browser.

Every command that has to stay alive for the whole session uses the same timeout as the sandbox, including the background port-forward. A command started with a shorter timeout is terminated when that timeout expires, which would close the forwarded port while the sandbox is still running.

<CodeGroup>
  ```typescript JavaScript & TypeScript theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
  import { Sandbox } from 'e2b'

  // One timeout for the sandbox and for every command run inside it,
  // including the background port-forward that serves the preview URL.
  const TIMEOUT_MS = 15 * 60_000

  const sandbox = await Sandbox.create('e2b/k3s', {
    timeoutMs: TIMEOUT_MS,
  })

  await sandbox.commands.run(
    `
      set -euo pipefail
      timeout 120s sh -c 'until kubectl get node -o name | grep -q .; do sleep 1; done'
      kubectl wait --for=condition=Ready node --all --timeout=120s
      timeout 120s sh -c 'until kubectl get pod --namespace=kube-system --selector=k8s-app=kube-dns -o name | grep -q .; do sleep 1; done'
      kubectl wait --for=condition=Ready pod \
        --namespace=kube-system \
        --selector=k8s-app=kube-dns \
        --timeout=120s
      kubectl create deployment web --image=nginx:alpine
      kubectl scale deployment web --replicas=2
      kubectl expose deployment web --port=80
      kubectl rollout status deployment/web --timeout=180s
    `,
    { timeoutMs: TIMEOUT_MS },
  )

  await sandbox.commands.run(
    'kubectl port-forward --address=0.0.0.0 service/web 8080:80',
    { background: true, timeoutMs: TIMEOUT_MS },
  )

  await sandbox.commands.run(
    'until curl -fsS http://127.0.0.1:8080 >/dev/null; do sleep 1; done',
    { timeoutMs: TIMEOUT_MS },
  )

  const url = `https://${sandbox.getHost(8080)}`
  console.log(`Open the application: ${url}`)
  console.log(`Stop it with: e2b sandbox kill ${sandbox.sandboxId}`)
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
  from e2b import Sandbox

  # One timeout for the sandbox and for every command run inside it,
  # including the background port-forward that serves the preview URL.
  TIMEOUT = 15 * 60

  sandbox = Sandbox.create("e2b/k3s", timeout=TIMEOUT)

  sandbox.commands.run(
      """
      set -euo pipefail
      timeout 120s sh -c 'until kubectl get node -o name | grep -q .; do sleep 1; done'
      kubectl wait --for=condition=Ready node --all --timeout=120s
      timeout 120s sh -c 'until kubectl get pod --namespace=kube-system --selector=k8s-app=kube-dns -o name | grep -q .; do sleep 1; done'
      kubectl wait --for=condition=Ready pod \
        --namespace=kube-system \
        --selector=k8s-app=kube-dns \
        --timeout=120s
      kubectl create deployment web --image=nginx:alpine
      kubectl scale deployment web --replicas=2
      kubectl expose deployment web --port=80
      kubectl rollout status deployment/web --timeout=180s
      """,
      timeout=TIMEOUT,
  )

  sandbox.commands.run(
      "kubectl port-forward --address=0.0.0.0 service/web 8080:80",
      background=True,
      timeout=TIMEOUT,
  )

  sandbox.commands.run(
      "until curl -fsS http://127.0.0.1:8080 >/dev/null; do sleep 1; done",
      timeout=TIMEOUT,
  )

  url = f"https://{sandbox.get_host(8080)}"
  print(f"Open the application: {url}")
  print(f"Stop it with: e2b sandbox kill {sandbox.sandbox_id}")
  ```
</CodeGroup>

The script keeps running while the port-forward is open, and the sandbox stays available for up to 15 minutes so you can open the URL. Stop the script with `Ctrl+C` when you are done watching it, and remove the sandbox with the command it printed:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
e2b sandbox kill <sandbox-id>
```

Stopping the script on its own does not delete the sandbox: it keeps serving the URL until you kill it or the 15-minute timeout expires.

To verify in-cluster DNS and ClusterIP routing, run a client pod against the Service:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
kubectl run cluster-check \
  --rm -i \
  --restart=Never \
  --image=curlimages/curl:latest \
  -- curl -fsS http://web
```

## Check k3s status and logs

k3s runs as a systemd service with automatic restart on crash. Use `sudo`: the sandbox has no user D-Bus session, so unprivileged `systemctl` and `journalctl` cannot connect.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
sudo systemctl status k3s        # unit state
sudo journalctl -u k3s -f        # server logs
kubectl get --raw='/readyz?verbose' | tail -5
kubectl get pods --all-namespaces
```

Server flags live in `/etc/rancher/k3s/config.yaml`; edit the file and `sudo systemctl restart k3s` to apply. Workload logs work as usual through `kubectl logs`.

## Where to use it

* Give a coding agent an isolated cluster for a repository that expects Kubernetes.
* Validate Helm charts, operators, CRDs, manifests, Services, and DNS in CI.
* Create a cluster per pull request for integration tests or browser previews.
* Reproduce Kubernetes issues without provisioning EKS, GKE, or AKS.
* Test untrusted workloads without sharing a staging cluster.

The useful production pattern is **cluster per task**: create a sandbox, deploy and test the workload, collect the results, and remove the sandbox. Promote the same images, charts, and manifests to your managed production cluster after validation.

<Steps>
  <Step title="Create an isolated k3s sandbox">
    The agent, CI job, or preview service gets its own Firecracker microVM and Kubernetes control plane.
  </Step>

  <Step title="Deploy the production artifacts">
    Apply the same container images, Helm charts, CRDs, or manifests that will be promoted later.
  </Step>

  <Step title="Test the complete workload">
    Exercise Pods, Services, DNS, storage, and browser-visible endpoints without affecting a shared cluster.
  </Step>

  <Step title="Promote and clean up">
    Send validated artifacts to the production cluster, save any required test output, and kill the sandbox.
  </Step>
</Steps>

## What is tested and supported

The template runs stock k3s with Flannel's default VXLAN backend. These workflows are validated end-to-end on every release:

* Deployments, Services with load balancing across replicas (kube-proxy iptables and nftables modes), and session affinity.
* Ingress (Traefik), NodePort, LoadBalancer (ServiceLB), and `hostPort`.
* NetworkPolicy, both ingress and egress, with the built-in controller.
* StatefulSets with local-path PersistentVolumeClaims, Jobs, CronJobs, and HPA with metrics-server.
* Alternative CNIs that use VXLAN, such as Calico with `CALICO_IPV4POOL_VXLAN=Always`.
* Sandbox pause and resume: the cluster comes back immediately, the clock is corrected, and CronJobs keep their schedule.

## Known limitations

* The template runs one Kubernetes node in one sandbox. It does not provide high availability, and multi-node clusters across sandboxes are not supported.
* A sandbox is disposable. Do not treat its local Kubernetes storage as a durable system of record.
* Cilium and other eBPF dataplanes do not work: the sandbox kernel has no BPF JIT.
* Service meshes with iptables interception (Istio, Linkerd) do not work: the kernel lacks the `owner` and `TPROXY` netfilter extensions their sidecars need.
* Calico's default IPIP mode is not supported; use its VXLAN mode. GRE tunnels and SCTP Services are also unavailable.
* IPVS kube-proxy mode and nested-virtualization runtimes such as Kata Containers or KubeVirt are not supported.
* `kubectl port-forward service/<name>` connects to one Pod selected when the forward starts; use in-cluster requests to exercise load balancing across replicas.

For production application hosting, use a managed or otherwise highly available Kubernetes cluster with durable storage, backups, monitoring, and load balancing. Use E2B k3s to develop and validate what you deploy there.
