Browserless Docker Setup: Self-Hosting Headless Chrome
Running Browserless yourself means you control everything: concurrency, memory, network, and cost. The Docker image packages Chrome, all its dependencies, and the Browserless API layer into a single container. Here's how to get it running and configured for real use.
Pulling the Image
Browserless v2 uses ghcr.io/browserless/chromium. The older v1 image (browserless/chrome on Docker Hub) still works but is no longer actively developed. This guide covers v2.
docker pull ghcr.io/browserless/chromiumFor a specific version (recommended in production):
docker pull ghcr.io/browserless/chromium:2.18.0Check the GitHub releases page for current tags.
Quick Start
Verify the image works before configuring anything:
docker run --rm -p 3000:3000 ghcr.io/browserless/chromiumThen hit the health endpoint:
curl http://localhost:3000/health
# {"status":"ok"}And test a screenshot:
curl -X POST http://localhost:3000/chromium/screenshot \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}' \
--output test.pngIf test.png is a valid image, your setup is working.
Environment Variables
These are the variables that matter most for production:
| Variable | Default | Purpose |
|---|---|---|
CONCURRENT |
10 |
Max simultaneous browser sessions |
QUEUED |
10 |
Sessions to queue when at max concurrent |
TIMEOUT |
30000 |
Request timeout in milliseconds |
TOKEN |
(none) | Auth token; if set, all requests require ?token= |
MAX_PAYLOAD_SIZE |
5mb |
Max request body size |
ALLOW_FILE_PROTOCOL |
false |
Allow file:// URLs (security risk, disable in prod) |
DEBUG |
(none) | Set to browserless* for verbose logging |
PORT |
3000 |
Port the service listens on |
HOST |
0.0.0.0 |
Bind address |
Setting a Token
Always set TOKEN in production. Without it, anyone who can reach the port can use your browser.
docker run --rm -p 3000:3000 \
-e TOKEN=mysecrettoken \
ghcr.io/browserless/chromiumRequests then require ?token=mysecrettoken in the query string or a Authorization: Bearer mysecrettoken header.
Concurrency and Queuing
CONCURRENT controls how many sessions run in parallel. QUEUED controls how many wait in line. When both limits are hit, Browserless returns a 429 with a Retry-After header.
For a server with 4 cores and 8GB RAM, a starting point:
-e CONCURRENT=5 \
-e QUEUED=10 \
-e TIMEOUT=60000Tune based on your workload. CPU is usually the bottleneck for rendering; memory is the bottleneck for concurrent sessions.
docker-compose Configuration
A complete docker-compose.yml for a self-hosted Browserless setup:
version: '3.8'
services:
browserless:
image: ghcr.io/browserless/chromium:2.18.0
restart: unless-stopped
ports:
- "3000:3000"
environment:
TOKEN: ${BROWSERLESS_TOKEN}
CONCURRENT: 10
QUEUED: 20
TIMEOUT: 60000
MAX_PAYLOAD_SIZE: 10mb
ALLOW_FILE_PROTOCOL: "false"
# Uncomment for debug logging:
# DEBUG: "browserless*"
deploy:
resources:
limits:
cpus: '4.0'
memory: 8G
reservations:
cpus: '1.0'
memory: 2G
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# Shared memory: Chrome needs it. Default 64MB is too small.
shm_size: '2gb'Store the token in a .env file next to your docker-compose.yml:
BROWSERLESS_TOKEN=your-secret-token-hereThen:
docker-compose up -d
docker-compose ps # check it's running
docker-compose logs -f # watch startup logsThe shm_size Requirement
Chrome uses /dev/shm (shared memory) for inter-process communication. Docker's default allocation is 64MB. Chrome will crash or behave erratically with anything complex under that limit.
Set shm_size: '2gb' in docker-compose, or pass --shm-size=2g on the docker command line. This allocates shared memory from host RAM — it doesn't reserve 2GB, it just sets the ceiling.
If you're seeing random crashes with no clear error, insufficient /dev/shm is usually the culprit.
Health Check Endpoint
GET /healthReturns {"status":"ok"} when the service is ready. Use this in:
- Docker health checks (shown above)
- Load balancer health probes
- Kubernetes liveness/readiness probes
For Kubernetes:
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10Resource Limits for Production
Memory
Each browser session uses roughly 150–400MB depending on page complexity. For CONCURRENT=10, budget at least 4–6GB of RAM for the container, plus headroom for the OS.
A rough formula: (CONCURRENT × 400MB) + 1GB overhead
At 10 concurrent sessions: (10 × 400) + 1000 = 5000MB → set memory limit to 6G.
CPU
Chrome is single-threaded per tab for JavaScript execution, but rendering and network I/O are parallel. A 4-core machine handles 10 concurrent sessions well for most workloads. Add cores before raising CONCURRENT past 15.
Disk
Chrome creates temporary files for each session. Set a low-storage alarm and monitor /tmp inside the container if you're running high-volume workloads.
Updating Browserless
With docker-compose, updating is:
docker-compose pull
docker-compose up -dIn-flight sessions will complete; new sessions use the new image after restart. For zero-downtime updates, run two instances behind a load balancer and roll one at a time.
Verifying Your Setup
A quick test script to confirm everything is configured correctly:
#!/bin/bash
TOKEN=your-secret-token-here
BASE_URL=http://localhost:3000
# Health check
echo "=== Health ==="
curl -sf "$BASE_URL/health" || { echo "FAIL: health check"; exit 1; }
echo ""
# Screenshot with auth
echo "=== Screenshot ==="
curl -sf -X POST "$BASE_URL/chromium/screenshot?token=$TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}' \
--output /tmp/browserless-test.png && echo "OK: screenshot saved" || echo "FAIL: screenshot"
# Test auth rejection
echo "=== Auth check ==="
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
"$BASE_URL/chromium/screenshot" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}')
[ "$STATUS" = "401" ] && echo "OK: auth working" || echo "WARN: expected 401, got $STATUS"If all three checks pass, your self-hosted Browserless is ready for Puppeteer and Playwright connections.