> For the complete documentation index, see [llms.txt](/llms.txt)

# Open Source Docker Deployment

The [open-source Browserless image](https://github.com/browserless/browserless) 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](https://github.com/browserless/browserless/pkgs/container/chromium).

**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

> **Looking for Enterprise?**
> 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](/enterprise/docker/overview).

## 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` |

> **ARM limitations**
> Chrome and Edge are only available on `linux/amd64`. The `multi` image on ARM includes Chromium, Firefox, and WebKit only.

## Quickstart

Start a Browserless container with a token and concurrency limit:

```bash
docker run \
  --rm \
  -p 3000:3000 \
  -e "CONCURRENT=10" \
  -e "TOKEN=6R0W53R135510" \
  ghcr.io/browserless/chromium
```

> **Warning**
> If you don't set `TOKEN`, Browserless does **not** generate one for you. Every endpoint is left unauthenticated, including `/function`, which runs arbitrary Puppeteer code from the request body. Set `TOKEN` for any deployment reachable beyond localhost.

Point your automation library at the running container:

  
**Puppeteer:**

```javascript
  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();
```
  
  
**Playwright:**

    
      
**JavaScript:**

```javascript
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();
```
      
      
**Python:**

```python
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()
```
      
      
**Java:**

```java
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();
}
```
      
      
**C#:**

```csharp
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();
```
      
    
  

Confirm the instance is running with a REST API call:

```bash
curl "http://localhost:3000/chromium/content?token=6R0W53R135510&url=https://example.com"
```

You should get back the rendered HTML of `example.com`:

```html
<!doctype html>
<html>
<head>
    <title>Example Domain</title>
...
```

Or open `http://localhost:3000` in your browser to see the built-in docs and debugger UI.

### Docker compose

For repeatable deployments, use a `docker-compose.yml`:

```yaml
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
```

```bash
docker compose up -d
```

> **Shared memory**
> 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 not set, all endpoints are unauthenticated, including `/function`. | (none) |
| `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. The image already sets this to `0.0.0.0`, which accepts connections on every interface inside the container. Reaching it from another container still needs a shared Docker network and a published port. The underlying library defaults to `localhost` only when run outside Docker. | `0.0.0.0` |

### 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](https://www.npmjs.com/package/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

```bash
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:

```bash
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:**

```javascript
const browser = await puppeteer.connect({
  browserWSEndpoint:
    "ws://localhost:3000?token=6R0W53R135510&--proxy-server=http://proxy.example.com:8080",
});
```
  
  
**Playwright:**

```javascript
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:

```bash
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:

```bash
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:

```bash
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](/enterprise/docker/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:**

1. Pull the new image:
```bash
docker pull ghcr.io/browserless/chromium:latest
```
2. Stop and remove the running container.
3. Start a new container with the same configuration.

For Compose deployments:

```bash
docker compose pull
docker compose up -d
```

> **Pin versions in production**
> Use a specific version tag instead of `latest` so upgrades are intentional:
> ```yaml
> image: ghcr.io/browserless/chromium:2.26.1
> ```

Check the [GitHub releases](https://github.com/browserless/browserless/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 |
| **Core REST APIs** (`/screenshot`, `/pdf`, `/content`, `/scrape`, `/function`, `/download`, `/performance`) | Yes | Yes | Yes |
| **Advanced REST APIs** (`/unblock`, `/smart-scrape`, `/search`, `/map`, `/crawl`, `/agent/run`) | No | Yes | No |
| **Session management** | Basic | Managed | Advanced (priority, persistence) |
| **BrowserQL** | No | Yes | Yes |
| **Stealth / CAPTCHA solving** | No | Yes | Yes |
| **`blockAds` parameter** | Yes | Yes | Yes |
| **`blockConsentModals` parameter** | 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) | Email | Priority engineering support |
| **Pricing** | Free | Usage-based | License-based |

Ready to upgrade? See the [Enterprise Deployment Guide](/enterprise/docker/enterprise-image) or [contact sales](https://www.browserless.io/contact).

## FAQ & Troubleshooting

<details>
<summary>Is Smart Scrape (<code>/smart-scrape</code>) available in the open-source image?</summary>

No. `/smart-scrape` is a Cloud-only endpoint, along with `/unblock`, `/search`, `/map`, `/crawl`, and `/agent/run`. The closest open-source equivalents are [`/scrape`](/rest-apis/scrape) for structured extraction and [`/content`](/rest-apis/content) for rendered HTML.

</details>

<details>
<summary>Can I use stealth or <code>/unblock</code> in the open-source image?</summary>

No. Stealth routes and CAPTCHA solving require Cloud or an [Enterprise license](/enterprise/docker/enterprise-image), and `/unblock` is Cloud-only. The open-source image runs standard browsers; you can still pass your own launch flags and proxies.

</details>

<details>
<summary>Do <code>blockAds</code> and <code>blockConsentModals</code> work in the open-source image?</summary>

`blockAds` works: the image ships a built-in ad blocker you can enable per request or connect call. `blockConsentModals` is not in the open-source image; it's available on Cloud and Enterprise.

</details>

<details>
<summary>Chrome crashes with "out of memory"</summary>

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.

</details>

<details>
<summary>"Connection refused" when connecting from another container</summary>

The image already binds to `0.0.0.0` by default, so this is usually something else: a firewall, the two containers not sharing a Docker network, or an explicit `HOST` override to something more restrictive like `127.0.0.1`. Confirm both containers are on the same network and that you haven't set `HOST` yourself.

</details>

<details>
<summary>Do I need to set a token?</summary>

Yes, unless the container is only reachable by you. Browserless doesn't generate one for you: leaving `TOKEN` unset leaves every endpoint unauthenticated, including `/function`, which executes arbitrary Puppeteer code from the request body. Set your own value with `-e "TOKEN=your-secret-value"`.

</details>

<details>
<summary>Getting 429 responses</summary>

Your request queue is full. Either increase `CONCURRENT` and `QUEUED`, or slow down your request rate. Check `/pressure` to see current load.

</details>

<details>
<summary>Sessions hang or don't close</summary>

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.

</details>

<details>
<summary>Fonts or characters render incorrectly</summary>

The Docker images include common fonts for Latin, CJK, Arabic, Thai, and Indic scripts. If you need additional fonts, [extend the image](/enterprise/docker/extending) with your own Dockerfile.

</details>

<details>
<summary>How do I use Firefox or WebKit instead of Chromium?</summary>

Use the corresponding image and WebSocket path:

```bash
# 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):

```javascript
const browser = await playwright.firefox.connect("ws://localhost:3000/firefox/playwright?token=6R0W53R135510");
```

</details>

<details>
<summary>Can I run multiple browsers in one container?</summary>

Yes, use the `multi` image. It includes all supported browsers and exposes each on its own path (`/chromium`, `/chrome`, `/firefox`, `/webkit`, `/edge`).

</details>

<details>
<summary>Where do I report bugs or request features?</summary>

Open an issue on [GitHub](https://github.com/browserless/browserless/issues).

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>