Deployment#
Ticket Board is designed to run as a single Docker Compose stack on one host — a homelab box, a small VM, or a developer laptop. The runtime is four long-running containers and one one-shot migrator behind a single Caddy origin, with Postgres as the only stateful dependency.
This section covers everything you need to operate that stack: the Compose topology, how to put a real domain in front of it, every environment variable, how to back the data up, and how to upgrade between versions without losing rows.
What runs#
flowchart LR
Browser([Browser])
Bot([Bot / MCP client])
subgraph Host[Docker host]
direction LR
Caddy[proxy<br/>caddy:2-alpine<br/>:80 / :443]
FE[frontend<br/>Next.js standalone<br/>:3000 internal]
BE[backend<br/>FastAPI + uvicorn<br/>:8000 internal]
MIG[migrate<br/>one-shot<br/>alembic upgrade head]
PG[(postgres<br/>postgres:16-alpine<br/>pgdata volume)]
end
Browser -- HTTPS --> Caddy
Bot -- HTTPS --> Caddy
Caddy -- "/" --> FE
Caddy -- "/api/v1/*" --> BE
Caddy -- "/mcp/bot/*" --> BE
Caddy -- "/healthz, /readyz" --> BE
FE -- "server-side fetch" --> BE
BE --> PG
MIG -. "runs once, then exits" .-> PG
Four long-running services and one one-shot:
| Service | Image | Role |
|---|---|---|
postgres |
postgres:16-alpine |
Sole data store. Holds domain rows, the audit log, FTS indexes, sessions. |
migrate |
ticket-board-backend:latest (entrypoint overridden) |
Runs alembic upgrade head against Postgres and exits. The backend depends_on it. |
backend |
ticket-board-backend:latest |
FastAPI + FastMCP under uvicorn. Serves /api/v1/* and /mcp/bot/*. |
frontend |
ticket-board-frontend:latest |
Next.js standalone build. Serves the UI; calls the backend over the internal Docker network. |
proxy |
caddy:2-alpine |
The only service that publishes ports to the host. Terminates HTTP/HTTPS, routes to backend or frontend, redacts MCP tokens in access logs. |
Only the proxy exposes host ports. The backend on :8000 and the frontend on :3000 are reachable only on Docker's internal network. This is deliberate — see Docker Compose reference for why.
Why one origin#
The backend issues a session cookie with SameSite=Lax. For that cookie to attach to subsequent API calls from the browser, the UI and the API must share an origin (scheme + host + port). Caddy provides that origin: the browser sees one host, and Caddy routes by path.
Splitting the backend onto a separate hostname breaks the cookie origin and silently breaks login.
What you'll do on day one#
A first deploy is short:
- Clone the repo and copy
.env.exampleto.env. Generate real secrets forSECRET_KEYandPOSTGRES_PASSWORD. - Decide whether you're running plain HTTP for local dev or real TLS for a homelab. Set
ENVandCOOKIE_SECUREaccordingly. See Environment variables. docker compose up -d. Watchdocker compose psuntilbackendreports healthy.- Open the UI, log in, and rotate the seeded password under Settings → Security (
/settings/security). - Configure backups before you start storing anything you'd be sad to lose. See Backup and restore.
If you intend to reach the stack from outside your LAN — even just from your phone over Tailscale — read Homelab setup first. It covers DNS, Caddy ACME, systemd auto-start, and resource sizing.
What you'll do on day N#
| When | Page |
|---|---|
| Configuring a stable home for the stack | Homelab setup |
| Tuning bcrypt, sweeper cadence, retention | Environment variables |
| Taking, verifying, and restoring backups | Backup and restore |
| Upgrading to a newer release | Upgrading |
| Validating that a deploy is healthy | Release checklist |
Out of scope#
Ticket Board does not target Kubernetes, ECS, Nomad, or any other orchestrator. The Compose file is the deployment artifact. If you need horizontal scale-out, a managed Postgres, or zero-downtime rolling deploys, this is not the system for that — its design budget assumes one operator, one host, and a homelab maintenance window.