Sandboxes

Docker in sandboxes

A sandbox can run a stock Docker Engine - docker run, docker build, Compose, and port publishing on the default bridge network, with Docker's own firewall rules intact. Every sandbox is its own virtual machine with its own kernel, so containers you start inside it are isolated from other sandboxes the same way everything else in the sandbox is.

Use it for CI jobs, for agents that need to build and run a project's own Dockerfile, or for anything that ships as a Compose file.

1. Build a Docker template (once)

Install Docker Engine from Docker's official Debian repository into a custom template on top of debian-12. The template's start_command starts the Docker daemon on every boot, so sandboxes come up ready with no setup calls. The recipe needs open network while building because Docker's package repository is not on the default allowlist.

build the docker template
curl -X POST https://api.orkestr.eu/v1/templates \
  -H "Authorization: Bearer $ORKESTR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "docker",
    "base_template": "debian-12",
    "network": "open",
    "recipe": [
      "apt-get update && apt-get install -y ca-certificates curl iptables e2fsprogs",
      "install -m 0755 -d /etc/apt/keyrings && curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && echo \"deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable\" > /etc/apt/sources.list.d/docker.list",
      "apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin",
      "curl -fsSL https://orkestr.eu/scripts/sandbox-docker-boot.sh -o /usr/local/bin/orkestr-docker-boot && curl -fsSL https://orkestr.eu/scripts/sandbox-docker-wait.sh -o /usr/local/bin/docker-wait && chmod 0755 /usr/local/bin/orkestr-docker-boot /usr/local/bin/docker-wait"
    ],
    "start_command": "/usr/local/bin/orkestr-docker-boot"
  }'

# => { "id": "tmpl_01J...", "status": "building", ... }  (about a minute)

The boot script is short and readable: sandbox-docker-boot.sh. You can paste it into a recipe step instead of downloading it.

2. Run containers

docker run
from orkestr import Sandbox

# "docker" resolves to the template's default tag. Docker starts on boot.
with Sandbox.create(template="docker", size="medium", network="open") as sbx:
    sbx.exec("docker-wait")  # blocks until the daemon answers
    print(sbx.exec("docker run --rm node:26-alpine node -e 'console.log(1+1)'").stdout)  # 2

    # Publish a port the normal way, then reach it from the internet:
    sbx.exec("docker run -d -p 8080:80 nginx:alpine")
    print(sbx.preview_url(8080))

docker-wait returns as soon as the daemon is up, usually within a second or two of boot. Ports you publish with -p are reachable inside the sandbox on localhost, and from anywhere through the sandbox's preview URL for that port.

Where Docker keeps its data

Docker needs an ext4 filesystem for its images and containers. The boot script picks one for you:

  • With a volume attached, Docker's data goes on the volume (/persist/docker). It is disk-backed, does not use the sandbox's memory, and pulled images plus build cache survive into the next sandbox that attaches the volume. This is the recommended setup for real builds, and it works even on a small sandbox. Size the volume for your images.
  • Without a volume, Docker gets an in-memory disk. Only the blocks Docker actually writes use memory, but images still count against the sandbox's RAM, so use medium or large.
docker with a volume
from orkestr import Sandbox

# Docker's data lives on the volume: disk-backed, and pulled images and
# build cache are still there the next time a sandbox attaches it.
sbx = Sandbox.create(
    template="docker",
    volume="docker-cache",
    network="restricted",
    allow_domains=["docker.io", "docker.com"],
)
sbx.exec("docker-wait && docker build -t app . && docker run --rm app")

Networking

  • open - pulls from any registry.
  • restricted - add the registries you pull from to allow_domains. For Docker Hub that is docker.io and docker.com (entries cover their subdomains; Docker Hub serves image layers from a docker.com CDN host). The Docker daemon uses the sandbox's egress proxy automatically.
  • off - no pulls, but images already cached on an attached volume run fine.
Docker's firewall stays on
Docker installs its normal iptables (or nftables) rules inside the sandbox, including the rules that block direct access to published ports. You do not need, and should not set, DOCKER_INSECURE_NO_IPTABLES_RAW. Rules inside the sandbox never widen its egress policy: that is enforced outside the sandbox.

Options

Set these as environment variables when you create the sandbox:

  • ORKESTR_DOCKER_ARGS - extra dockerd flags.
  • ORKESTR_DOCKER_DISK_MB - size cap of the in-memory data disk when no volume is attached (default: what the sandbox's memory allows).
options
# Docker 29's nftables firewall backend instead of iptables:
Sandbox.create(template="docker", env={"ORKESTR_DOCKER_ARGS": "--firewall-backend=nftables"})

# Cap the in-memory data disk (MB) when no volume is attached:
Sandbox.create(template="docker", size="large", env={"ORKESTR_DOCKER_DISK_MB": "4096"})

Tested with

  • Docker Engine 29 from Docker's official Debian repository, on debian-12
  • Debian's default iptables-nft, and Docker 29's nftables firewall backend
  • docker run, docker build (BuildKit), Compose, -p port publishing
  • Pause and resume: the daemon and running containers are restored with the sandbox

Not supported: Swarm mode, and running virtual machines inside the sandbox (no nested virtualization).

Reading with an agent? This page is also plain markdown at /docs/sandboxes/docker.md, and the full docs index lives at /docs/llms.txt.