Running Chromium on ARM64 Linux Servers: docker-compose Setup and Pitfalls
Google does not publish an official linux/arm64 build of Chrome. There is no google-chrome-stable package for ARM64, and there never has been. Chromium fills that gap, and on Debian-based ARM64 hosts, apt install chromium works fine. The two browsers share the same DevTools Protocol surface, so any automation library that targets CDP works against Chromium in the same way.
Use Chromium, Not Chrome, on ARM64 Linux
The Debian chromium package is maintained and updated regularly. Alpine also ships a chromium package, but its musl libc causes silent failures with some Node.js automation libraries that link against glibc. Stick with debian:bookworm-slim as your base image. Alpine saves about 20 MB and buys you hours of debugging. The maths is not in its favour.
Pin the version. If you leave the package unpinned, a container rebuild can pull a newer Chromium and break your automation without warning. Find the current package version and lock it:
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
chromium=120.0.6099.216-1~deb12u1 \
fonts-liberation \
libnspr4 \
libnss3 \
&& rm -rf /var/lib/apt/lists/*
Replace the version string with whatever apt-cache policy chromium returns on your target host before you write the Dockerfile. If the pinned version disappears from the repo, the build fails loudly. That is the right failure mode.
/dev/shm and the Non-Root User
Chromium uses /dev/shm for shared memory between the browser process and its renderer processes. Docker sets /dev/shm to 64 MB by default. Chromium wants closer to 256 MB before it starts behaving properly. Set it in your Compose file:
services:
chromium:
image: your-chromium-image:latest
shm_size: "256mb"
user: "1001:1001"
Running as root inside a container is the easy path and the wrong one. Create a non-root user in the Dockerfile and make sure that user owns /dev/shm access. The user: key in Compose handles the runtime UID drop. Combined with --no-sandbox covered below, this keeps the attack surface smaller.
RUN groupadd -r chrome && useradd -r -g chrome -G audio,video chrome
USER chrome
Headless Chrome ARM64 and the CDP Bind Change
From Chromium M113 onward, --remote-debugging-address=0.0.0.0 no longer works in headless mode. Chromium forces the CDP listener to bind to 127.0.0.1 whatever you pass. That breaks setups that try to connect to the CDP port from another container or from the host. The flag is ignored.
The fix is a socat proxy inside the Chromium container. Run socat as a sidecar process that forwards from 0.0.0.0:9222 to 127.0.0.1:9223, and start Chromium on port 9223:
RUN apt-get install -y --no-install-recommends socat
Then in your entrypoint:
#!/bin/bash
chromium \
--headless=new \
--no-sandbox \
--disable-gpu \
--remote-debugging-port=9223 \
--remote-debugging-address=127.0.0.1 &
socat TCP-LISTEN:9222,fork TCP:127.0.0.1:9223 &
wait
This keeps Chromium on loopback and exposes port 9222 for other containers to reach over the Docker Compose network.
Wiring It Up in docker-compose
Do not publish port 9222 to the host. If you are running monitoring dashboards or other automation on an ARM server, keep the CDP port on the internal Compose network only:
services:
chromium:
build: ./chromium
shm_size: "256mb"
user: "1001:1001"
networks:
- browser-net
automation:
image: your-automation-image:latest
depends_on:
chromium:
condition: service_healthy
networks:
- browser-net
networks:
browser-net:
driver: bridge
The automation container reaches Chromium at http://chromium:9222. The port is not reachable from outside the Compose network.
Verifying the WebSocket Endpoint Before Connecting
Your automation script should not assume Chromium is ready the moment the container starts. Add a healthcheck that polls the /json/version endpoint:
chromium:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9222/json/version"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
A 200 response from /json/version confirms the CDP WebSocket is live. The response body includes the webSocketDebuggerUrl, which your library should use directly rather than building the URL itself. On a cold ARM64 container start, Chromium usually takes 3 to 6 seconds before CDP responds. The start_period covers that.
The --no-sandbox Flag
--no-sandbox is required when running as a non-root user inside a Docker container because the kernel user namespace setup Chrome’s sandbox relies on is not available in most default container setups. The flag disables the renderer sandbox, not network isolation. The real host-level isolation comes from Docker’s seccomp profile and the fact that the container runs as a non-root UID.
Do not add --disable-web-security alongside it. That flag disables same-origin enforcement and has no place in an automation or monitoring container. Keep the flag list small:
chromium \
--headless=new \
--no-sandbox \
--disable-gpu \
--disable-dev-shm-usage \
--remote-debugging-port=9223
--disable-dev-shm-usage tells Chromium to use /tmp for shared memory if /dev/shm is too small. With shm_size: "256mb" set in Compose, this flag is a fallback rather than the main fix. Still worth keeping.
A non-root UID, a locked Chromium version, an internal-only CDP port, and a proper shm allocation are enough to run browser automation reliably on ARM64. None of it is complicated. The awkward bit is knowing which defaults get in the way.



