Hetzner Two-Server Deployment¶
Production layout for Linux:
CCX23: public dashboard + PostgreSQL + report engineAX42-U: private runner host +claude/codex+ Docker labs- Cloudflare in front of the dashboard origin
Why this layout¶
- The dashboard is a web application with a database and report generation.
- The runner launches AI CLIs on the host and should not be exposed publicly.
- Labs, Playwright, and
pentest-toolsbelong on the dedicated runner server.
Repository changes required for this deployment¶
This repository now includes the minimum production changes for the two-server setup:
- PostgreSQL driver installed in backend requirements
- Production compose uses
postgresql+psycopg://... - Production compose accepts
RUNNER_URL - Production compose mounts
engagements/read-write - Demo seed users can be disabled in production with
SEED_DEMO_USERS=false
Production server (CCX23)¶
Required files¶
dashboard/docker-compose.production.ymldashboard/.env.production.exampledashboard/deploy/Caddyfile.example
Environment¶
Copy dashboard/.env.production.example to dashboard/.env and set:
DASHBOARD_URL=https://app.example.comCLIENT_PORTAL_URL=https://app.example.comRUNNER_URL=http://<private-or-firewalled-runner-ip>:8881CORS_ORIGINS=https://app.example.comSEED_DEMO_USERS=falseSEEDED_USERS_MUST_CHANGE_PASSWORD=true
Notes¶
- Keep PostgreSQL internal only.
- Expose only
80/443publicly. - Put Cloudflare in
Full (strict)mode.
Runner server (AX42-U)¶
Required components¶
claudeCLI on hostcodexCLI on host if used operationally- Docker
- built
pentest-toolsimage - Python venv for
dashboard.runner.runner - systemd unit based on
dashboard/deploy/bd-runner.service.example
Network exposure¶
- expose
8881/tcponly to the production server IP or private network - do not publish the runner behind Cloudflare
- do not expose
8881to the public internet
Verification checklist¶
curl http://127.0.0.1:8880/api/v1/healthsucceeds onCCX23curl http://127.0.0.1:8881/healthsucceeds onAX42-Ucurl http://<runner-ip>:8881/healthsucceeds fromCCX23- dashboard login works with the configured admin credentials
- no demo non-admin accounts exist in production when
SEED_DEMO_USERS=false - generated reports are written successfully
- creating/updating engagements and findings works without filesystem permission errors