Skip to documentation
API + guides

01Documentation

Docker in Sandbox

Docker in Sandbox lets a Sandbox0 Cloud sandbox run a Docker daemon inside the sandbox. Use it when a coding agent or test workflow expects Docker commands such as docker run or docker build.

Typical uses:

  • Run Redis, PostgreSQL, or other service containers beside application tests.
  • Build a throwaway image from code generated inside the sandbox.
  • Run tools that require docker build, docker run, or docker compose style workflows.

/var/lib/docker uses ephemeral runtime storage. Images, containers, layers, and Docker-managed volumes are discarded by filesystem-only pause/resume, cold runtime replacement, or sandbox deletion. Store source code and durable outputs elsewhere in the sandbox rootfs so pause, resume, and named rootfs snapshots can retain them.

ℹ

The built-in default and coding-agent templates provide Docker binaries and writable Docker state, but they do not start dockerd before the sandbox is claimed. The examples below use default; claim coding-agent instead when the same sandbox also needs the bundled coding agents and Playwright CLI.

ℹ

Docker should run inside the sandbox boundary. Do not mount a host Docker socket into an agent sandbox just to make Docker commands work.


Template Requirements#

Installing the Docker CLI alone does not enable Docker in Sandbox. A Docker-enabled image must include the daemon, its nested runtime dependencies, and /usr/local/bin/sandbox0-dockerd-entrypoint. Configure its template with:

yaml
spec: mainContainer: image: <immutable-docker-enabled-image> securityClass: privileged resources: memory: 2Gi ephemeralMounts: - mountPath: /var/lib/docker sizeLimit: 1Gi

The privileged class grants capabilities only inside the gVisor guest. The startup helper mounts guest cgroup controllers, selects the classic vfs image store (including the Docker 29 compatibility flag), and disables Docker bridge/NAT. It refuses to start if Docker state is not backed by the template's bounded tmpfs. The tmpfs competes with processes for the sandbox memory limit; choose its size and memory allocation together. Docker PID files and runtime state live under /tmp and are recreated after resume.

Start the daemon again after filesystem-only pause/resume. The workspace survives; Docker images, containers, volumes, and processes do not. Template capability changes apply to newly created sandboxes, not to an existing sandbox's immutable security class.

Cloud Usage Pattern#

For Sandbox0 Cloud, use host networking for service containers:

  • Start a sandbox-local Docker daemon with Docker-managed bridge networking disabled.
  • Run service containers with --network=host.
  • Connect to those services from commands in the same sandbox at 127.0.0.1:<port>.

Start Docker once per running sandbox:

bash
if ! docker info >/dev/null 2>&1; then nohup /usr/local/bin/sandbox0-dockerd-entrypoint \ >/tmp/sandbox0-dockerd.log 2>&1 </dev/null & docker_pid=$! for attempt in $(seq 1 60); do docker info >/dev/null 2>&1 && break kill -0 "$docker_pid" 2>/dev/null || break sleep 1 done docker info >/dev/null 2>&1 || { cat /tmp/sandbox0-dockerd.log >&2 exit 1 } fi

Start Test Databases#

The examples claim a default sandbox, start Redis and PostgreSQL, verify readiness, print sandbox-local URLs, and clean up. Insert your project test commands inside the sandbox before the deferred/finally/exit cleanup runs. The printed loopback URLs cannot be used by tests running on your local computer.

go
package main import ( "context" "fmt" "log" "os" "strings" "time" sandbox0 "github.com/sandbox0-ai/sdk-go" ) func main() { ctx := context.Background() client, err := sandbox0.NewClient( sandbox0.WithToken(os.Getenv("SANDBOX0_TOKEN")), sandbox0.WithBaseURL(os.Getenv("SANDBOX0_BASE_URL")), ) if err != nil { log.Fatal(err) } sandbox, err := client.ClaimSandbox(ctx, "default", sandbox0.WithSandboxHardTTL(1800)) if err != nil { log.Fatal(err) } defer client.DeleteSandbox(ctx, sandbox.ID) if _, err := sandbox.Cmd(ctx, "rm -f /tmp/sandbox0-test-databases.ready"); err != nil { log.Fatal(err) } startScript := strings.Join([]string{ "set -e", "if ! docker info >/dev/null 2>&1; then", "rm -f /var/run/docker.sock /run/docker.sock /var/run/docker.pid", "/usr/local/bin/sandbox0-dockerd-entrypoint >/tmp/sandbox0-dockerd.log 2>&1 &", "for docker_attempt in $(seq 1 60); do docker info >/dev/null 2>&1 && break; sleep 1; done", "docker info >/dev/null 2>&1 || { cat /tmp/sandbox0-dockerd.log >&2; exit 1; }", "fi", "rm -f /tmp/sandbox0-test-databases.ready", "docker rm -f test-redis test-postgres >/dev/null 2>&1 || true", "docker pull redis:7-alpine", "docker pull postgres:16-alpine", "docker run -d --name test-redis --network=host redis:7-alpine", "docker run -d --name test-postgres --network=host -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=app_test postgres:16-alpine", "until docker exec test-redis redis-cli ping | grep -q PONG; do sleep 1; done", "until docker exec test-postgres pg_isready -U postgres >/dev/null; do sleep 1; done", "touch /tmp/sandbox0-test-databases.ready", "tail -f /dev/null", }, "\n") start, err := sandbox.Cmd(ctx, "sh", sandbox0.WithCommand([]string{"/bin/sh", "-lc", startScript}), sandbox0.WithCmdWait(false), sandbox0.WithCmdTTL(1800), ) if err != nil { log.Fatal(err) } defer sandbox.DeleteContext(ctx, start.ContextID) readyCommand := "test -f /tmp/sandbox0-test-databases.ready && printf READY" deadline := time.Now().Add(5 * time.Minute) for { result, err := sandbox.Cmd(ctx, "sh", sandbox0.WithCommand([]string{"/bin/sh", "-lc", readyCommand}), ) if err != nil { log.Fatal(err) } if strings.Contains(result.OutputRaw, "READY") { break } if time.Now().After(deadline) { log.Fatal("timed out waiting for test databases") } time.Sleep(2 * time.Second) } fmt.Println("REDIS_URL=redis://127.0.0.1:6379") fmt.Println("DATABASE_URL=postgres://postgres:[email protected]:5432/app_test?sslmode=disable") }

