Dockerfile(Debian) — the HTTP self-host image. Runs Charlotte Remote (streamable HTTP transport) with Chromium’s sandbox enabled. This is the imagedocker-compose.yml’s primarycharlotteservice builds. Full self-hosting walkthrough (token generation,publicOrigin/OAuth facade, tunnel setup): see Self-Hosting.Dockerfile.alpine— the local/dev stdio image. Runs the original stdio MCP transport for Claude Desktop-style local use, sandbox disabled. See Sandbox and security posture below for why these two images differ.
Quick start
Debian, HTTP mode (self-host):Running the container
HTTP mode (Debian image, recommended for self-hosting)
Your mounted
charlotte.config.json needs at least http.host: "0.0.0.0" so the server is reachable from outside the container (the code default, 127.0.0.1, is loopback-only — see Sandbox and security posture for why that default exists and Self-Hosting for the full config). See docker-compose.yml for the same invocation with comments on the seccomp/SYS_ADMIN tradeoff.stdio mode (Alpine image, local/dev)
Charlotte’s original transport communicates via stdio. To test it interactively:Using with Claude Desktop
To use the containerized Charlotte with Claude Desktop, you’ll need a wrapper that connects stdio to the container. Exampleclaude_desktop_config.json:
Important notes
Shared memory (shm_size)
Charlotte’s BrowserManager passes --disable-dev-shm-usage to Chromium, which makes it use /tmp instead of /dev/shm. This means --shm-size is not strictly required. However, the docker-compose file and examples below include --shm-size=2gb as a safety net — if the flag is ever removed or overridden, the default Docker shm size (64MB) would cause Chromium crashes.
Sandbox and security posture
Chromium’s own sandbox (user-namespaces + seccomp-BPF) is a load-bearing defense for the HTTP image: Charlotte navigates its browser to arbitrary, often untrusted, URLs, and in HTTP mode a hostile page would otherwise be exploiting the renderer on your server, not a visitor’s machine. Project decision D22 settled this after a spike (R4) confirmed Chromium’s own sandbox runs enabled in-container, without--privileged, on this host’s
Docker (27.4.1) — the Ubuntu unprivileged-userns AppArmor restriction that
normally blocks it can be worked around without relaxing AppArmor. There are
three postures, in the order the Debian HTTP image expects you to reach for
them:
1. Surgical seccomp profile (default, recommended)
docker/chrome-seccomp.json. It’s Docker’s own default seccomp profile
(pinned to Docker v27.4.1) with exactly one change: the 23-syscall
namespace/mount bucket (clone, clone3, unshare, setns, mount, …)
that default seccomp only allows to processes holding CAP_SYS_ADMIN is
un-gated from that capability requirement. Nothing else is relaxed; the
container does not get CAP_SYS_ADMIN or any other capability. Docker’s
default AppArmor profile is kept — do NOT add
--security-opt apparmor=unconfined. Counterintuitively, AppArmor’s
default (confined) profile is what grants the userns-creation exemption
on Ubuntu 23.10+/24.04-like hosts; unconfining AppArmor removes that
exemption and breaks the sandbox again. This is what docker-compose.yml
and the docker run example above use.
2. cap_add: [SYS_ADMIN] with default seccomp (fallback)
Also verified working in spike R4. Useful on hosts/platforms where
mounting a custom seccomp profile is awkward (some managed container
services). Broader than posture 1 — SYS_ADMIN grants far more than the
syscalls the sandbox actually needs (arbitrary ioctl, BPF, etc.) — so
prefer the seccomp profile when you can mount one. Commented out, ready
to swap in, in docker-compose.yml.
3. No relaxation needed
On hosts that don’t carry the unprivileged-userns AppArmor restriction (i.e. not an Ubuntu 23.10+/24.04-like kernel/AppArmor combo), Chromium’s sandbox may already initialize under Docker’s plain defaults. Worth trying first if you’re unsure which camp your host falls into; postures 1/2 are what to reach for if you seeNo usable sandbox! in the logs
(see Troubleshooting). The shipped docker-compose.yml already applies
posture 1 regardless, which is safe to leave on even on a host where
posture 3 would also work.
Do not run the HTTP image with
--no-sandbox / CHARLOTTE_NO_SANDBOX=1.
That’s a real option in browser.noSandbox / the CLI, but it’s a hardening
regression specifically in remote mode — charlotte doctor --http will
WARN about it on purpose (see Configuration for the flag).Dockerfile.alpine keeps
CHARLOTTE_NO_SANDBOX=1 (sandbox disabled) rather than guessing that the
surgical profile also works against Alpine’s system Chromium package. If you
need a sandboxed Alpine image, that needs its own spike first.
If you encounter permission errors on an image/host combination none of the
above covers, the blunt (and much weaker) fallback is:
Network access
Charlotte needs network access to browse websites. By default, Docker provides this. If using custom networks:Image comparison
Alpine’s system Chromium pulls in many transitive dependencies (mesa, ffmpeg, pipewire, llvm, etc.) that negate the base image size advantage over Debian. The Debian image uses Puppeteer’s bundled Chromium for guaranteed compatibility, while Alpine may occasionally have version mismatches — and it’s now also the only image with a verified sandbox-enabled path, so it’s the one used for HTTP self-hosting.
Environment variables
Building for production
For CI/CD, build with cache optimization:Testing
A smoke test script exercises the full MCP tool suite against a running container. It sends JSON-RPC messages over stdio and verifies Charlotte can initialize, navigate, observe, find elements, type, click, screenshot, scroll, evaluate JS, and more.Prerequisites
The smoke test serves the sandbox pages from your host machine. The container accesses them via--network=host (Linux) or host.docker.internal (macOS/Windows).
Running the smoke test
What the smoke test covers
Test results (2026-02-23)
Historical — from before decision D22 switched the Debian image’sCMD to HTTP mode. The charlotte:debian row reflects the old stdio CMD; the stdio smoke test no longer applies to that image (see note above).
No performance regressions observed from the container environment. Both images complete the full tool suite in under 7 seconds.
Troubleshooting
”Failed to launch the browser process”
- Ensure
--shm-size=2gbis set - Debian/HTTP image: make sure you passed the seccomp profile or SYS_ADMIN fallback — see Sandbox and security posture;
--security-opt seccomp=unconfinedworks too but is a much broader relaxation than this image needs - Alpine/stdio image: try
--security-opt seccomp=unconfinedif you still hit permission errors with the sandbox already off - Check that the container has network access
”No usable sandbox!” / “Running without sandbox”
- Debian/HTTP image: this image runs with the sandbox ON by design (D22) — seeing this error means the seccomp/AppArmor posture isn’t right yet, not that something needs to be disabled. Check, in order: (1) you passed
--security-opt seccomp=./docker/chrome-seccomp.json(orcap_add: SYS_ADMINas the fallback) — plaindocker run charlotte:httpwith no security flags will fail this way, by design (config (a) in spike R4); (2) you did not also pass--security-opt apparmor=unconfined— combining it with the seccomp profile breaks the sandbox again (see Sandbox and security posture); (3)CHARLOTTE_NO_SANDBOXisn’t set to a truthy value in your environment/config, which would disable the sandbox on purpose. - Alpine image: expected — this image runs with
CHARLOTTE_NO_SANDBOX=1(sandbox off) because that posture is unverified for Alpine’s system Chromium. Charlotte’s BrowserManager passes--no-sandbox/--disable-setuid-sandboxto Chromium itself in that case, so this isn’t an error condition for that image.