Installation
Install the cluster manager with Docker — one container, one command, one key to obtain.
Docker is the only supported way to install the control plane. It ships as a single
hardened container with named volumes for state, the release webroot and the
load-balancer config. You boot one container, and the only value you have to
supply is your organization key (VEIL_KEY) — the installer derives or generates
everything else, including the auth secret. A first-run wizard walks you through
creating the admin, configuring the cluster and TLS, and adding your first
worker — all from the UI.
Get your org API key
Sign in at veilbrowser.net and open Connect. Copy your organization API key — it authorizes both the private image pull and the browser runtime. The Connect page also shows the lowercased form used in the registry path.
One key, two roles
Your org key is the only secret you have to obtain. Its lowercased form is the
registry path segment (registry.veilbrowser.net/<org-key-lower>/…) and the same key
is passed to the runtime as VEIL_KEY. Treat it like a password and rotate it if it
leaks. The compose file also wants an AGENT_TOKEN, but that is generated locally
rather than issued to you.
Install with one command
On a fresh host, run this once. It installs Docker if it's missing, pulls the image
through the registry gate (no docker login, no source code), writes a minimal .env,
and starts the manager:
curl -fsSL https://veilbrowser.net/install.sh | VEIL_KEY=<your-org-key> bashVEIL_KEY is the only input. The installer derives VEIL_ORG_KEY_LOWER (the lowercased
key used as the registry path segment) and DOCKER_GID (so the wizard can provision
same-host workers), generates an AGENT_TOKEN for later self-update, writes all four to
.env in /opt/veil-cluster, and runs docker compose up -d. Pipe to bash without
VEIL_KEY= to be prompted for the key.
Manual — what the script does. If you'd rather inspect and run the steps yourself
(still no source, no docker login):
# 1. Docker + the Compose v2 plugin (skip if already installed)
curl -fsSL https://get.docker.com | sh
# 2. Fetch the compose file
mkdir -p /opt/veil-cluster && cd /opt/veil-cluster
curl -fsSL https://veilbrowser.net/deploy/compose.yaml -o compose.yaml
# 3. Write .env — VEIL_KEY is the only value you supply; the rest is derived
# or generated here, exactly as install.sh would
printf 'VEIL_KEY=%s\nVEIL_ORG_KEY_LOWER=%s\nDOCKER_GID=%s\nAGENT_TOKEN=%s\n' \
"<your-org-key>" "<your-org-key-lowercased>" \
"$(getent group docker | cut -d: -f3)" "$(openssl rand -hex 32)" > .env
# 4. Bring it up
docker compose up -dAGENT_TOKEN is not optional, even with self-update off
The compose file guards the updater sidecar with ${AGENT_TOKEN:?}, and Compose
interpolates every service before it filters by profile — so an absent token fails
up even though the updater profile is off. The guard is strict on purpose: an
empty token makes the agent crash-loop. install.sh generates one for exactly this
reason, which is why the command above does too.
The image is pulled through the registry gate at
registry.veilbrowser.net/<org-key-lower>/cluster-manager — the gate authenticates the
private upstream for you, so no docker login is needed.
On first boot the container auto-generates and persists its auth secret, applies
database migrations in-process, and renders a placeholder config so the UI answers
immediately. State lives in named volumes (cm-state, cm-webroot, cm-openresty), so
upgrades and restarts keep your data.
Why mount the Docker socket?
The compose file mounts /var/run/docker.sock so the wizard can provision a worker
on the same host with one click. Remove that mount (and group_add) and you can
still add workers manually with a copy-paste docker run snippet.
Run the first-run wizard
Open the UI (https://<your-host>/). Because the database is empty, you land in the
setup wizard, which walks through five steps:
- Create admin — paste the one-time setup token from the container logs.
- Cluster basics — name the cluster and set its hostnames.
- TLS — choose Cloudflare, your own certificate, or automatic HTTPS.
- Add worker — provision one on this host, or copy a
docker runsnippet. - Review — Render & Reload the load-balancer config and health-check the cluster.
Every field is explained in the onboarding walkthrough, and the TLS options in Edge security & TLS.
Connect your automation
The wizard finishes with your connect URL. Point Puppeteer or Playwright at the cluster's launch endpoint:
wss://<public-hostname>/launchDNS records: one or two
The simplest deployment needs a single DNS record — the main hostname serves the
console and the client wss://…/launch endpoint together, and same-box workers reach
the release origin over the internal Docker network. Add a second record
(release.<domain>) only when you run workers on other servers, so those remote
workers can pull browser + profile bundles. Workers themselves are never DNS-resolved
by clients — the manager proxies to them by IP, so adding capacity on more servers
never means adding hostnames.
Updating later
You can update from the console instead of the command line by enabling the optional updater-agent profile — a container can't recreate itself, so a small sidecar does it. See Upgrades.
Deployment templates
For shapes the wizard does not cover, two templates are published next to the
compose file above and fetched the same way — still no source, no docker login:
| Template | Use |
|---|---|
compose/single-host.yaml | Cluster manager, one worker and an updater agent on one box |
compose/env.example | The .env that goes beside single-host.yaml |
k8s/worker-cluster.yaml | Horizontally scaled workers behind a LoadBalancer service |
curl -fsSL https://veilbrowser.net/deploy/compose/single-host.yaml -o single-host.yaml
curl -fsSL https://veilbrowser.net/deploy/compose/env.example -o .envEach opens with a header comment giving the commands to run it, so it is self-contained once downloaded. The template index covers the requirements they share.
Fill in VEIL_ORG_KEY_LOWER — your org key, lowercased — before bringing
either up. It is the path segment the images are pulled through, so compose
stops with an error naming the variable if you leave it blank. There is still
no registry username or password: the gate authorizes on the path alone.
Managed platforms are not supported
Fly.io, Render and DigitalOcean App Platform cannot run a worker today. A
worker needs the Chromium sandbox — privileged or SYS_ADMIN plus at least
1 GB of /dev/shm — and none of the three exposes those controls. Run
workers on hosts or a Kubernetes cluster you control.
Workers have host requirements the manager does not — the Chromium sandbox, a
sized /dev/shm, a writable install volume. Run a worker
states them once and is the page to follow when you add capacity by hand instead
of through the wizard.
Versioning & channels
Each image is versioned independently from its own source. Pick a channel by tag:
:X.Y
Auto-patch within a minor. Recommended for production.
:X.Y.Z
Pinned/reproducible. Use for strict change control.
:canary
Built from dev — pre-merge testing.
:beta
Prerelease line for opt-in testers.
The /v1 cloud API is a frozen contract to self-hosted workers: additive-only
within v1; breaking changes ship as /v2 with an overlap window (workers
supported within 2 minors / 90 days).
Workers advertise their own contract set on /health and read the cloud's at
boot. The check is advisory, not a gate: a mismatched major prints
INCOMPATIBLE saas-api contract and the worker keeps running, and an
unreachable cloud is logged as unverified. Treat that line as a startup alarm
worth wiring to your log monitoring, because nothing downstream will stop a
worker from serving against a cloud it cannot talk to correctly.
Next steps
- Onboarding walkthrough — every wizard field explained.
- Upgrades — drain-then-roll worker updates.
- Configuration reference — environment variables, ports, volumes and roles.
- Operating the cluster — day-to-day fleet work.
- Backup & restore — protect control-plane state.