Deploy with Docker Compose¶
Who this is for: hosters and developers — development and evaluation.
Docker Compose runs the whole stack from the
captions-deploy
repository on a single machine. It is best for development, local evaluation
and testing — try a change or rehearse an event without touching a server. For
a real, internet-facing deployment, prefer
Co-op Cloud.
Nothing here is thrown away: the same speech service, relay and web UI run in both methods.
What you get¶
| Service | Internal port | Serves |
|---|---|---|
caption-local |
8765 | Speech service and capture page |
captionninja-relay |
8787 | Private WebSocket relay |
captionninja-web |
80 | Static web UI (proxies /ws to the relay) |
In the base file, all three bind to loopback only (127.0.0.1:*). The
production override adds Traefik, which terminates TLS and is the only thing
reachable from the outside.
Prepare the repository¶
Clone it with its submodules — the build needs the application source they carry.
git clone --recurse-submodules https://git.coopcloud.tech/captions.coop/captions-deploy.git
cd captions-deploy
If you already cloned without them, fetch the submodules.
git submodule update --init --recursive
Generate the relay configuration¶
The relay reads a private JSON file with room tokens and the exact browser origins allowed to connect. Generate one with fresh random tokens.
mkdir -p secrets
docker run --rm -v "$PWD/captionninja/relay":/w -v "$PWD/secrets":/s -w /w node:24-alpine \
node init-config.cjs /s/relay.private.json
Grant the container's user (uid/gid 1000) read access without making the file
world-readable.
docker run --rm -v "$PWD/secrets/relay.private.json":/c alpine \
sh -c 'chown 1000:1000 /c && chmod 600 /c'
Edit the origins array to the exact origins your pages are served from (scheme
included, no trailing slash). See Relay and rooms
and Access and sharing.
Run it locally¶
Build and start the stack; the UI is pre-wired to the local relay and capture page, so the landing-page links work with no extra configuration.
docker compose up --build -d
docker compose logs -f
Local addresses:
- UI / hub — http://localhost:8080
- Capture page — http://localhost:8765
- Relay —
ws://localhost:8787(through the UI proxy:ws://localhost:8080/ws)
Stop it while keeping the downloaded models.
docker compose down
Optional: protect the speech service¶
The speech service can require a bearer token on its API. Create the git-ignored
secrets/caption-local.env.
CAPTION_API_KEY=<at least 24 ASCII characters, no whitespace>
CAPTION_ALLOWED_ORIGINS=https://ui.example.org,https://caption.local.example.org
Leave the file out entirely for the trusted-loopback model. The production Compose file treats it as optional, so nothing breaks when it is absent.
Optional: production-style TLS locally¶
To rehearse the public setup, use the production override. It adds Traefik and a
persistent letsencrypt volume. Set your hostnames and ACME email in the tracked
.env.production file first (it is not secret).
LETSENCRYPT_EMAIL=you@example.org
CAPTURE_HOST=caption.local.example.org
RELAY_HOST=relay.example.org
UI_HOST=ui.example.org
Then build and start everything, with certificates obtained automatically.
docker compose --env-file .env.production \
-f docker-compose.yml -f docker-compose.prod.yml up -d
The whisper-host middleware is required
The speech service only accepts the hosts localhost, 127.0.0.1 and
[::1]. Traefik rewrites the backend Host header through a whisper-host
middleware. Without it the service returns 400 Bad Request.
Optional: HTTP Basic Auth¶
Basic Auth is opt-in through a separate overlay. Create the password file, then add the overlay when you start the stack.
mkdir -p secrets
htpasswd -nbB admin mySecretPassword > secrets/traefik.htpasswd
docker compose --env-file .env.production \
-f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.auth.yml up -d
Check and maintain¶
Verify every service reports healthy.
docker compose --env-file .env.production \
-f docker-compose.yml -f docker-compose.prod.yml ps
To force a TLS renewal after a DNS change, stop Traefik, delete the certificate volume, and start it again.
docker compose -f docker-compose.yml -f docker-compose.prod.yml down traefik
docker volume rm $(docker volume ls -q | grep letsencrypt)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d traefik
Continue with Backups, upgrades and recovery and Logs, privacy and security.