Skip to content

Deployment

The repo ships a docker compose–based deploy kit. The same files cover a local smoke run and a real VPS deployment; the difference is which domain you point MEET_DOMAIN at and whether Caddy auto-issues a Let's Encrypt certificate.

Dev (localhost, native processes)

Fully documented in Quickstart. make run spins up all three processes via honcho; make run-alt swaps in the alt-backend. Ports: SFU :8443, backend :8000, front :5173. Fast feedback loop; no Docker required.

Prod stack (compose)

One command brings the full stack up locally or on a fresh VPS:

cp deploy/.env.production.example .env.production
mkdir -p deploy/galene/groups deploy/galene/data
# generate a signing key and write the two files —
# see deploy/galene/README.md
# Edit every CHANGE-ME in both files.

make prod-up       # Litestar backend
# ... or
make prod-up-alt   # the aiohttp alt-backend (same SFU + frontend)

Under the hood this runs docker compose --env-file .env.production -f compose.prod.yml up --build -d. The Makefile targets add pre-flight checks (a missing .env.production gets a clear error with the command to run).

Tear down with make prod-down. Tail logs with make prod-logs.

Services

Service Image built from Role
postgres postgres:16-alpine (upstream) Backend's database
sfu ops/galene/Dockerfile — pinned upstream Galene, ~21 MB Media forwarding and /ws signalling
backend backend/Dockerfile — python + uv, ~280 MB Litestar /api/v1/ + Alembic migrations on entrypoint
alt-backend alt-backend/Dockerfile — python + aiohttp, ~170 MB Alternate minimal backend, activated via --profile alt
front front/Dockerfile — Node build → Caddy runtime, ~60 MB Svelte SPA + reverse proxy + TLS termination

The alt profile swaps the backend. The Caddyfile's @api matcher routes to the service name backend; alt-backend joins the compose network with that alias, so the same Caddyfile works unchanged.

Each Dockerfile lives next to what it builds (ops/galene/Dockerfile compiles pinned upstream Galene from source; backend/Dockerfile, etc.) with a per-component build context. Caddyfile ships inside front/ since Caddy and the SPA deploy as one unit.

Layout

graph TD
    NET(["Internet<br/>:80 / :443"])

    subgraph compose["docker compose network"]
        F["front<br/>Caddy: TLS, reverse proxy, static SPA"]
        K["backend<br/>(or alt-backend, via --profile alt)"]
        G["sfu<br/>pinned upstream Galene"]
        P[("postgres")]
    end

    NET --> F
    F -- "/api/*" --> K
    F -- "/ws + WebRTC media" --> G
    K --> P
    K -. "operator /ws, one per active room" .-> G

    classDef ours fill:#e8eaf6,stroke:#3f51b5,color:#1a237e
    classDef dep fill:#f1f8e9,stroke:#689f38,color:#33691e
    class F,K ours
    class G,P dep

The operator connection is dialled from the backend to the SFU inside the compose network, so Caddy isn't in that path. Nothing is dialled the other way: upstream Galene makes no outbound connections.

Local smoke (no domain, no TLS)

Override the ports and set MEET_DOMAIN to a bind address:

cat >> .env.production <<EOF

# Local-only overrides
MEET_DOMAIN=:80
HOST_HTTP_PORT=8080
HOST_HTTPS_PORT=8443
EOF

make prod-up
curl -s http://localhost:8080/api/v1/config
# → {"app_name":"galene","environment":"prod",...}

Caddy listens on :80 inside the container, the host maps 8080→80, and no TLS cert is issued. Useful for pre-production verification.

Production on a real VPS

Edit .env.production:

MEET_DOMAIN=meet.example.com       # your public FQDN
HOST_HTTP_PORT=80                  # leave as the default
HOST_HTTPS_PORT=443

Caddy's automatic HTTPS issues a Let's Encrypt cert on first boot. Requirements: DNS for MEET_DOMAIN points at the VPS; ports 80 + 443 are reachable from the public internet during the ACME challenge.

Behind a corporate firewall where 80/443 aren't reachable, switch to the DNS-01 challenge — see the Caddy docs.

Built-in TURN

