Skip to main content
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.zst archive itself. On an older Python, add zstd to the packages.
  • Packages. The libraries are the ones ldd reports missing for the browser on this base. Linux servers has the same list for Debian 12 and Ubuntu 22.04. xvfb adds 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 install puts the browser and the Widevine module in the image (685 MiB). apostate fonts install windows installs 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 the apostate user 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:
An amd64 image on an Apple silicon Mac runs under emulation. Use that only to check the build. Hosts has what emulation shows to a detector.

Headed

The package starts Xvfb inside the container and stops it when the browser closes. The output is the same as above. Unlike a headless launch, a headed one gets a WebGPU adapter, and its first tab shows a 56-pixel bar for --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 /tmp and 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=1g or --ipc=host, or pass --disable-dev-shm-usage.
The binary needs --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 on node:22-trixie-slim, which also needs ca-certificates for git:
The 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.