# 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 when the sandbox runtime is replaced or deleted. Store source code and durable outputs elsewhere in the sandbox rootfs so pause, resume, and named rootfs snapshots can retain them.

<Callout variant="info">
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.
</Callout>

<Callout variant="warning">
Docker should run inside the sandbox boundary. Do not mount a host Docker socket into an agent sandbox just to make Docker commands work.
</Callout>

---

## 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 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

Claim a `default` sandbox, start Redis and PostgreSQL with Docker, then run tests against `localhost` from inside the sandbox.

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `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 &",
        "until docker info >/dev/null 2>&1; do sleep 1; done",
        "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:postgres@127.0.0.1:5432/app_test?sslmode=disable")
}`
    },
    {
      label: "Python",
      language: "python",
      code: `import os
import time

from sandbox0 import Client, CmdOptions
from sandbox0.apispec.models.sandbox_config import SandboxConfig

client = Client(
    token=os.environ["SANDBOX0_TOKEN"],
    base_url=os.environ.get("SANDBOX0_BASE_URL", "https://api.sandbox0.ai"),
)

sandbox = client.claim_sandbox(
    template="default",
    config=SandboxConfig.from_dict({"hard_ttl": 1800}),
)

try:
    sandbox.cmd("rm -f /tmp/sandbox0-test-databases.ready")

    start_script = """
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 &
  until docker info >/dev/null 2>&1; do sleep 1; done
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
"""
    start = sandbox.cmd(
        "sh",
        CmdOptions(command=["/bin/sh", "-lc", start_script], wait=False, ttl_sec=1800),
    )
    try:
        ready_command = "test -f /tmp/sandbox0-test-databases.ready && printf READY"
        deadline = time.time() + 300
        while True:
            result = sandbox.cmd(
                "sh",
                CmdOptions(command=["/bin/sh", "-lc", ready_command]),
            )
            if "READY" in result.output_raw:
                break
            if time.time() > deadline:
                raise TimeoutError("timed out waiting for test databases")
            time.sleep(2)

        print("REDIS_URL=redis://127.0.0.1:6379")
        print("DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app_test?sslmode=disable")
    finally:
        sandbox.delete_context(start.context_id)
finally:
    client.delete_sandbox(sandbox.id)`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `async function main() {
    const { Client } = await import('sandbox0');

    const client = new Client({
        token: process.env.SANDBOX0_TOKEN!,
        baseUrl: process.env.SANDBOX0_BASE_URL || 'https://api.sandbox0.ai',
    });

    const sandbox = await client.sandboxes.claim('default', { hardTtl: 1800 });

    try {
        await sandbox.cmd('rm -f /tmp/sandbox0-test-databases.ready');

        const startScript = [
            '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 &',
            'until docker info >/dev/null 2>&1; do sleep 1; done',
            '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',
        ].join('\\n');

        const start = await sandbox.cmd('sh', {
            command: ['/bin/sh', '-lc', startScript],
            wait: false,
            ttlSec: 1800,
        });
        try {
            const readyCommand = 'test -f /tmp/sandbox0-test-databases.ready && printf READY';
            const deadline = Date.now() + 300_000;
            while (true) {
                const result = await sandbox.cmd('sh', {
                    command: ['/bin/sh', '-lc', readyCommand],
                });
                if (result.outputRaw.includes('READY')) {
                    break;
                }
                if (Date.now() > deadline) {
                    throw new Error('timed out waiting for test databases');
                }
                await new Promise((resolve) => setTimeout(resolve, 2000));
            }

            console.log('REDIS_URL=redis://127.0.0.1:6379');
            console.log('DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app_test?sslmode=disable');
        } finally {
            await sandbox.deleteContext(start.contextId);
        }
    } finally {
        await client.sandboxes.delete(sandbox.id);
    }
}

main().catch((err) => {
    console.error(err);
    process.exit(1);
});`
    },
    {
      label: "CLI",
      language: "bash",
      code: `SANDBOX_ID="$(s0 -o json sandbox create --template default --hard-ttl 1800 | jq -r '.id')"
trap 's0 sandbox delete "$SANDBOX_ID" >/dev/null 2>&1 || true' EXIT

s0 sandbox exec "$SANDBOX_ID" -- rm -f /tmp/sandbox0-test-databases.ready

s0 sandbox exec "$SANDBOX_ID" --no-wait --ttl 1800 -- /bin/sh -lc '
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 &
  until docker info >/dev/null 2>&1; do sleep 1; done
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; do sleep 1; done
touch /tmp/sandbox0-test-databases.ready
tail -f /dev/null
'

ready=0
for _ in $(seq 1 150); do
  if [ "$(s0 sandbox exec "$SANDBOX_ID" -- /bin/sh -lc 'test -f /tmp/sandbox0-test-databases.ready && printf READY')" = "READY" ]; then
    ready=1
    break
  fi
  sleep 2
done
if [ "$ready" -ne 1 ]; then
  echo "Timed out waiting for test databases" >&2
  exit 1
fi

db_urls="$(s0 sandbox exec "$SANDBOX_ID" -- /bin/sh -lc '
set -e
docker exec test-redis redis-cli ping | grep -q PONG
docker exec test-postgres pg_isready -U postgres >/dev/null
printf "REDIS_URL=redis://127.0.0.1:6379\\n"
printf "DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app_test?sslmode=disable\\n"
')"
if ! printf "%s" "$db_urls" | grep -q "^REDIS_URL="; then
  echo "Test databases are not ready" >&2
  exit 1
fi
printf "%s" "$db_urls"`
    }
  ]}
