# Run Kubernetes with k3s (/docs/use-cases/k3s)

<!-- agent-signals: reading_time_min: 8 · est_tokens: 3207 · updated: 2026-07-30 -->
Related: [Coding Agents](/docs/use-cases/coding-agents.md), [Computer use](/docs/use-cases/computer-use.md), [GitHub Actions CI/CD](/docs/use-cases/ci-cd.md), [Cloud browser](/docs/use-cases/remote-browser.md)

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 [#quickstart]

Create and connect to a k3s sandbox from the CLI:

```bash
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
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>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```typescript  
      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()
      }
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      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()
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Deploy and preview an application [#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>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```typescript  
      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}`)
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      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}")
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</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
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
kubectl run cluster-check \
  --rm -i \
  --restart=Never \
  --image=curlimages/curl:latest \
  -- curl -fsS http://web
```

## Check k3s status and logs [#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
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 [#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 [#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 [#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.
