Skip to content

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:

  1. Clone the repo and copy .env.example to .env. Generate real secrets for SECRET_KEY and POSTGRES_PASSWORD.
  2. Decide whether you're running plain HTTP for local dev or real TLS for a homelab. Set ENV and COOKIE_SECURE accordingly. See Environment variables.
  3. docker compose up -d. Watch docker compose ps until backend reports healthy.
  4. Open the UI, log in, and rotate the seeded password under Settings → Security (/settings/security).
  5. 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.