/>

Use those URLs from commands that run inside the same sandbox:

```bash
REDIS_URL=redis://127.0.0.1:6379 \
DATABASE_URL='postgres://postgres:postgres@127.0.0.1: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.

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `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 &",
    "until docker info >/dev/null 2>&1; do sleep 1; done",
    "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")

result, err := sandbox.Cmd(ctx, "sh",
    sandbox0.WithCommand([]string{"/bin/sh", "-lc", script}),
)
if err != nil {
    log.Fatal(err)
}
if !strings.Contains(result.OutputRaw, "hello from docker in sandbox") {
    log.Fatalf("docker build test failed:\\n%s", result.OutputRaw)
}
fmt.Print(result.OutputRaw)`
    },
    {
      label: "Python",
      language: "python",
      code: `script = """
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 &
  until docker info >/dev/null 2>&1; do sleep 1; done
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
"""

result = sandbox.cmd(
    "sh",
    CmdOptions(command=["/bin/sh", "-lc", script]),
)
if "hello from docker in sandbox" not in result.output_raw:
    raise RuntimeError("docker build test failed:\\n" + result.output_raw)
print(result.output_raw, end="")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const script = [
    '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 &',
    'until docker info >/dev/null 2>&1; do sleep 1; done',
    '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',
].join('\\n');

const result = await sandbox.cmd('sh', {
    command: ['/bin/sh', '-lc', script],
});
if (!result.outputRaw.includes('hello from docker in sandbox')) {
    throw new Error('docker build test failed:\\n' + result.outputRaw);
}
process.stdout.write(result.outputRaw);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 sandbox exec "$SANDBOX_ID" --stream -- /bin/sh -lc '
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 &
  until docker info >/dev/null 2>&1; do sleep 1; done
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
'`
    }
  ]}
/>

---

## 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](https://kind.sigs.k8s.io/docs/user/known-issues/#older-linux-distributions) 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.
- Docker state does not survive runtime replacement. 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

<CardGroup>
  <Card title="Sandbox Services" href="/docs/sandbox/services" cta="Continue">
    Expose named sandbox ports through public HTTP service routes.
  </Card>

  <Card title="Snapshot And Restore" href="/docs/sandbox/snapshot-restore" cta="Continue">
    Publish initialized source code, test fixtures, and generated artifacts as rootfs state.
  </Card>
</CardGroup>
