Browserless Docker Setup: Self-Hosting Headless Chrome

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/chromium

For a specific version (recommended in production):

docker pull ghcr.io/browserless/chromium:2.18.0

Check 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/chromium

Then 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.png

If 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/chromium

Requests 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=60000

Tune 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-here

Then:

docker-compose up -d
docker-compose ps          # check it's running
docker-compose logs -f     # watch startup logs

The 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 /health

Returns {"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: 10

Resource 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 -d

In-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.

Read more

Start now free