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'sauthKeysis 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
/metricsyet — on the roadmap (§2). - All three services log to stdout.
make prod-logstails 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 downdoesn't wipe volumes.make prod-downkeepspgdata,sfu-recordings, and Caddy's cert cache. Add-vto 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 configurationdeploy/galene/README.md— generating the production signing keybackend/.env.example— full backend env list