Run project tests with those URLs inside the same sandbox, before cleanup. The repository and its dependencies must already be present there:

bash
REDIS_URL=redis://127.0.0.1:6379 \ DATABASE_URL='postgres://postgres:[email protected]:5432/app_test?sslmode=disable' \ go test ./...

Build And Run An Image#

Docker build cache also lives under /var/lib/docker, so this is best for temporary test images rather than durable artifacts. These snippets assume an existing running sandbox handle and the imports from the database example. Use them before sandbox cleanup, or claim a new sandbox; the completed database examples delete their sandbox. Builds can exceed a synchronous request budget, so use streaming execution.

go
script := strings.Join([]string{ "set -e", "if ! docker info >/dev/null 2>&1; then", "rm -f /var/run/docker.sock /run/docker.sock /var/run/docker.pid", "/usr/local/bin/sandbox0-dockerd-entrypoint >/tmp/sandbox0-dockerd.log 2>&1 &", "for docker_attempt in $(seq 1 60); do docker info >/dev/null 2>&1 && break; sleep 1; done", "docker info >/dev/null 2>&1 || { cat /tmp/sandbox0-dockerd.log >&2; exit 1; }", "fi", "cat > main.go <<'EOF'", "package main", "import \"fmt\"", "func main() { fmt.Println(\"hello from docker in sandbox\") }", "EOF", "CGO_ENABLED=0 go build -o hello main.go", "cat > Dockerfile <<'EOF'", "FROM scratch", "COPY hello /hello", "ENTRYPOINT [\"/hello\"]", "EOF", "docker build -t sandbox0-docker-test .", "docker run --rm --network=none sandbox0-docker-test", }, "\n") // Also import "io". stream, err := sandbox.CmdStream(ctx, "sh", sandbox0.WithCommand([]string{"/bin/sh", "-lc", script}), ) if err != nil { log.Fatal(err) } defer stream.Close() var output strings.Builder for { chunk, err := stream.Recv() if err == io.EOF { break } if err != nil { log.Fatal(err) } output.WriteString(chunk.Data) } done, err := stream.Wait() if err != nil { log.Fatal(err) } if done.ExitCode == nil || *done.ExitCode != 0 || !strings.Contains(output.String(), "hello from docker in sandbox") { log.Fatalf("docker build test failed: %s", output.String()) } fmt.Print(output.String())

Nested Kubernetes And kind#

Nested Kubernetes tools such as kind and k3d need more than basic Docker command support. They create Kubernetes node containers and expect Docker to support cgroup namespaces plus host-like container networking. If the command fails with an error such as cgroup namespaces aren't enabled in the kernel, that sandbox cannot run kind reliably even if ordinary docker run and docker build commands work.

For projects that require kind, run that part of the workflow in a VM, a CI runner, or another environment with full Docker bridge networking and cgroup namespace support. See the kind known issues for the cgroup namespace requirement.


Operational Notes#

  • Containers launched by Docker are sandbox-local. In Sandbox0 Cloud, use --network=host for Redis, PostgreSQL, nginx, and similar service containers, then connect to 127.0.0.1 from commands inside the sandbox.
  • If you run against an environment where Docker port publishing works, -p hostPort:containerPort is also fine. The Cloud examples use --network=host because it avoids relying on Docker bridge/NAT.
  • Use Sandbox Services when a containerized HTTP service needs to be reachable from outside the sandbox.
  • Filesystem-only checkpoints exclude Docker state. Do not assume a nested Docker workload is compatible with experimental memory capture without qualifying it. Keep source, generated files, or database dumps outside /var/lib/docker, then use a named rootfs snapshot or a custom template when they must survive sandbox cleanup.
  • Docker state uses the size limit of the /var/lib/docker ephemeral mount and counts against sandbox memory. This is separate from the RootFS ephemeralStorage capacity. Inspect the selected template configuration for its current limits.
  • Pulling images can take longer than a single synchronous command request. Start long Docker setup commands asynchronously, then poll readiness with short commands.
  • kind and similar nested Kubernetes tools require cgroup namespace and networking support beyond ordinary Docker builds. Use a VM or CI runner when those kernel features are not available.
  • Long-running containers count against the sandbox resource limits. Stop containers when a test run finishes if the sandbox will stay alive.

Next Steps#