Multi-browser grid with automatic fingerprint injection. Manage pools of Camoufox, Chromium, and Firefox instances — each with a unique, realistic browser fingerprint — through a single API.
SDK ──POST /api/sessions──▶ Grid ──▶ fpservice (generates fingerprint)
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Camoufox Chromium Firefox
(C++ env vars) (context opts + (context opts +
init scripts) init scripts)
│ │ │
└───── ws_endpoint ◀──────────────┘
│
SDK ◀── BrowserContext ◀─┘
Tested against CreepJS and BrowserScan — the two most rigorous fingerprint detection tools available.
| Camoufox | Chromium | Firefox |
|---|---|---|
![]() |
![]() |
![]() |
| Camoufox | Chromium | Firefox |
|---|---|---|
![]() |
![]() |
![]() |
Each browser gets a unique, consistent fingerprint per session. Camoufox provides the deepest spoofing (C++ level), while Chromium and Firefox use Playwright context options and init scripts.
| Browser | Method | What gets spoofed |
|---|---|---|
| Camoufox | C++ level via CAMOU_CONFIG_* env vars |
Canvas, WebGL, fonts, navigator, screen, timezone — native spoofing undetectable by JS |
| Chromium | Playwright context options + JS init scripts | userAgent, viewport, locale, timezone, navigator props, screen geometry |
| Firefox | Playwright context options + JS init scripts | Same as Chromium |
Run BrowserGrid as a single container — grid + fpservice + all browsers in one image:
docker run -d --name browsergrid \
-p 9090:9090 \
--shm-size=2g \
ghcr.io/danield2g/browsergrid-standalone:latestThat's it. Dashboard at http://localhost:9090, API at http://localhost:9090/api/health.
With custom options:
docker run -d --name browsergrid \
-p 9090:9090 \
--shm-size=2g \
-e MAX_WORKERS=50 \
-e IDLE_TIMEOUT=120 \
-e API_KEY=your-secret-key \
-v browsergrid-data:/data \
ghcr.io/danield2g/browsergrid-standalone:latestDownload the production compose file and start:
curl -fsSL https://raw.githubusercontent.com/DanielD2G/BrowserGrid/main/setup.sh | bashOr manually:
curl -fsSL https://raw.githubusercontent.com/DanielD2G/BrowserGrid/main/docker-compose.prod.yml -o docker-compose.prod.yml
docker compose -f docker-compose.prod.yml up -dgit clone https://github.com/DanielD2G/BrowserGrid.git
cd BrowserGrid
docker compose up -dThis uses docker/Dockerfile.all which includes Camoufox, Chromium, and Firefox — all precompiled and ready to use.
For smaller images, use a browser-specific Dockerfile:
# docker-compose.yml
services:
grid:
build:
context: .
dockerfile: docker/Dockerfile.camoufox # or .chromium, .firefoxdocker compose build && docker compose up -dcurl http://localhost:9090/api/healthOpen http://localhost:9090 for the dashboard with live view.
Python:
pip install ./sdkTypeScript / JavaScript (Node & Bun):
npm install browsergrid-sdk
# or
bun add browsergrid-sdkimport { BrowserGrid } from "browsergrid-sdk";
const grid = new BrowserGrid("http://localhost:9090");
const { context, close } = await grid
.configure({ os: "windows", browserType: "chromium" })
.connect();
const page = await context.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "screenshot.png" });
await close();await using conn = await new BrowserGrid("http://localhost:9090")
.configure({ os: "windows" })
.connect();
const page = await conn.context.newPage();
await page.goto("https://example.com");
// automatically closed on scope exitconst grid = new BrowserGrid("http://localhost:9090");
const session = await grid
.configure({ browserType: "chromium", os: "windows" })
.getSession();
console.log(session.wsEndpoint); // "ws://localhost:9090/ws?session_id=..."
console.log(session.contextOptions); // { user_agent: "...", viewport: {...} }
console.log(session.initScripts); // ["Object.defineProperty(navigator, ...)"]
// Connect manually with playwright-core
import { chromium } from "playwright-core";
const browser = await chromium.connect(session.wsEndpoint);
const context = await browser.newContext(session.contextOptions);
for (const script of session.initScripts) {
await context.addInitScript(script);
}
const page = await context.newPage();
await page.goto("https://example.com");
await context.close();
await browser.close();
await grid.kill();const ws = await new BrowserGrid("http://localhost:9090")
.configure({ os: "windows" })
.getWs();
// "ws://localhost:9090/ws?session_id=abc-123"const grid = new BrowserGrid("http://localhost:9090");
const { context, close } = await grid
.configure({ browserType: "camoufox" })
.connect();
const page = await context.newPage();
await page.goto("https://example.com/export");
// ... trigger a download ...
const files = await grid.listDownloads();
await grid.saveDownload(files[0].name, "./downloads/");
await close();const grid = new BrowserGrid("http://localhost:9090", "your-secret-key");// Session 1: login and accumulate cookies
{
await using conn = await new BrowserGrid("http://localhost:9090")
.configure({ contextKey: "my-user", browserType: "chromium", os: "windows" })
.connect();
const page = await conn.context.newPage();
await page.goto("https://example.com/login");
// cookies saved automatically on close
}
// Session 2: same fingerprint, cookies restored
{
await using conn = await new BrowserGrid("http://localhost:9090")
.configure({ contextKey: "my-user", browserType: "chromium" })
.connect();
const page = await conn.context.newPage();
await page.goto("https://example.com/dashboard");
// already logged in
}import { BrowserGrid } from "browsergrid-sdk";
const grid = new BrowserGrid("http://localhost:9090");
grid.configure({
browserType: "camoufox", // "camoufox" | "chromium" | "firefox"
os: "windows", // "windows" | "macos" | "linux"
headless: false,
proxy: "http://proxy:8080",
geoip: true, // auto-set timezone/locale from IP
locale: "en-US",
blockImages: false,
blockWebrtc: true,
humanize: true,
contextKey: "my-user", // reuse fingerprint + cookies
wait: true, // wait for capacity (default: true)
waitTimeout: 300, // seconds to wait (default: 300)
});from browser_grid import BrowserGrid
# Camoufox (default) — deepest fingerprint spoofing
with BrowserGrid("http://localhost:9090").configure(os="windows") as ctx:
page = ctx.new_page()
page.goto("https://example.com")
page.screenshot(path="screenshot.png")
# Chromium
with BrowserGrid("http://localhost:9090").configure(browser_type="chromium", os="macos") as ctx:
page = ctx.new_page()
page.goto("https://example.com")
# Firefox (Playwright's Firefox, not Camoufox)
with BrowserGrid("http://localhost:9090").configure(browser_type="firefox", os="linux") as ctx:
page = ctx.new_page()
page.goto("https://example.com")The context manager returns a Playwright BrowserContext with the fingerprint already injected. Create pages, navigate, take screenshots — standard Playwright API.
from browser_grid import AsyncBrowserGrid
async with AsyncBrowserGrid("http://localhost:9090").configure(browser_type="chromium", os="windows") as ctx:
page = await ctx.new_page()
await page.goto("https://example.com")Use get_session() when you need direct access to the connection details:
from browser_grid import BrowserGrid
from playwright.sync_api import sync_playwright
grid = BrowserGrid("http://localhost:9090")
session = grid.configure(browser_type="chromium", os="windows").get_session()
print(session.browser_type) # "chromium"
print(session.ws_endpoint) # "ws://localhost:9090/ws?session_id=..."
print(session.context_options) # {"user_agent": "...", "viewport": {...}, ...}
print(session.init_scripts) # ["Object.defineProperty(navigator, ...)"]
# Connect manually
with sync_playwright() as pw:
browser = pw.chromium.connect(session.ws_endpoint)
context = browser.new_context(**session.context_options)
for script in session.init_scripts:
context.add_init_script(script)
page = context.new_page()
page.goto("https://example.com")
context.close()
browser.close()
grid.kill() # destroy the sessionFor integrations that just need the raw WebSocket URL:
ws = BrowserGrid("http://localhost:9090").configure(os="windows").get_ws()
# "ws://localhost:9090/ws?session_id=abc-123"from browser_grid import BrowserGrid, SessionConfig
config = SessionConfig(
browser_type="camoufox", # "camoufox" | "chromium" | "firefox"
os="windows", # "windows" | "macos" | "linux"
headless=False, # True for headless mode
proxy_server="http://proxy:8080",
proxy_username="user",
proxy_password="pass",
geoip=True, # auto-set timezone/locale from IP geolocation
locale="en-US",
block_images=False,
block_webrtc=True,
humanize=True, # human-like mouse/keyboard timing
)
with BrowserGrid("http://localhost:9090").configure(config) as ctx:
page = ctx.new_page()
page.goto("https://example.com")# Start grid with API key
docker compose up -d # set API_KEY env vargrid = BrowserGrid("http://localhost:9090", api_key="your-secret-key")Use context_key to reuse the same fingerprint and cookies across sessions. The grid saves the fingerprint on first use, and the SDK automatically persists storage state (cookies, localStorage) on exit.
# Session 1: browse and accumulate cookies
with BrowserGrid("http://localhost:9090").configure(
context_key="my-user", browser_type="chromium", os="windows"
) as ctx:
page = ctx.new_page()
page.goto("https://example.com/login")
# ... login, cookies are saved automatically on exit
# Session 2: same fingerprint, cookies restored
with BrowserGrid("http://localhost:9090").configure(
context_key="my-user", browser_type="chromium"
) as ctx:
page = ctx.new_page()
page.goto("https://example.com/dashboard")
# Already logged in — cookies were restoredDelete a context to reset the identity:
curl -X DELETE http://localhost:9090/api/contexts/my-userAll endpoints accept an optional Authorization: Bearer <api_key> header.
curl -X POST http://localhost:9090/api/sessions \
-H "Content-Type: application/json" \
-d '{"browser_type": "chromium", "os": "windows"}'Response:
{
"id": "abc-123",
"ws_endpoint": "ws://localhost:9090/ws?session_id=abc-123",
"browser_type": "chromium",
"context_options": {
"user_agent": "Mozilla/5.0 ... Chrome/145.0.0.0 ...",
"viewport": {"width": 1920, "height": 1080}
},
"init_scripts": ["Object.defineProperty(navigator, 'hardwareConcurrency', {get: () => 8});..."],
"created_at": "2026-03-01T06:43:25Z"
}GET /ws?session_id=abc-123 # reconnect to existing session
GET /ws?os=windows&browser_type=chromium # create + connect in one step
Each session has a dedicated download directory. Files downloaded by the browser are accessible via the API while the session is active.
List downloads:
curl http://localhost:9090/api/sessions/{id}/downloads{
"session_id": "abc-123",
"files": [{"name": "report.pdf", "size": 245760, "modified": "2026-03-02T21:30:00Z"}],
"count": 1
}Download a file:
curl -O http://localhost:9090/api/sessions/{id}/downloads/report.pdfReturns the file with Content-Disposition: attachment and auto-detected content type. Path traversal attempts are blocked.
SDK usage:
from browser_grid import BrowserGrid
grid = BrowserGrid("http://localhost:9090")
with grid.configure(browser_type="camoufox") as ctx:
page = ctx.new_page()
page.goto("https://example.com/export")
# ... trigger a download in the browser ...
# List files downloaded by this session
files = grid.list_downloads()
print(files) # [{"name": "report.pdf", "size": 245760, "modified": "..."}]
# Get file contents as bytes
data = grid.get_download("report.pdf")
# Save directly to disk
grid.save_download("report.pdf", "./downloads/")Async variant:
from browser_grid import AsyncBrowserGrid
grid = AsyncBrowserGrid("http://localhost:9090")
async with grid.configure(browser_type="camoufox") as ctx:
# ... trigger download ...
files = await grid.list_downloads()
await grid.save_download(files[0]["name"], "./downloads/")| Method | Path | Description |
|---|---|---|
GET |
/api/sessions |
List all active sessions |
GET |
/api/sessions/{id} |
Get session details |
DELETE |
/api/sessions/{id} |
Destroy a session |
GET |
/api/sessions/{id}/downloads |
List downloaded files |
GET |
/api/sessions/{id}/downloads/{filename} |
Download a file |
GET |
/api/contexts/{key} |
Get context info (exists, has_fingerprint, has_state) |
PUT |
/api/contexts/{key}/state |
Save storage state for a context |
DELETE |
/api/contexts/{key} |
Delete a persistent context |
GET |
/api/health |
Health check (includes fpservice status) |
GET |
/api/stats |
Pool stats (active, max, available) |
The dashboard at http://localhost:9090 includes a real-time live view of any active session. Each browser runs on its own Xvfb display, and the dashboard streams JPEG frames over WebSocket at up to 10 FPS.
Features:
- Screen streaming — watch what the browser is rendering in real time
- Mouse & keyboard input — click, type, and scroll directly in the live view
- Navigation controls — URL bar, back/forward, reload from the dashboard
- Session list — see all active sessions with status, browser type, and duration
Click the eye icon next to any session in the dashboard to open its live view. Disable in production with LIVE_VIEW=false.
Four Dockerfile variants in docker/. All use a single-layer install to minimize image size (no duplicate binaries across layers).
| Dockerfile | Browsers | Use case |
|---|---|---|
Dockerfile.standalone |
All + fpservice embedded | Single docker run, no Compose needed |
Dockerfile.all |
Camoufox + Chromium + Firefox | Full flexibility (default, ~3.4 GB) |
Dockerfile.camoufox |
Camoufox only | Smallest image, deepest spoofing |
Dockerfile.chromium |
Chromium only | Chrome fingerprints only |
Dockerfile.firefox |
Firefox only | Firefox fingerprints only |
Switch image in docker-compose.yml:
grid:
build:
context: .
dockerfile: docker/Dockerfile.camoufox # or .chromium, .firefox, .all| Variable | Default | Description |
|---|---|---|
MAX_WORKERS |
10 |
Maximum concurrent browser instances |
IDLE_TIMEOUT |
300 |
Seconds before idle sessions are killed |
FP_SERVICE_URL |
http://fpservice:8484 |
Fingerprint service URL |
API_KEY |
(empty) | API authentication key (empty = no auth) |
CAMOUFOX_PATH |
/opt/camoufox/camoufox-bin |
Camoufox binary path |
CHROMIUM_PATH |
(auto-detect) | Chromium binary path |
FIREFOX_PATH |
(auto-detect) | Firefox binary path |
CONTEXT_STORE_DIR |
/data/contexts |
Directory for persistent context storage |
CHROMIUM_ARGS |
(defaults) | Comma-separated Chromium flags (see below). none = no extra flags |
HEADLESS |
false |
Run browsers headless |
LIVE_VIEW |
true |
Enable live view screen streaming (disable in production) |
browser-grid \
--host 0.0.0.0 \
--port 9090 \
--max-workers 20 \
--idle-timeout 600 \
--camoufox-path /opt/camoufox/camoufox-bin \
--chromium-path /usr/bin/chromium \
--firefox-path /usr/bin/firefox \
--headlessBy default, Chromium launches with flags that disable unused features inside Docker/Xvfb:
| Flag | What it disables |
|---|---|
--disable-dev-shm-usage |
Uses /tmp instead of /dev/shm (avoids 64 MB Docker limit) |
--disable-background-networking |
Background network requests (update checks, etc.) |
--disable-breakpad |
Crash reporter / dump collection |
--disable-component-update |
Component auto-updater |
--disable-extensions |
Extension subsystem |
--disable-sync |
Chrome sync service |
--disable-translate |
Built-in translation service |
--metrics-recording-only |
Disables metrics reporting (keeps internal recording) |
--no-first-run |
First-run setup tasks and welcome page |
Override via CHROMIUM_ARGS (comma-separated):
environment:
# Custom flags
- CHROMIUM_ARGS=--disable-dev-shm-usage,--disable-extensions,--disable-gpu
# Or disable all extra flags
- CHROMIUM_ARGS=none
# Unset = use defaults aboveMeasured with 3 concurrent instances per browser on a clean container restart (Dockerfile.all):
| Browser | RAM per Instance | Notes |
|---|---|---|
| Camoufox | ~194 MiB | Lightest — C++ engine, no JS overhead |
| Chromium | ~255 MiB | |
| Firefox | ~256 MiB | Playwright's Firefox (not Camoufox) |
Container baseline (no browsers): ~17 MiB.
| Browser | Avg Startup | Notes |
|---|---|---|
| Chromium | ~1.3s | Fastest |
| Firefox | ~1.6s | |
| Camoufox | ~2.1s | Loads C++ fingerprint config |
| Image | Size |
|---|---|
Dockerfile.all (Camoufox + Chromium + Firefox) |
~3.4 GB |
| Server RAM | Recommended MAX_WORKERS |
shm_size |
|---|---|---|
| 4 GB | 10-15 | 1g |
| 8 GB | 20-30 | 2g |
| 16 GB | 40-60 | 4g |
| 32 GB | 80-120 | 8g |
Example high-concurrency config:
services:
grid:
environment:
- MAX_WORKERS=50
- IDLE_TIMEOUT=120
shm_size: "4g"
deploy:
resources:
limits:
memory: 12gTests all three browsers against anti-detection sites at increasing concurrency:
python3 -u tests/test_reliability_audit.pyRuns 4 phases:
- Sequential — 1 session per browser (3 total)
- 3 concurrent — one per browser in parallel
- 10 concurrent — mixed browsers
- 20 concurrent — stress test
Each session navigates to CreepJS, Browserleaks, and BrowserScan, takes full-page screenshots, and reports timing. Results and screenshots are saved to tests/reliability_audit/.
Simulates sustained production traffic with configurable concurrency:
python3 -u tests/long_stress_test.py --duration 45 --target 15Features:
- Continuous session rotation — sessions open/close in waves to maintain target concurrency
- Mixed close modes — 75% clean close, 15% kill via API, 10% abandon (tests orphan cleanup)
- Multi-site browsing — each session visits 2-4 sites from 6 categories (news, tech, shopping, social, anti-detect, general)
- Multiple tabs — 40% of sessions open extra tabs
- Server-side concurrency tracking — checks
/api/statsto prevent session accumulation from abandoned sessions - 10-minute insight snapshots — periodic reports with pool stats, CPU, memory, health status
- Reusable — saves full JSON report to
tests/long_stress/report.json
Options:
| Flag | Default | Description |
|---|---|---|
--duration |
45 |
Test duration in minutes |
--target |
15 |
Target concurrent sessions |
--output |
tests/long_stress |
Output directory |
Validated result: 732 sessions over 45 min, 0 failures, stable memory (~5-7 GB under load).
┌──────────────────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌─────────────┐ ┌──────────────────────────────────┐ │
│ │ fpservice │ │ grid │ │
│ │ (Python) │◀───│ (Go binary) │ │
│ │ │ │ │ │
│ │ browserforge│ │ ┌──────────┐ ┌──────────────┐ │ │
│ │ webgl db │ │ │ Worker │ │ Worker Pool │ │ │
│ │ fonts.json │ │ │ Pool │ │ Health Loop │ │ │
│ │ │ │ ├──────────┤ └──────────────┘ │ │
│ └─────────────┘ │ │ Worker 1 │ ← node launchServer│ │
│ │ │ Worker 2 │ .js → Playwright │ │
│ │ │ Worker N │ Browser Servers │ │
│ │ └──────────┘ │ │
│ │ ▲ WebSocket proxy │ │
│ │ │ │ │
│ │ ┌────┴──────┐ │ │
│ │ │ HTTP/WS │ :9090 │ │
│ │ │ Server │ │ │
│ │ └───────────┘ │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
▲
│ SDK (Python / TypeScript) / REST API / WebSocket
│
┌────┴────┐
│ Client │
└─────────┘
- Client calls
POST /api/sessionswith config - Grid calls fpservice to generate a browser-appropriate fingerprint
- Grid spawns a Playwright browser server via
node launchServer.js - For Camoufox: fingerprint injected at C++ level via env vars
- For Chromium/Firefox: fingerprint returned as context options + JS init scripts
- Client connects via WebSocket, SDK applies context options and scripts
- Client gets a
BrowserContextwith the fingerprint baked in
BrowserGrid/
├── grid/ # Go server
│ ├── cmd/browser-grid/ # Binary entrypoint
│ ├── internal/
│ │ ├── context/ # Persistent context storage (fingerprint + state)
│ │ ├── fingerprint/ # FP generation + Playwright options builder
│ │ ├── screencast/ # Live view (X11 capture, input dispatch)
│ │ ├── server/ # HTTP/WS server + routes + dashboard
│ │ ├── session/ # Session lifecycle
│ │ ├── worker/ # Browser process management + pool
│ │ └── proxy/ # WebSocket bidirectional proxy
│ ├── go.mod
│ └── go.sum
├── fpservice/ # Python fingerprint microservice
│ ├── main.py # Flask app (browserforge + WebGL + fonts)
│ ├── data/
│ │ ├── browserforge.yml # Fingerprint field mapping
│ │ └── fonts.json # OS-specific font lists
│ ├── requirements.txt
│ └── Dockerfile
├── sdk/ # Client SDKs
│ ├── typescript/ # TypeScript/JS SDK (npm: browsergrid-sdk)
│ └── browser_grid/ # Python SDK
│ ├── __init__.py
│ ├── client.py # BrowserGrid / AsyncBrowserGrid
│ └── types.py # SessionConfig / SessionResult
├── docker/
│ ├── Dockerfile.all # All browsers
│ ├── Dockerfile.camoufox # Camoufox only
│ ├── Dockerfile.chromium # Chromium only
│ └── Dockerfile.firefox # Firefox only
├── launchServer.js # Node.js bridge to Playwright's launchServer
├── docker-compose.yml
├── entrypoint.sh # Xvfb + grid startup
└── tests/
├── test_browsers.py # Screenshot tests (CreepJS, Browserleaks, BrowserScan)
├── test_integration.py # Session creation, capacity limits, idle cleanup
├── test_persistent_context.py # Context persistence (fingerprint + storage state)
├── test_reliability_audit.py # 4-phase concurrent browser reliability audit
├── test_sdk_retry.py # SDK retry logic
├── test_downloads.py # Per-session download retrieval
├── test_stale_cleanup.py # Idle session cleanup
├── long_stress_test.py # 30-60 min stress test with mixed close modes
└── stress_test.py # Concurrent session benchmarks
The SDK provides aliases for the original CamoufoxGrid and StealthGrid names:
from browser_grid import CamoufoxGrid # alias for BrowserGrid
from browser_grid import StealthGrid # alias for BrowserGridThe get_ws() method still works for WebSocket-only usage.