The SFU ships a built-in TURN server on :3478 (UDP + TCP). compose.prod.yml maps both to the host. For clients behind symmetric NATs that's the difference between a meeting working and failing. If you run an external coturn, drop the SFU's TURN: add -turn "" to its CMD (or override via compose) and remove the 3478 port mappings.

Postgres backups

The compose stack uses a named volume (pgdata). For operational backups:

# Daily dump
docker exec galene-postgres-1 pg_dump -U galene galene \
    | gzip > galene-$(date +%F).sql.gz

# Restore
gunzip < galene-2026-04-21.sql.gz \
    | docker exec -i galene-postgres-1 psql -U galene galene

Back up externally (S3, rsync to another host). The named volume is local to the Docker host.

Secrets

Secret Set in Shared with Purpose
authKeys[].k (SFU) / GALENE_AUTH_KEY (backend) deploy/galene/groups/meetings.json + .env.production SFU ↔ backend HMAC for the join JWTs the backend signs and browsers present
JWT_SECRET_KEY (backend) .env.production backend only Signs the user-session cookie
POSTGRES_PASSWORD .env.production backend ↔ postgres DB auth
TLS cert + key managed by Caddy meet.example.com HTTPS (auto Let's Encrypt)

Rotation

  • POSTGRES_PASSWORD: ALTER USER galene WITH PASSWORD '…'; in the DB, update .env.production, make prod-down && make prod-up. Brief downtime.
  • JWT_SECRET_KEY (backend session cookie): rotate whenever you suspect a leak. Old sessions are invalidated immediately — users have to log in again. Guest sessions are unaffected (they don't use this cookie).
  • The SFU signing key (GALENE_AUTH_KEY / authKeys): both ends must agree at all times. Galene's authKeys is a list, so a dual-key window is possible: add the new key alongside the old, restart the SFU, switch the backend to the new key, then drop the old one. Anyone holding this key can mint a token for any room with any username and permissions — treat it like a database password.
  • TLS cert: Caddy renews automatically ~30 days before expiry. No action needed unless the ACME challenge path changes (e.g. you moved behind a firewall).

File layout

Dockerfiles        → live next to the code they build
  ops/galene/Dockerfile
  backend/Dockerfile  +  backend/entrypoint.sh
  alt-backend/Dockerfile
  front/Dockerfile    +  front/Caddyfile

compose.yml        → dev infra only (Postgres for `make run`)
compose.prod.yml   → full stack (SFU + backend + Postgres + Caddy)

deploy/            → operator-edited config templates
  .env.production.example
  sfu-config.example.toml
  galene/          (gitignored; operator writes the group file + config here)

ops/               → honcho Procfiles for native dev / smoke / e2e
  Procfile
  Procfile.e2e
  Procfile.smoke
  Procfile.alt
  Procfile.alt-e2e

.env.production and deploy/galene/ are gitignored — operator-specific and secret-carrying.

Observability

  • Upstream Galene exposes /galene-api/v0/.stats (admin auth). It carries no usernames and its format is documented as unstable, so treat it as coarse health only.
  • Backend has no built-in /metrics yet — on the roadmap (§2).
  • All three services log to stdout. make prod-logs tails them; route into your aggregator (Loki, ELK, CloudWatch Logs, whatever) via your docker logging driver.
  • No distributed tracing yet — see roadmap §2 for the OpenTelemetry plan.

What's not here yet

  • CI image publish (roadmap §1.g): tagged container images pushed to a registry on every release. Today you build locally.
  • docker compose down doesn't wipe volumes. make prod-down keeps pgdata, sfu-recordings, and Caddy's cert cache. Add -v to fully reset: docker compose -f compose.prod.yml --profile alt down -v.
  • Multi-host / clustering: the SFU is single-process. A media-router cluster is a post-1.0 concern.
  • Zero-downtime secret rotation: documented workaround above; proper dual-key support is on the roadmap.

Further reading

  • Roadmap §1 (Deploy kit) — remaining work (CI publish, secret-rotation runbook polish)
  • Quickstart — native dev loop for day-to-day work
  • ops/galene/README.md — the SFU's whole configuration
  • deploy/galene/README.md — generating the production signing key
  • backend/.env.example — full backend env list