This is the recommended deployment shape for a single-tenant, trusted-operator production install.
- Auto Browser controller + browser node on one private host
- Front the controller and takeover UI with Cloudflare Access or Tailscale
- Keep published ports bound to
127.0.0.1 - Use
docker_ephemeralsession isolation if the operator may touch multiple accounts/workflows
At minimum:
APP_ENV=production
API_BEARER_TOKEN=<strong-random-secret>
REQUIRE_OPERATOR_ID=true
AUTH_STATE_ENCRYPTION_KEY=<44-char-fernet-key>
REQUIRE_AUTH_STATE_ENCRYPTION=true
REQUEST_RATE_LIMIT_ENABLED=true
METRICS_ENABLED=true
CONTROLLER_ALLOWED_HOSTS=<controller-ingress-hosts>Strongly recommended:
SESSION_ISOLATION_MODE=docker_ephemeral
MAX_SESSIONS=1
ALLOWED_HOSTS=<your-real-allowlist>
STATE_DB_PATH=/data/db/operator.db
ARTIFACT_RETENTION_HOURS=168
UPLOAD_RETENTION_HOURS=168
AUTH_RETENTION_HOURS=168CONTROLLER_ALLOWED_HOSTS protects the controller itself from unexpected HTTP Host headers.
Keep ALLOWED_HOSTS for browser navigation targets, and set CONTROLLER_ALLOWED_HOSTS
to the hostnames operators use to reach the controller, such as browser.example.com
or localhost,127.0.0.1,::1 for a local-only install.
Auto Browser now supports reusable named auth profiles under:
/data/auth/profiles/<profile-name>/
Use them when you want a human to log in once and let future sessions resume from saved browser state without passing raw storage-state file paths around.
Recommended pattern:
- use one profile per account/workflow
- keep auth-state encryption enabled in production
- treat profile names as operator-facing labels, not secrets
You now have two viable ways to authenticate model providers:
Use:
OPENAI_API_KEYANTHROPIC_API_KEYGEMINI_API_KEY
This is still the cleanest option for CI and broadly automated installs.
Use this on a trusted private box if you already run:
codexvia ChatGPT/Codex loginclaudevia Claude Code login/subscriptiongeminivia Gemini CLI login
Set:
OPENAI_AUTH_MODE=cli
CLAUDE_AUTH_MODE=cli
GEMINI_AUTH_MODE=cli
CLI_HOME=/data/cli-homeThen copy the signed-in CLI state into the mounted data directory:
mkdir -p data/cli-home
rsync -a ~/.codex data/cli-home/.codex
cp ~/.claude.json data/cli-home/.claude.json
rsync -a ~/.claude data/cli-home/.claude
rsync -a ~/.gemini data/cli-home/.geminiTreat data/cli-home like a password vault. Never commit it.
If the easiest path is to sign in on the target box directly, use:
./scripts/bootstrap_cli_auth.sh codex
./scripts/bootstrap_cli_auth.sh claude
./scripts/bootstrap_cli_auth.sh geminiThat helper is for the default writable /data/... auth cache flow. It opens the provider CLI inside the controller image with HOME=$CLI_HOME (normally /data/cli-home), so the resulting login state lands in the mounted ./data directory where *_AUTH_MODE=cli expects it.
If the target machine already has those subscription logins locally, prefer the host-mount override instead of copying caches:
CLI_HOST_HOME=/home/botuser \
OPENAI_AUTH_MODE=cli \
CLAUDE_AUTH_MODE=cli \
GEMINI_AUTH_MODE=cli \
docker compose -f docker-compose.yml -f docker-compose.host-subscriptions.yml up -d --buildThat override mounts ~/.codex, ~/.claude, ~/.claude.json, and ~/.gemini read-only at the same home-path inside the container and sets CLI_HOME to that host-style home. If your login home is different, change CLI_HOST_HOME. Do not use bootstrap_cli_auth.sh in that mode; sign in on the host first and then start the override.
If Codex subscription auth still fails inside Docker, switch only OpenAI to the host bridge:
mkdir -p data/host-bridge
python3 scripts/codex_host_bridge.py --socket-path data/host-bridge/codex.sockTo keep that bridge running like a host-local skill, install the included user-service template:
mkdir -p ~/.config/systemd/user
cp ops/systemd/codex-host-bridge.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now codex-host-bridge.serviceThen:
OPENAI_AUTH_MODE=host_bridge
OPENAI_HOST_BRIDGE_SOCKET=/data/host-bridge/codex.sockThat keeps codex on the host, reuses the host login state directly, and lets the container call it over a shared Unix socket.
The controller now health-checks that socket and the bridge kills stuck host codex jobs after 55 seconds by default.
Treat the socket as a host-trust boundary: any local process that can connect to it can trigger host-side codex exec.
In APP_ENV=production, startup now fails fast if:
- any
*_AUTH_MODEvalue is not one of its supported modes (api,cli, and for OpenAI alsohost_bridge) - a provider is set to
climode but its CLI binary is missing - OpenAI is set to
host_bridgemode but the bridge socket is missing, stale, not a real Unix socket, or failing/healthz CLI_HOMEis set but the expected auth-state files are missing for that CLI
python - <<'PY'
from cryptography.fernet import Fernet
print(Fernet.generate_key().decode())
PYcp .env.example .env
# edit .env with the required production values above
docker compose -f docker-compose.yml -f docker-compose.isolation.yml up -d --buildcurl -fsS http://127.0.0.1:8000/healthz | jq
curl -fsS http://127.0.0.1:8000/readyz | jq
curl -fsS http://127.0.0.1:8000/metrics | head
curl -fsS http://127.0.0.1:8000/maintenance/status | jqIf METRICS_ENABLED=false, skip the /metrics check; the endpoint returns 404.
- recreate the session first; do not assume browser state is still valid
- check controller logs and browser-node logs before retrying the same action loop
- inspect the latest artifacts/screenshots to confirm where the flow broke
- the site may have expired cookies, revoked the session, or triggered MFA again
- recreate the profile with a fresh manual login
- avoid reusing one profile across multiple unrelated accounts or workflows
- keep controller and noVNC ports bound to
127.0.0.1 - verify your access layer first: Cloudflare Access, Tailscale, or reverse tunnel
- if remote takeover breaks, treat it as a gateway/tunnel problem before debugging Playwright
- this is intentional in
APP_ENV=production - check required env vars first: bearer token, operator ID enforcement, encryption key, rate limiting
- if using
*_AUTH_MODE=cliorhost_bridge, verify the expected auth files or socket actually exist
- update
ALLOWED_HOSTSto include the real target domains involved in login, redirects, uploads, and downloads - remember that identity providers, CDNs, and file hosts may differ from the main app domain
- Tesseract OCR is helpful, not authoritative
- use screenshots and DOM observations as the primary source of truth
- expect worse OCR quality on small text, low-contrast UIs, and dense dashboards
Use one of:
- Cloudflare Access in front of the controller + noVNC paths
- Tailscale and keep the whole stack private
- the included reverse-SSH path for bastion-style access when direct reachability is not available
Do not expose raw controller or noVNC ports directly to the public internet.
Back up at least:
/data/db/if using SQLite/data/sessions//data/jobs//data/auth/if you intentionally keep reusable auth state/data/audit/
The controller can now prune stale artifacts/uploads/auth-state automatically.
Manual run:
curl -s http://127.0.0.1:8000/maintenance/cleanup \
-X POST \
-H "Authorization: Bearer $API_BEARER_TOKEN" \
-H 'X-Operator-Id: ops' | jqBefore live debugging, gather:
- OpenAI / Anthropic / Gemini API keys, or populated CLI auth caches under
data/cli-home - gateway credentials (Cloudflare Access or Tailscale)
- bastion SSH details if using reverse tunnels
- operator identity convention (
X-Operator-Idvalues) - allowlisted target hosts/domains
- Bring the stack up privately
- Verify
/readyz - Verify
/metrics(unlessMETRICS_ENABLED=false) - Create one session against a non-sensitive site
- Verify observe/click/type flow
- Add real creds
- Test one real target workflow with human takeover ready