Skip to main content
Sandboxes run inside gVisor (runsc). Docker is not installed automatically — you install it yourself after the sandbox is ready, then start dockerd with a small bootstrap script so nested containers work.

When to use this recipe

  • Spinning up Postgres, Redis, or other services with docker compose during a test run.
  • Letting an agent build and run containers without host Docker access.
  • Prototyping a multi-service setup in a disposable environment.

Prerequisites

  • A Brimble account with sandbox access and BRIMBLE_SANDBOX_KEY set.
  • The SDK installed (see SDKs).
  • Outbound network for pulling packages and images (egress.mode defaults to open).
  • A Debian/Ubuntu template such as ubuntu-24, node-22, or python-3.12 (images must have apt).
  • 1 GB+ RAM recommended (specs.memory: 1024 or higher).

Why a custom dockerd startup?

A plain apt install docker.io plus default dockerd often fails inside gVisor with overlay mount errors. Use a startup script that:
  1. Mounts tmpfs on /var/lib/docker — avoids overlay: invalid argument when pulling and running images.
  2. Runs dockerd with --iptables=false and --ip6tables=false — Docker does not manage iptables inside the sandbox; you set SNAT manually instead.
  3. Passes --feature containerd-snapshotter=false on Docker ≥ 29 — the default containerd snapshotter does not work in gVisor; the legacy storage path does.
Save this as start-dockerd.sh inside the sandbox (for example under /usr/local/bin/):
start-dockerd.sh
#!/bin/bash
set -xe -o pipefail

USE_OVERLAY_DRIVER=true
while [[ $# -gt 0 ]]; do
  case $1 in
    --no-overlay)
      USE_OVERLAY_DRIVER=false
      shift
      ;;
    *)
      shift
      ;;
  esac
done

if [[ "${USE_OVERLAY_DRIVER}" == "true" ]]; then
  current_fs=$(stat -f -c %T /var/lib/docker 2>/dev/null || echo "none")
  if [[ "${current_fs}" != "tmpfs" ]]; then
    mkdir -p /var/lib/docker
    mount -t tmpfs -o size="${DOCKER_TMPFS_SIZE:-512M}" tmpfs /var/lib/docker
  fi
fi

EXTRA_DOCKERD_FLAGS=()
docker_version=$(dockerd --version)
if [[ ${docker_version} =~ version\ ([0-9]+) ]] && [[ ${BASH_REMATCH[1]} -ge 29 ]]; then
  EXTRA_DOCKERD_FLAGS+=(--feature containerd-snapshotter=false)
fi

dev=$(ip route show default | sed 's/.* dev \([^ ]*\) .*/\1/')
addr=$(ip addr show dev "$dev" | grep -w inet | sed 's/^\s*inet \([^/]*\)\/.*$/\1/')

echo 1 > /proc/sys/net/ipv4/ip_forward
iptables-legacy -t nat -A POSTROUTING -o "$dev" -j SNAT --to-source "$addr" -p tcp
iptables-legacy -t nat -A POSTROUTING -o "$dev" -j SNAT --to-source "$addr" -p udp

exec dockerd --iptables=false --ip6tables=false "${EXTRA_DOCKERD_FLAGS[@]}"

Recipe

Create a sandbox, install Docker, upload the script, start the daemon, then run a container.
import { Sandbox } from "@brimble/sandbox";
import { readFileSync } from "node:fs";

const client = new Sandbox();

const handle = await client.sandboxes.create({
  region: "auto",
  template: "ubuntu-24",
  specs: { cpu: 500, memory: 1024 },
  egress: { mode: "open" },
});

try {
  const install = await handle.exec({
    cmd: "export DEBIAN_FRONTEND=noninteractive && apt-get update -qq && apt-get install -y docker.io iptables",
    timeout_seconds: 300,
  });
  if (install.exit_code !== 0) throw new Error(install.stderr || install.stdout);

  await handle.putFile(
    "/usr/local/bin/start-dockerd.sh",
    readFileSync("./start-dockerd.sh"),
  );

  const start = await handle.exec({
    cmd: "chmod +x /usr/local/bin/start-dockerd.sh && DOCKER_TMPFS_SIZE=512M nohup /usr/local/bin/start-dockerd.sh >/tmp/dockerd.log 2>&1 & sleep 12 && docker info >/dev/null",
    timeout_seconds: 120,
  });
  if (start.exit_code !== 0) throw new Error(start.stderr || start.stdout);

  const hello = await handle.exec({
    cmd: "docker run --rm hello-world",
    timeout_seconds: 300,
  });
  console.log(hello.stdout);
} finally {
  await handle.destroy();
}
exec requests accept timeout_seconds between 1 and 300. Split long apt-get runs or re-run install if needed.

Without uploading a file

You can inline the same steps with shell exec if you prefer not to upload start-dockerd.sh:
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq && apt-get install -y docker.io iptables
# paste start-dockerd.sh to /usr/local/bin/start-dockerd.sh, then:
chmod +x /usr/local/bin/start-dockerd.sh
DOCKER_TMPFS_SIZE=512M nohup /usr/local/bin/start-dockerd.sh >/tmp/dockerd.log 2>&1 &
sleep 12 && docker info && docker run --rm hello-world

What’s happening

  1. create. Blocks until the sandbox VM is ready (gVisor container running, no Docker yet).
  2. apt-get install docker.io. Installs the Docker engine and CLI on Debian/Ubuntu.
  3. start-dockerd.sh. Mounts tmpfs on /var/lib/docker, configures SNAT for outbound traffic, and starts dockerd --iptables=false --ip6tables=false with --feature containerd-snapshotter=false when Docker is version 29 or newer.
  4. docker run. Talks to the local daemon over /var/run/docker.sock inside the sandbox.
  5. --network=host. Port mapping via -p is not supported inside sandboxes — bind on the host network instead when exposing services.
If you pause and resume a sandbox, reinstall Docker and restart dockerd — the daemon does not persist across resume.

Limitations

  • Template OS: agent-only images (for example codex, bun-1) may not have apt; use ubuntu-24, node-22, or python-3.12.
  • Port mapping: use --network=host, not -p / --expose.
  • Install time: first apt-get install can take several minutes; use the maximum timeout_seconds (300) per exec call.
  • Performance: nested containers are slower than running Docker on a bare VM.
  • Resume: after pause/resume, run the install and start-dockerd.sh steps again.

Next steps

Last modified on July 1, 2026