Open Source Docker Deployment
The open-source Browserless image runs headless browsers inside Docker with built-in support for Puppeteer, Playwright, and REST APIs. It's free, self-hosted, and available on GitHub Container Registry.
What you get:
- Browser images for Chromium, Chrome, Firefox, WebKit, and Edge
- Native Puppeteer and Playwright connectivity over WebSocket
- REST APIs for screenshots, PDFs, scraping, and more
- Session management, health checks, and a built-in debugger UI
The open-source image covers core browser automation. If you need BrowserQL, stealth/CAPTCHA solving, session recording, live debugging, webhooks, or OpenTelemetry, see Enterprise Docker.
Available images
All images are published to ghcr.io/browserless/ and support both linux/amd64 and linux/arm64 architectures.
| Image | Browsers Included | Pull Command |
|---|---|---|
chromium | Chromium | docker pull ghcr.io/browserless/chromium |
chrome | Chrome | docker pull ghcr.io/browserless/chrome |
firefox | Firefox | docker pull ghcr.io/browserless/firefox |
webkit | WebKit | docker pull ghcr.io/browserless/webkit |
edge | Microsoft Edge | docker pull ghcr.io/browserless/edge |
multi | All of the above | docker pull ghcr.io/browserless/multi |
Chrome and Edge are only available on linux/amd64. The multi image on ARM includes Chromium, Firefox, and WebKit only.
Quick start
Run the Container
Start a Browserless container with a token and concurrency limit:
docker run \
--rm \
-p 3000:3000 \
-e "CONCURRENT=10" \
-e "TOKEN=6R0W53R135510" \
ghcr.io/browserless/chromiumwarningBrowserless always requires a token. If you don't set
TOKEN, Browserless generates a random token and prints it to stdout on startup.Connect Your Application
Point your automation library at the running container:
- Puppeteer
- Playwright
const puppeteer = require("puppeteer-core");
const browser = await puppeteer.connect({
browserWSEndpoint: "ws://localhost:3000?token=6R0W53R135510",
});
const page = await browser.newPage();
await page.goto("https://example.com");
const screenshot = await page.screenshot();
await browser.close();- JavaScript
- Python
- Java
- C#
const { chromium } = require("playwright");
const browser = await chromium.connectOverCDP(
"ws://localhost:3000?token=6R0W53R135510"
);
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://example.com");
const screenshot = await page.screenshot({ type: "png" });
await browser.close();from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(
"ws://localhost:3000?token=6R0W53R135510"
)
context = browser.new_context()
page = context.new_page()
page.goto("https://example.com")
screenshot = page.screenshot(type="png")
browser.close()import com.microsoft.playwright.*;
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium()
.connectOverCDP("ws://localhost:3000?token=6R0W53R135510");
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://example.com");
byte[] screenshot = page.screenshot(
new Page.ScreenshotOptions().setType(ScreenshotType.PNG)
);
browser.close();
}using Microsoft.Playwright;
var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium
.ConnectOverCDPAsync("ws://localhost:3000?token=6R0W53R135510");
var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();
await page.GotoAsync("https://example.com");
var screenshot = await page.ScreenshotAsync(
new PageScreenshotOptions { Type = ScreenshotType.Png }
);
await browser.CloseAsync();Verify
Confirm the instance is running with a REST API call:
curl "http://localhost:3000/chromium/content?token=6R0W53R135510&url=https://example.com"You should get back the rendered HTML of
example.com:<!doctype html>
<html>
<head>
<title>Example Domain</title>
...Or open
http://localhost:3000in your browser to see the built-in docs and debugger UI.
Docker compose
For repeatable deployments, use a docker-compose.yml:
services:
browserless:
image: ghcr.io/browserless/chromium
ports:
- "3000:3000"
environment:
- TOKEN=6R0W53R135510
- CONCURRENT=10
- QUEUED=10
- TIMEOUT=30000
shm_size: "2g"
restart: unless-stopped
docker compose up -d
Always set shm_size: "2g" (or --shm-size=2g on the CLI). Docker defaults to 64 MB of shared memory, which causes Chrome to crash under load.
Endpoints
The open-source image exposes REST APIs, WebSocket endpoints, and management routes.
REST APIs
Each browser has its own path prefix. For Chromium:
| Endpoint | Method | Description |
|---|---|---|
/chromium/content | POST | Returns rendered HTML content of a page |
/chromium/pdf | POST | Generates a PDF from a page |
/chromium/screenshot | POST | Captures a viewport screenshot as PNG or JPEG |
/chromium/scrape | POST | Scrapes structured data from a page |
/chromium/download | POST | Downloads a file triggered by page interaction |
/chromium/function | POST | Runs a custom function with a browser context |
/chromium/performance | POST | Returns performance metrics for a page |
Replace /chromium with /chrome, /firefox, /webkit, or /edge when using those browser images.
All REST endpoints accept a ?token= query parameter for authentication.
WebSocket endpoints
Connect automation libraries over WebSocket:
| Endpoint | Description |
|---|---|
/chromium/playwright | Playwright WebSocket connection |
/?token= (root) | Puppeteer CDP WebSocket connection |
/chromium | CDP connection for Chromium |
Management endpoints
| Endpoint | Method | Description |
|---|---|---|
/active | GET | Liveliness probe (returns 204) |
/config | GET | Current server configuration |
/metrics | GET | Session and performance metrics |
/metrics/total | GET | Aggregate metrics |
/pressure | GET | Current CPU, memory, and queue pressure |
/sessions | GET | Active browser sessions |
Configuration reference
Configure the container with environment variables. Pass them with -e on the CLI or in the environment block of your Compose file.
Core settings
| Variable | Description | Default |
|---|---|---|
TOKEN | API token for authenticating requests. If unset, Browserless generates a random token on startup. | (random) |
CONCURRENT | Maximum concurrent browser sessions. Additional requests queue. | 10 |
QUEUED | Maximum queued requests. Once full, new requests get a 429 response. | 10 |
TIMEOUT | Session timeout in milliseconds. Set to -1 to disable (make sure your code closes browsers). | 30000 |
PORT | Internal HTTP port. Map externally with Docker's -p flag. | 3000 |
HOST | Address to bind to. Set to 0.0.0.0 in Docker so other containers on the same network can connect. | localhost |
Storage
| Variable | Description | Default |
|---|---|---|
DATA_DIR | User data directory for cookies, cache, and local storage. Mount a volume for persistence. | OS temp dir |
DOWNLOAD_DIR | Directory for file downloads. Mount a volume to access downloaded files. | OS temp dir |
METRICS_JSON_PATH | Path to persist metrics across restarts. | OS temp dir |
Health checks
| Variable | Description | Default |
|---|---|---|
HEALTH | Enable pre-request health checks. Returns 503 when CPU or memory usage exceeds the threshold. | false |
MAX_CPU_PERCENT | CPU usage threshold (requires HEALTH=true). | 99 |
MAX_MEMORY_PERCENT | Memory usage threshold (requires HEALTH=true). | 99 |
Networking & security
| Variable | Description | Default |
|---|---|---|
CORS | Enable CORS headers. | false |
CORS_ALLOW_ORIGIN | Allowed CORS origins. | * |
CORS_ALLOW_METHODS | Allowed HTTP methods for CORS. | OPTIONS, POST, GET |
CORS_MAX_AGE | CORS preflight cache max age in seconds. | 2592000 |
ALLOW_GET | Allow GET requests with URL-encoded JSON body. | false |
ALLOW_FILE_PROTOCOL | Allow file:// URLs. | false |
EXTERNAL | Public-facing URL for link generation (when behind a reverse proxy). | (none) |
Debugging
| Variable | Description | Default |
|---|---|---|
DEBUG | Debug log namespaces (debug module). * for all, -* for none. | browserless*,-**:verbose |
TZ | Container timezone. | UTC |
ENABLE_DEBUGGER | Show the built-in debugger UI at the root URL. | true |
Webhook alerts
| Variable | Trigger |
|---|---|
QUEUE_ALERT_URL | Requests start queuing |
REJECT_ALERT_URL | Requests rejected (429) |
TIMEOUT_ALERT_URL | Sessions timeout |
ERROR_ALERT_URL | Unhandled errors |
FAILED_HEALTH_URL | Health check failures |
Common recipes
Set the timezone
docker run --rm -p 3000:3000 \
-e "TOKEN=6R0W53R135510" \
-e "TZ=America/New_York" \
ghcr.io/browserless/chromium
Tune concurrency and queueing
Allow 20 concurrent sessions with a queue of 30, and a 5-minute session timeout:
docker run --rm -p 3000:3000 \
-e "TOKEN=6R0W53R135510" \
-e "CONCURRENT=20" \
-e "QUEUED=30" \
-e "TIMEOUT=300000" \
ghcr.io/browserless/chromium
Use a proxy
Pass proxy settings per-session through launch arguments. Browserless doesn't bundle a proxy server, so you'll need to bring your own.
- Puppeteer
- Playwright
const browser = await puppeteer.connect({
browserWSEndpoint:
"ws://localhost:3000?token=6R0W53R135510&--proxy-server=http://proxy.example.com:8080",
});
const browser = await chromium.connectOverCDP(
"ws://localhost:3000?token=6R0W53R135510&--proxy-server=http://proxy.example.com:8080"
);
Persist user data (sessions, cookies, cache)
Mount a volume to DATA_DIR so browser data survives container restarts:
docker run --rm -p 3000:3000 \
-e "TOKEN=6R0W53R135510" \
-e "DATA_DIR=/data" \
-v /path/on/host:/data \
ghcr.io/browserless/chromium
This forces all sessions to share the same user data directory. To use different profiles per session, override the user data directory in your automation code via launch arguments instead.
Enable health checks
Reject new sessions when the container is under heavy load:
docker run --rm -p 3000:3000 \
-e "TOKEN=6R0W53R135510" \
-e "HEALTH=true" \
-e "MAX_CPU_PERCENT=80" \
-e "MAX_MEMORY_PERCENT=80" \
ghcr.io/browserless/chromium
Run behind a reverse proxy
When Browserless sits behind NGINX or another proxy, set EXTERNAL so generated URLs (like those in the /sessions response) point to the correct address:
docker run --rm -p 3000:3000 \
-e "TOKEN=6R0W53R135510" \
-e "EXTERNAL=https://browserless.yourcompany.com" \
ghcr.io/browserless/chromium
For full load-balancing setups, see NGINX Load Balancing.
Upgrading
Images on ghcr.io/browserless/ use a latest tag and versioned tags (e.g., ghcr.io/browserless/chromium:2.26.1).
To upgrade:
- Pull the new image:
docker pull ghcr.io/browserless/chromium:latest - Stop and remove the running container.
- Start a new container with the same configuration.
For Compose deployments:
docker compose pull
docker compose up -d
Use a specific version tag instead of latest so upgrades are intentional:
image: ghcr.io/browserless/chromium:2.26.1
Check the GitHub releases for changelogs and breaking changes before upgrading.
Open Source vs. Cloud vs. Enterprise
| Feature | Open Source | Cloud | Enterprise Docker |
|---|---|---|---|
| Puppeteer / Playwright | Yes | Yes | Yes |
| REST APIs (screenshot, PDF, scrape) | Yes | Yes | Yes |
| Session management | Basic | Managed | Advanced (priority, persistence) |
| BrowserQL | No | Yes | Yes |
| Stealth / CAPTCHA solving | No | Yes | Yes |
| Session recording | No | Yes | Yes |
| Live debugger | No | Yes | Yes |
| Webhooks | Alerts only | N/A | Full lifecycle events |
| OpenTelemetry | No | N/A | Yes |
| Token roles | No | N/A | Yes |
| Managed proxies | No | Yes | No (bring your own) |
| Support | Community (GitHub Issues) | Priority engineering support | |
| Pricing | Free | Usage-based | License-based |
Ready to upgrade? See the Enterprise Deployment Guide or contact sales.
Troubleshooting & FAQ
Chrome crashes with "out of memory"
Docker defaults shared memory (/dev/shm) to 64 MB. Chrome needs more.
Fix: Add --shm-size=2g to your docker run command, or shm_size: "2g" in Compose.
"Connection refused" when connecting from another container
Browserless binds to localhost by default, which isn't reachable from other containers on the same Docker network.
Fix: Set HOST=0.0.0.0:
docker run --rm -p 3000:3000 \
-e "TOKEN=6R0W53R135510" \
-e "HOST=0.0.0.0" \
ghcr.io/browserless/chromium
How do I find the auto-generated token?
If you don't set TOKEN, Browserless generates one on startup and prints it to stdout. Check your container logs:
docker logs <container_id>
Getting 429 responses
Your request queue is full. Either increase CONCURRENT and QUEUED, or slow down your request rate. Check /pressure to see current load.
Sessions hang or don't close
Set a reasonable TIMEOUT (default is 30 seconds). If you set TIMEOUT=-1, make sure your code calls browser.close(). Otherwise sessions stay open indefinitely and consume resources.
Fonts or characters render incorrectly
The Docker images include common fonts for Latin, CJK, Arabic, Thai, and Indic scripts. If you need additional fonts, extend the image with your own Dockerfile.
How do I use Firefox or WebKit instead of Chromium?
Use the corresponding image and WebSocket path:
# Firefox
docker run --rm -p 3000:3000 -e "TOKEN=6R0W53R135510" ghcr.io/browserless/firefox
# WebKit
docker run --rm -p 3000:3000 -e "TOKEN=6R0W53R135510" ghcr.io/browserless/webkit
Connect with Playwright (Firefox and WebKit don't support CDP, so Puppeteer won't work):
const browser = await playwright.firefox.connect("ws://localhost:3000/firefox/playwright?token=6R0W53R135510");
Can I run multiple browsers in one container?
Yes, use the multi image. It includes all supported browsers and exposes each on its own path (/chromium, /chrome, /firefox, /webkit, /edge).
Where do I report bugs or request features?
Open an issue on GitHub.