Skip to main content
Charlotte can run in a Docker container for isolated, reproducible deployments. There are two images with two different jobs:
  • Dockerfile (Debian) — the HTTP self-host image. Runs Charlotte Remote (streamable HTTP transport) with Chromium’s sandbox enabled. This is the image docker-compose.yml’s primary charlotte service 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):
Alpine, stdio mode (local/dev, ~1.2GB):

Running the container

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. Example claude_desktop_config.json:
(Claude Desktop drives Charlotte over stdio, so this uses the Alpine stdio image, not the HTTP image above.)

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: 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 see No 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).
The Alpine image’s sandbox posture is unverified — spike R4 only tested the Debian image (Puppeteer’s bundled Chromium). 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:
This is what the Alpine stdio image effectively assumes isn’t needed by running with the sandbox off instead — prefer that over unconfining seccomp on a sandbox-enabled container.

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.
stdio-only, Alpine image only. tests/docker-smoke-test.mjs spawns docker run -i --rm <image> and speaks JSON-RPC over the container’s stdin/stdout — that only works against an image whose CMD runs the stdio transport. Since the Debian Dockerfile now defaults to HTTP mode (--http, decision D22), this script no longer applies to it; run it only against charlotte:alpine. The Debian/HTTP image’s smoke coverage is the manual /healthz + real MCP-call-over-HTTP check in the “HTTP mode” section above (and the eventual I9 suite for Charlotte Remote).

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’s CMD 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=2gb is set
  • Debian/HTTP image: make sure you passed the seccomp profile or SYS_ADMIN fallback — see Sandbox and security posture; --security-opt seccomp=unconfined works too but is a much broader relaxation than this image needs
  • Alpine/stdio image: try --security-opt seccomp=unconfined if 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 (or cap_add: SYS_ADMIN as the fallback) — plain docker run charlotte:http with 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_SANDBOX isn’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-sandbox to Chromium itself in that case, so this isn’t an error condition for that image.

Chromium version mismatch (Alpine only)

If you see version warnings, switch to the Debian Dockerfile which bundles a compatible Chromium.