Skip to content

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:

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.