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.23, chromium 143+
- Image size: ~770MB
- Fonts:
font-noto,font-noto-emoji,font-wqy-zenhei,ttf-freefont - 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 and checks if <html is in the response. Customize it by exporting IMAGE_NAME, HOST_PORT, or SKIP_BUILD=1 if you already built the image.
Inspired by: