# VeilBrowser deployment templates

Ready-to-adapt templates for running VeilBrowser in cluster mode
(`wss://<host>/launch`). Two are supported; pick the one that matches your
scale. Both are served from veilbrowser.net, so you can fetch one without the
repo:

| Target | Fetch | Best for |
| ------ | ----- | -------- |
| Single host (all-in-one) | [`/deploy/compose/single-host.yaml`](https://veilbrowser.net/deploy/compose/single-host.yaml) | Evaluation, small fleets — cluster-manager + a worker + updater agent on one box |
| Kubernetes | [`/deploy/k8s/worker-cluster.yaml`](https://veilbrowser.net/deploy/k8s/worker-cluster.yaml) | Horizontally-scaled worker fleet (`kubectl scale`) |

`fly/`, `render/` and `digitalocean/` are **unsupported** and deliberately not
served. Managed platforms do not expose the privileged / `SYS_ADMIN` and
`/dev/shm` controls the Chromium sandbox needs, and DigitalOcean App Platform's
`registry_type` accepts only `DOCR`, `DOCKER_HUB` or `GHCR`, so it cannot name
our registry host at all. Each file says so in its own header; read it before
reviving one.

`compose/cloud.yaml` is not served either: it is the first-party
veilbrowser.net stack, not a self-host template.
`apps/web/scripts/copy-deploy-artifacts.mjs` publishes an explicit allowlist, so
serving a file is a deliberate act rather than a side effect of adding it here.

## Common requirements

- **Chromium sandbox:** containers need `--privileged` (compose) or `SYS_ADMIN`
  (k8s), plus a roomy `/dev/shm` and a `tmpfs` `/tmp`. On Ubuntu 24.04+ hosts:
  `sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`.
- **Licensing:** each worker needs `VEIL_KEY` (org API key) and `SAAS_API_URL`
  so it can `authorize` launches (fail-closed).
- **Browser binaries** are pulled on first use from `RELEASE_ORIGIN_URL`
  (a cluster-manager release origin). Set `VEIL_BOOTSTRAP_RELEASES=1` to warm
  the cache at boot.
- **Private images:** pull via the registry gate using your lowercased org key
  as the path segment:
  `docker pull registry.veilbrowser.net/<org-key-lower>/local-api:latest`.
  The gate authorizes on that path and issues no auth challenge, so there is no
  `docker login` and no registry username or password to supply anywhere. A
  platform that insists on a credential pair cannot pull these images.

## Scaling & sticky routing

A bare Service/LoadBalancer is fine for *launching* (stateless). For
reconnect / VNC / devtools to reach the exact worker holding a browser, front
the fleet with the **cluster-manager** LB, which path-encodes the worker id
(`/w/<workerId>/browsers/<id>/...`) — enable `clusterProxyEnabled` on the
Cluster row. See the self-hosting docs.

## Updating

- **cluster-manager:** self-update from the UI (registry check → click Update),
  or `docker compose pull cluster-manager && up -d`.
- **workers:** drain-then-roll from the cluster-manager UI. Deliberately never
  unattended: replacing a worker kills the browsers on it, so a roll waits for
  live sessions to finish first. Browser binaries update independently via the
  release origin, no container restart.
