Chrome Fleet Control is a dashboard for managing multiple Chrome or Chromium instances through the Chrome DevTools Protocol (CDP). It supports isolated profiles, optional port forwarding, per-instance logs, tab control, and an optional REST API.
- Create, edit, and delete local or external browser instances
- Start, stop, and inspect instances from the web UI
- Per-instance cards show live uptime (how long it has been running) and open tab count
- Bulk Start All / Stop All and instant instance search/filter
- Keep separate browser profiles per instance
- Port forwarding through
socat - Per-instance launch mode selector with
GUI,Headless via Xvfb, andNative Chrome Headless - Smooth, low-latency live tab control: continuous flicker-free streaming, adjustable frame rate, full mouse (click, drag, scroll, right-click) and keyboard input (typing, special keys, and Ctrl/Cmd shortcuts)
- Persistent CDP connections per tab keep interactive control fast
- Import cookies into a running browser instance through CDP from Netscape or JSON exports
- Server dashboard for CPU, memory, disk, uptime, and network interfaces
- Basic Auth for the UI and legacy
/api/*endpoints - Optional API key protected REST API under
/rest/* run.shauto-creates.envfrom.env.exampleon first launch and enables WebGL-friendly Chrome flags by default
- Node.js and npm
- Google Chrome, Chromium, or Chrome for Testing
socatlsofwgetandunzipif you want to download a portable browser binaryscreenif you wantRUN_IN_SCREEN=trueXvfbif you run Linux in a headless display setup
This example detects the current OS and CPU architecture, resolves the matching official Chrome for Testing download, and extracts it into the project directory.
If the extracted folder matches one of the app's built-in browser search paths, the app can auto-detect it without additional config.
PROJECT_DIR="$(pwd)"
case "$(uname -s):$(uname -m)" in
Linux:x86_64|Linux:amd64)
CFT_PLATFORM="linux64"
CFT_DIR="chrome-linux64"
CHROME_BIN_PATH="$PROJECT_DIR/chrome-linux64/chrome"
;;
Darwin:arm64|Darwin:aarch64)
CFT_PLATFORM="mac-arm64"
CFT_DIR="chrome-mac-arm64"
CHROME_BIN_PATH="$PROJECT_DIR/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
;;
Darwin:x86_64)
CFT_PLATFORM="mac-x64"
CFT_DIR="chrome-mac-x64"
CHROME_BIN_PATH="$PROJECT_DIR/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
;;
MINGW*:x86_64|MSYS_NT*:x86_64|CYGWIN*:x86_64)
CFT_PLATFORM="win64"
CFT_DIR="chrome-win64"
CHROME_BIN_PATH="$PROJECT_DIR/chrome-win64/chrome.exe"
;;
MINGW*:i686|MSYS_NT*:i686|CYGWIN*:i686)
CFT_PLATFORM="win32"
CFT_DIR="chrome-win32"
CHROME_BIN_PATH="$PROJECT_DIR/chrome-win32/chrome.exe"
;;
*)
echo "Unsupported OS/arch: $(uname -s) $(uname -m)" >&2
exit 1
;;
esac
CFT_JSON="https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"
CFT_URL="$(wget -qO- "$CFT_JSON" | node -e 'const fs = require("fs"); const data = JSON.parse(fs.readFileSync(0, "utf8")); const platform = process.argv[1]; const item = data.channels.Stable.downloads.chrome.find((entry) => entry.platform === platform); if (!item) { console.error(`No Chrome for Testing download found for ${platform}`); process.exit(1); } process.stdout.write(item.url);' "$CFT_PLATFORM")"
ARCHIVE_PATH="/tmp/$(basename "$CFT_URL")"
wget -O "$ARCHIVE_PATH" "$CFT_URL"
rm -rf "$PROJECT_DIR/$CFT_DIR"
unzip -q "$ARCHIVE_PATH" -d "$PROJECT_DIR"
echo "Downloaded platform: $CFT_PLATFORM"
echo "Chrome binary: $CHROME_BIN_PATH"
export CHROME_BIN="$CHROME_BIN_PATH"Notes:
- At the time of writing, the official stable Chrome for Testing JSON publishes
linux64,mac-arm64,mac-x64,win32, andwin64. - If your platform is not published in that list, use your own Chrome or Chromium binary and point
CHROME_BINto it. - On Linux and macOS, extracting into the project root matches the app's built-in browser auto-detection paths.
If you specifically want the system-wide Google Chrome .deb package:
wget -O /tmp/google-chrome-stable_current_amd64.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt-get install -y /tmp/google-chrome-stable_current_amd64.debIf you already have a browser binary, just point CHROME_BIN to it:
export CHROME_BIN="/absolute/path/to/chrome"Run the preflight checker before starting the server:
./prep.shIf you want the script to try installing dependencies that can be installed automatically:
./prep.sh AUTO_INSTALL=trueprep.sh will:
- detect OS family and available package manager
- check
node,npm, browser availability,socat,Xvfb,screen, andlsof - run
npm installwhennode_modulesis missing
Configuration is managed via a .env file. To get started, copy the example configuration:
cp .env.example .envEdit the .env file to set your credentials (CHROME_FLEET_USERNAME, CHROME_FLEET_PASSWORD), PORT, REST_API options, and background daemon settings (RUN_IN_SCREEN). You can also configure POP_UP_REAL_BROWSER=true to force a GUI launch instead of headless mode, point CHROME_BIN at a specific browser binary, and tune SCREENSHOT_QUALITY (10–100) for the live control stream. See .env.example for the full, documented list.
If you run ./run.sh without a .env, it copies .env.example to .env automatically and continues with the defaults (review the credentials before exposing the server).
Once configured, simply run the launcher:
./run.shNotes:
- Local instances now use
launch_mode:guixvfbchrome_headless
GUImode needs a real desktop display on Linux.Headless via XvfbrequiresXvfband does not silently fall back to another mode.run.shenablesCHROME_MANAGER_ENABLE_WEBGL=1by default.- If
REST_API=trueandREST_API_KEYis empty, the launcher aborts.
Run everything — the Node dashboard and a real browser — in one container.
The image bundles Google's official headless-shell Chromium (multi-arch
amd64 + arm64) plus every helper the app shells out to (socat, Xvfb,
lsof), so the only host requirement is Docker.
./docker.sh # build the image and start the dashboard (default action)
./docker.sh logs # follow logs
./docker.sh down # stop and remove
./docker.sh help # all commandsdocker.sh auto-detects your Docker flavour and works with the docker compose
plugin, the legacy docker-compose binary, or plain docker (no Compose
needed). It bootstraps .env from .env.example, creates ./data for
persistent profiles + database, and exposes the dashboard on PORT (default
3000). Configuration is the same .env used by run.sh.
To run on a machine with no internet (or no build toolchain), export the image
to the deploy/ folder and copy that folder to the target host:
./docker.sh export # -> deploy/chrome-fleet-control-<arch>.tar.gz
# copy the whole deploy/ folder to the offline machine, then there:
cd deploy && ./load.sh # imports the image and starts the dashboarddeploy/ is a self-contained bundle (image tarball + load.sh +
docker-compose.yml + .env.example); see deploy/README.md. The tarball is
architecture-specific — build it on the same CPU architecture as the target
(arm64 vs amd64).
Full functionality — instance spawning, tab navigation, CDP, cookie import, and
live screenshots / live-control — is verified working in the container on
both amd64 (native Linux) and arm64 (Apple Silicon via colima).
The CDP client used for live control was switched from
chrome-remote-interfaceto a small raw-WebSocket client (lib/cdp-raw.js): the former crashes modern Chromium (150+) the moment a screenshot/input session is opened, while a raw WebSocket carrying the identical CDP commands works reliably.
- The UI and legacy
/api/*endpoints use Basic Auth withUSERNAMEandPASSWORD. - The REST API under
/rest/*is only enabled whenREST_API=true. - The REST API accepts
X-API-Key: <key>orAuthorization: Bearer <key>.
All endpoints below are mounted under /rest when the REST API is enabled.
GET /instancesGET /instances/:idPOST /instancesPUT /instances/:idPATCH /instances/:idDELETE /instances/:idPOST /instances/:id/startPOST /instances/:id/spawnPOST /instances/:id/stopGET /instances/:id/logs
GET /instances/:id includes:
hostandportlaunch_mode,launch_backend,headless_enabled, andxvfb_enableddebug_endpointsforward_targetsforward_to
GET /healthzGET /healtzGET /server/statsGET /server/logsGET /server/healthzGET /server/healtz
/healthz returns CPU usage, memory usage, disk usage, uptime, network interfaces, and an instance status summary.
The REST API also exposes the same operational features that exist in the legacy /api surface:
GET /configPOST /configDELETE /config/:keyGET /instances/:id/tabsPOST /instances/:id/tabs/newPOST /instances/:id/tabs/:tabId/activatePOST /instances/:id/tabs/:tabId/navigateDELETE /instances/:id/tabs/:tabIdGET /instances/:id/tabs/:tabId/screenshotPOST /instances/:id/tabs/:tabId/inputPOST /instances/:id/cookies/import
POST /instances/:id/cookies/import expects JSON like:
{
"files": [
{
"name": "x.com_cookies.txt",
"content": "# Netscape HTTP Cookie File\n..."
}
]
}Supported import formats:
- Netscape cookie files such as browser-exported
.txt - JSON arrays of cookies
- JSON objects containing a
cookiesarray
List instances:
curl -H "X-API-Key: super-secret-key" http://localhost:3000/rest/instancesGet instance details:
curl -H "X-API-Key: super-secret-key" http://localhost:3000/rest/instances/1Spawn an instance:
curl -X POST -H "X-API-Key: super-secret-key" http://localhost:3000/rest/instances/1/spawnUpdate an instance:
curl -X PATCH \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{"name":"Chrome-1001","notes":"updated from REST"}' \
http://localhost:3000/rest/instances/1Delete an instance:
curl -X DELETE -H "X-API-Key: super-secret-key" http://localhost:3000/rest/instances/1Health check:
curl -H "X-API-Key: super-secret-key" http://localhost:3000/rest/healthzImport cookies:
curl -X POST \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{"files":[{"name":"x.com_cookies.txt","content":"# Netscape HTTP Cookie File\n.x.com\tTRUE\t/\tTRUE\t1808403617\tauth_token\tvalue"}]}' \
http://localhost:3000/rest/instances/1/cookies/import