Run headless chromium in a minimal alpine container. This is a fork of jlandure/alpine-chrome adapted to new alpine and chrome versions plus some fonts.
Features:
- alpine 3.24, chromium 150+
- Image size: ~800MB
- Fonts:
font-noto,font-noto-emoji,font-wqy-zenhei,ttf-freefont— the generic families (sans-serif,serif,monospace) resolve to Noto; WenQuanYi serves CJK via fallback - Works with or without
--no-sandboxthanks to a seccomp profile - socat to access debug port (because chromium removed
remote-debugging-address=0.0.0.0) - s6-overlay to manage chromium and socat processes
- GitHub action to build (weekly) and push the image to ghcr.io
- Images for
linux/amd64andlinux/arm64 - Test script to verify basic functionality
Image:
ghcr.io/patte/alpine-chromium
docker run --rm \
--security-opt seccomp=./chrome.json \
-p 9222:9222 \
ghcr.io/patte/alpine-chromiumservices:
chrome:
image: ghcr.io/patte/alpine-chromium
security_opt:
- seccomp=./chrome.json
ports:
- '9222:9222'The Dockerfile sets default args for chromium in CHROMIUM_ARGS. You can overwrite them by setting the CHROMIUM_ARGS environment variable in your docker-compose.yml file. Make sure to keep at least: --headless --remote-debugging-port=9223 --disable-crash-reporter --no-crashpad.
Eg. without security_opt but --no-sandbox:
services:
chrome:
image: ghcr.io/patte/alpine-chromium
ports:
- '9222:9222'
environment:
CHROMIUM_ARGS: '--headless --no-sandbox --no-zygote --remote-debugging-port=9223 --disable-crash-reporter --no-crashpad'Connect puppeteer to the running container:
const browser = await puppeteer.connect({
browserURL: 'http://localhost:9222',
});Or if you want to use websockets, get webSocketDebuggerUrl from localhost:9222/json/version:
const chromeVersion = await fetch('http://localhost:9222/json/version').then((res) => {
if (!res.ok) {
throw new Error(`Can't connect to Chrome: ${res.statusText}`);
}
return res.json();
}).catch((e) => {
console.error('Failed to fetch chrome version', e);
throw new Error(`Failed to fetch chrome version`);
});
const browser = await puppeteer.connect({
browserWSEndpoint: chromeVersion.webSocketDebuggerUrl,
});Chrome's remote debugging only accepts connections with the host header set to localhost or an IP address (source). To use a different hostname, resolve it to an IP address first, e.g.:
async function connectToChrome() {
const chromeHost = env.NODE_ENV === 'production' ? 'systemd-chrome' : 'localhost';
// resolve hostname to ip, otherwise chrome responds with:
// "Host header is specified and is not an IP address or localhost."
const { address: hostname } = await dns.lookup(chromeHost, 4).catch((e) => {
console.error('Failed to resolve chrome host', e);
throw new Error(`Failed to resolve chrome host`);
});
const chromeVersion = await fetch(`http://${hostname}:9222/json/version`)
.then((res) => {
if (!res.ok) {
throw new Error(`Can't connect to Chrome: ${res.statusText}`);
}
return res.json();
})
.catch((e) => {
console.error('Failed to fetch chrome version', e);
throw new Error(`Failed to fetch chrome version`);
});
const browser = await puppeteer.connect({
browserWSEndpoint: chromeVersion.webSocketDebuggerUrl,
});
return browser;
}To manually build the image, run the following command:
docker build -t localhost/alpine-chromium .Run the same test that CI executes by calling scripts/test.sh:
./scripts/test.shThe script builds the image, starts the container with the seccomp profile, waits for localhost:9222/json/version to respond (confirming socat exposes Chromium's debug port), opens https://example.com through the /json/new endpoint, renders a test page (latin text, color emoji, CJK) to a screenshot inside the container and compares it against scripts/test-golden.png (RMSE via ImageMagick, skipped if not installed), and fails on fatal GPU/sandbox errors in the container logs (Chromium can fall back to software compositing and look alive while its GPU process is broken). It then repeats the startup and log check with --disable-gpu args. Customize it by exporting IMAGE_NAME, HOST_PORT, or SKIP_BUILD=1 if you already built the image.
If a chromium or font update legitimately changes rendering, regenerate the golden image with:
UPDATE_GOLDEN=1 ./scripts/test.shInspired by: