self-hosting/docker-compose.
Recommended baseline
Start with Docker Compose for everything, then move a single service to host runtime when you need faster iteration.3030) when testing WorkOS and GitHub callbacks.
Core services
- Backend (
backend/, port 3000 by default) β orchestrator + REST APIs for repos/orgs/jobs. - Statesman (
taco/cmd/statesman, port 8080) β state storage API and Terraform Cloud-compatible endpoints. - UI (
ui/, port 3030) β TanStack Start frontend that talks to both services and WorkOS. When tunneling (e.g., ngrok), expose the UI host; WorkOS and GitHub callbacks should point to the UI domain.
Prerequisites
- Go toolchain for backend + statesman, Node 18+ for UI (
pnpmornpm). - A WorkOS project with User Management enabled and at least one organization + member (needed for UI auth and org IDs).
- Optional: GitHub App for repo onboarding (the backend can help you create one via
/github/setup).
Shared secrets and ports
- Pick two secrets and reuse them across components:
ORCHESTRATOR_BACKEND_SECRETβ‘DIGGER_INTERNAL_SECRET(backend) β‘ UI env.STATESMAN_BACKEND_WEBHOOK_SECRETβ‘OPENTACO_ENABLE_INTERNAL_ENDPOINTS(statesman) β‘ UI env.
- Default ports: backend
3000, statesman8080, UI3030.
Hybrid mode: run one service outside Docker
Run that service from source
Update service URLs so callers can still reach it
| Service running on host | If UI is in Docker | If UI is on host |
|---|---|---|
Backend (:3000) | In ui container config, set ORCHESTRATOR_BACKEND_URL=http://host.docker.internal:3000 | In ui/.env.local, set ORCHESTRATOR_BACKEND_URL=http://localhost:3000 |
Statesman (:8080) | In ui container config, set STATESMAN_BACKEND_URL=http://host.docker.internal:8080 | In ui/.env.local, set STATESMAN_BACKEND_URL=http://localhost:8080 |
Token service (:8081) | In ui container config, set TOKENS_SERVICE_BACKEND_URL=http://host.docker.internal:8081 | In ui/.env.local, set TOKENS_SERVICE_BACKEND_URL=http://localhost:8081 |
Drift (:3001, optional) | In ui container config, set DRIFT_REPORTING_BACKEND_URL=http://host.docker.internal:3001 | In ui/.env.local, set DRIFT_REPORTING_BACKEND_URL=http://localhost:3001 |
UI (:3030) | Not applicable | In ui/.env.local, keep backend/statesman URLs on localhost |
On Linux,
host.docker.internal may require host-gateway mapping. If unavailable, use your host IP reachable from containers.Treat
self-hosting/docker-compose/docker-compose.yml ports: mappings as the source of truth for host ports.What is superseded
- Manually creating per-service local databases as the default first step.
- Treating ngrok as backend-only; UI tunnel is now the primary callback endpoint.
- Repeating full setup steps independently on each service page.
What remains important
- Shared secret alignment across backend, statesman, and UI.
- WorkOS organization/user sync into backend and statesman for authenticated UI flows.
- GitHub App callback/webhook URLs pointing to the public UI domain.
Troubleshooting cheatsheet
- Backend /api/ returns 404*:
DIGGER_ENABLE_API_ENDPOINTSnottrueor org not upserted. - Statesman 403: webhook secret mismatch. Statesman 404/500 resolving org: org not synced (missing
external_org_id). - UI WorkOS auth succeeds but org is empty: add membership in WorkOS and resync org/user to services.
- GitHub connect opens 404: set
ORCHESTRATOR_GITHUB_APP_URLto a valid install/setup URL.

