examples/docker/ has a Dockerfile and a script, check.py, that launches a Windows persona and prints what a page reads. The commands and output on this page were run on 2026-09-27 with apostate 0.4.3 and Docker 29.4 (OrbStack) on an Apple M4 Max, for linux/arm64 and for linux/amd64 under emulation.
The Dockerfile
- Base image. Python 3.14 unpacks the browser’s
.tar.zstarchive itself. On an older Python, addzstdto the packages. - Packages. The libraries are the ones
lddreports missing for the browser on this base. Linux servers has the same list for Debian 12 and Ubuntu 22.04.xvfbadds 10 MB. Leave it out if you only run headless. - User. The image runs as
apostate(UID 1000). The driver the package uses passes--no-sandbox, so the container needs no extra capability. Under root, WebGPU returns no adapter even in a headed launch. See Run as a regular user. - Browser and fonts.
apostate installputs the browser and the Widevine module in the image (685 MiB).apostate fonts install windowsinstalls 142 font files (341 MiB). From 0.4.4 it also installs Marlett. Containers start without downloading anything. The image is 663 MB compressed. profiles. A named volume mounted on this directory takes its owner, so theapostateuser can write to it.
Build and run
segoeUI is True when text set in Segoe UI measures differently from the fallback font, so the fonts in the image reach the page. check.py passes locale and timezone, so no GeoIP lookup runs.
For an x86 server, build for linux/amd64:
Headed
--no-sandbox, which the browser needs in a container. Linux servers has the measurements.
Keep profiles on a volume
--profile calls launch_persistent_context("profiles/shop", ...) with no seed. The first run draws a machine and stores its seed in profiles/shop/apostate/identity on the volume. The second run printed the same machine. To bind-mount a host directory instead, make it writable by UID 1000.
Shared memory
Chromium keeps shared memory in/dev/shm. Docker Engine gives a container 64 MB there unless you pass --shm-size. The results below were measured with --shm-size=64m.
- Through the package. Its driver passes
--disable-dev-shm-usage, so Chromium uses/tmpand 64 MB is enough. A page drawing 40 canvases of 3840x2160 finished with--shm-size=64m. - The browser binary on its own. With 64 MB, the same page crashed the browser once and hung it twice. Start the container with
--shm-size=1gor--ipc=host, or pass--disable-dev-shm-usage.
--no-sandbox in a container even as a regular user. Without it, it exits with No usable sandbox!, because Docker’s default seccomp profile blocks the user namespaces Chromium’s sandbox uses. With --security-opt seccomp=unconfined, it started with the sandbox on. Raw binary covers the other switches the packages add.
CPU and memory limits
A persona never claims more cores or memory than the container can see.
The core count follows the CPUs the container may run on, not a CPU quota. Memory follows the host’s physical memory, not the container’s limit. Hosts explains the cap.
Node
The same packages onnode:22-trixie-slim, which also needs ca-certificates for git:
node user of the base image runs the browser. The Node code from Headed launches, saved as check.mjs, printed [ 'Win32', 1920, 1080, 143 ] in this image. The 143 includes the 56-pixel --no-sandbox bar on the first tab. A bar on the first tab has the workaround.