# 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](https://orkestr.eu/docs/sandboxes/templates) 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. ```curl 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](https://orkestr.eu/scripts/sandbox-docker-boot.sh). You can paste it into a recipe step instead of downloading it. ## 2. Run containers **Python:** ```python 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)) ``` **JavaScript:** ```javascript import { Sandbox } from "orkestr"; // "docker" resolves to the template's default tag. Docker starts on boot. await Sandbox.withTemp({ template: "docker", size: "medium", network: "open" }, async (sbx) => { await sbx.exec("docker-wait"); // blocks until the daemon answers const r = await sbx.exec("docker run --rm node:26-alpine node -e 'console.log(1+1)'"); console.log(r.stdout); // 2 // Publish a port the normal way, then reach it from the internet: await sbx.exec("docker run -d -p 8080:80 nginx:alpine"); console.log(await sbx.previewUrl(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](https://orkestr.eu/docs/sandboxes/networking) 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](https://orkestr.eu/docs/sandboxes/volumes) (`/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`. **Python:** ```python 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") ``` **JavaScript:** ```javascript import { Sandbox } from "orkestr"; // 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. const sbx = await Sandbox.create({ template: "docker", volume: "docker-cache", network: "restricted", allowDomains: ["docker.io", "docker.com"], }); await 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). ```python # 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).