Caption Stack
Self‑hosted live captioning stack using Whisper STT and a private caption relay.
Table of Contents
- Prerequisites
- Quick start
- Submodule handling
- Configuration
- Docker Compose overview
- Running the stack
- Access URLs
- Troubleshooting
- Contributing
- License
Prerequisites
git(≥2.30)docker&docker compose(v2.x)- Internet connection for submodule download
Quick start
git clone --recurse-submodules https://git.coopcloud.tech/captions.coop/captions-deploy.git
cd captions-deploy
Submodule handling
If the repository was cloned without --recurse-submodules:
git submodule update --init --recursive
The submodules provide the Whisper service (caption-local) and the relay/web UI (captionninja).
Configuration
The relay expects a private JSON file with room tokens. Create it from the example:
cd captionninja/relay
cp .example.private.json .caption-ninja-relay.private.json
# edit the file and add your secrets
.gitignore already excludes any *.private.json files from version control.
Docker Compose overview
docker-compose.yml defines three services:
| Service | Build context | Port(s) | Purpose |
|---|---|---|---|
caption-local |
./caption-local |
8765:8765 |
Whisper speech‑to‑text service |
captionninja-relay |
./captionninja/relay |
127.0.0.1:8787:8787 |
WebSocket relay, reads the private config |
captionninja-web |
nginx:alpine |
8080:80 |
Serves the static UI and proxies /ws to the relay |
Running the stack
docker compose up --build -d
Monitor logs with docker compose logs -f.
Access URLs
- UI: http://localhost:8080
- WebSocket endpoint:
ws://localhost:8787/ws
Troubleshooting
- Missing submodules – run
git submodule update --init --recursive. - Port conflict – edit the corresponding ports in
docker-compose.yml. - Secret file not found – ensure
.caption-ninja-relay.private.jsonexists; it is ignored by git.
Contributing
- Fork the repository.
- Create a feature branch.
- Make changes and test with
docker compose up. - Open a pull request.
Production deployment with Traefik
The stack is intended to be run behind Traefik which provides automatic TLS certificates via Let’s Encrypt and optional HTTP authentication.
Prerequisites for production
- A domain name that resolves to the host where the stack will run.
- An email address for Let’s Encrypt notifications (set in
.env.production). - Docker Compose v2 or later.
Environment file
Create a file named .env.production in the repository root (it is ignored by Git):
LETSENCRYPT_EMAIL=you@example.com
The email will be used by Traefik to obtain certificates.
Secrets
- The relay requires a private JSON file with room tokens (
.caption‑ninja‑relay.private.json). It is already ignored by.gitignore. - (Optional) To protect the whole UI with a username/password, generate an htpasswd file:
mkdir -p secrets
htpasswd -nbB admin mySecretPassword > secrets/traefik.htpasswd
Then create a Docker secret: docker secret create traefik-htpasswd ./secrets/traefik.htpasswd.
Running the production stack
# Initialise submodules if you haven’t yet
git submodule update --init --recursive
# Build and start everything (Traefik will obtain TLS certs automatically)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Traefik will expose the services on the hostnames you configured in docker-compose.prod.yml (default placeholders are caption.local.yourdomain.com, relay.yourdomain.com, and ui.yourdomain.com). Adjust them to match your actual DNS records.
Access URLs (when TLS is active)
- UI:
https://ui.yourdomain.com - Relay WebSocket:
wss://relay.yourdomain.com/ws - Whisper API:
https://caption.local.yourdomain.com
Optional HTTP Basic Auth
If you created the traefik-htpasswd secret, the UI, Relay and Whisper routers are already configured with a basicAuth middleware. Browsers will display a login prompt. To disable it, simply remove the traefik.http.routers.<name>.middlewares=... labels from docker-compose.prod.yml and redeploy.
Health‑check verification
Each service now defines a Docker health‑check (see docker-compose.prod.yml). Verify they are healthy with:
docker compose ps
The STATUS column should show healthy for caption-local, captionninja-relay, captionninja-web, and traefik.
Troubleshooting
- Let’s Encrypt rate‑limit – If you repeatedly hit the ACME rate limit, delete the
letsencryptvolume (docker volume rm captions-deploy_letsencrypt) and retry after an hour. - Missing submodules – Run
git submodule update --init --recursive. - Port conflict – Ensure ports 80 and 443 are free on the host, or change the mappings in
docker-compose.prod.yml. - Auth failures – Verify the secret exists (
docker secret ls) and that theusersFilepath in the middleware label points to/run/secrets/traefik-htpasswd.
Contributing
- Fork the repository.
- Create a feature branch.
- Make changes and test with the production compose file:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d - Open a pull request.
License
This project is licensed under the GNU Affero General Public License v3.0 – see the LICENSE file. This project is licensed under the GNU Affero General Public License v3.0 – see the LICENSE file.