7.3 KiB
Configuration guide
This deployment exposes a single public origin. Nginx serves the room UI and proxies the speech-to-text API, capture page and private relay internally.
1. docker-compose.yml
Four application services:
| Service | Loopback port | Notes |
|---|---|---|
caption-local |
8765 |
Whisper STT; state in the caption-local-keys volume at /data/rooms.db |
captionninja-relay |
8787 |
Reads secrets/relay.json |
captionninja-web |
8080 |
Public UI and same-origin proxy; bind with CAPTION_WEB_BIND |
wiki |
8081 |
Documentation wiki |
All application ports bind to 127.0.0.1. Do not widen them; public traffic
must go through Traefik and the nginx proxy.
caption-local reads optional secrets/caption-local.env. Without a configured
master and relay address, room creation returns 404.
2. docker-compose.prod.yml
Publishes only UI_HOST and WIKI_HOST through Traefik. caption-local and the
relay have no Traefik routers. The UI router reaches nginx; nginx forwards:
/api/*and/api/admin/*tocaption-local:8765/health,/relay-config,/transcribe,/streams/*,/v1/*tocaption-local:8765/static/*,/capture-local.htmland/admin.htmltocaption-local:8765/wstocaptionninja-relay:8787/room/<id>,/room/<id>/editand/watch/<id>to the static room, editor and audience pages
Requests to caption-local carry Host: 127.0.0.1:8765 because
TrustedHostMiddleware only accepts localhost names. There is no longer a
public whisper-host middleware or a separate capture/relay hostname.
3. traefik.yml
Static Traefik config: HTTP/HTTPS entrypoints, ACME resolver, Docker provider
with exposedByDefault: false. The ACME email is passed on the command line from
.env.production.
4. .env.production
Tracked and non-secret:
LETSENCRYPT_EMAIL=you@example.com
UI_HOST=ui.example.org
WIKI_HOST=wiki.example.org
CAPTURE_HOST and RELAY_HOST are no longer used.
5. Secrets
secrets/relay.json
Created by captionninja/relay/init-config.cjs:
{
"origins": ["https://ui.example.org"],
"master": "<base64url, at least 32 bytes>"
}
originsmust list every exact page origin that opens a WebSocket. With the single-origin model this is normally just the UI origin. Includehttp://localhost:8080for local testing.- The last WebSocket header usually carries the public origin, not a capture subdomain.
- Recreate the relay after editing the file.
The container runs as uid/gid 1000, so the bind-mounted file must be readable
by that user:
chown 1000:1000 secrets/relay.json
chmod 600 secrets/relay.json
secrets/caption-local.env
CAPTION_ROOMS_DB=/data/rooms.db
CAPTION_RELAY_MASTER=<same master as relay.json>
CAPTION_ADMIN_KEY=<at least 24 ASCII characters, optional>
CAPTION_ROOM_CREATION=open
RELAY_ADDRESS=wss://ui.example.org/ws
RELAY_SITE=https://ui.example.org/
CAPTION_COOKIE_SECURE=1
CAPTION_ALLOWED_ORIGINS=https://ui.example.org
| Variable | Meaning |
|---|---|
CAPTION_ROOMS_DB |
SQLite room registry path; must be on the persisted /data volume |
CAPTION_RELAY_MASTER |
Shared base64url master; enables room creation and config API |
RELAY_ADDRESS |
Relay WebSocket address handed to browsers |
RELAY_SITE |
Public UI base used to build admin/audience links |
CAPTION_ADMIN_KEY |
Protects /admin.html and /api/admin/rooms |
CAPTION_ROOM_CREATION |
open (default) or off |
CAPTION_COOKIE_SECURE |
auto by default, or force 1/0 |
CAPTION_TOKEN_TTL |
Relay token lifetime in seconds; default 3600 |
CAPTION_SESSION_TTL |
Room cookie lifetime in seconds; default 30 days |
CAPTION_MAX_ROOMS |
Active room cap; default 400 |
CAPTION_CREATE_LIMIT |
Room creations per IP per window; default 5 |
CAPTION_CREATE_WINDOW |
Rate-limit window in seconds; default 3600 |
CAPTION_ALLOWED_ORIGINS |
Extra exact origins for /transcribe; normally empty |
CAPTION_ALLOW_INSECURE_LAN |
Permit ws:///http:// relay URLs for private LAN offline use |
CAPTION_LOG_REQUESTS |
Metadata-only request logs; no tokens, IDs or captions |
The env file is optional (required: false). Do not put CAPTION_API_KEY or
relay tokens here; the shared-token API model has been replaced by room sessions.
secrets/traefik.htpasswd
Only needed with docker-compose.auth.yml. It adds a Basic Auth perimeter to the
UI and wiki. It is not the room access model.
6. Web UI image
web/Dockerfile builds the static UI from captionninja/ and serves it with
nginx.
| Variable | Purpose |
|---|---|
RELAY_URL |
Relay URL written into /site-config.js for legacy pages |
CAPTURE_URL |
Legacy capture-page URL for compatibility links |
CAPTION_UPSTREAM |
Internal Whisper upstream; default caption-local:8765 |
RELAY_UPSTREAM |
Internal relay upstream; default captionninja-relay:8787 |
TRANSLATION_MODELS |
Bergamot model download policy |
No secrets or relay tokens are injected into static pages.
7. Room API
All routes are same-origin; room routes use the caption_room cookie.
| Method | Route | Auth |
|---|---|---|
POST |
/api/rooms |
public, rate-limited, must be enabled |
POST |
/api/rooms/<id>/session |
admin secret in body or Bearer |
GET |
/api/me |
room cookie |
GET/PATCH |
/api/rooms/<id> |
room cookie |
POST |
/api/rooms/<id>/rotate |
room cookie; returns a new admin link once |
DELETE |
/api/rooms/<id> |
room cookie |
GET |
/api/rooms/<id>/config |
room cookie |
GET |
/api/public/rooms/<id> |
public, rate-limited; output read token only |
GET |
/api/admin/rooms |
admin key |
POST |
/api/admin/rooms/<id>/rotate |
admin key |
DELETE |
/api/admin/rooms/<id> |
admin key |
Public pages (/, /watch/<id>, /room/<id>, /capture-local.html,
/admin.html, /health, static assets) do not require a room cookie. They only
reveal public data; room APIs still enforce auth.
8. Offline LAN profile
Use .env.lan.example as a template. Set CAPTION_WEB_BIND to the host LAN IP,
replace the relay host placeholders, and add the LAN origin to secrets/relay.json.
Set CAPTION_ALLOW_INSECURE_LAN=1 only for an isolated private network. Run once
with CAPTION_OFFLINE=0 to cache the model, then set CAPTION_OFFLINE=1 for the
offline event.
cp .env.lan.example .env.lan
docker compose --env-file .env.lan -f docker-compose.yml up -d --build
Only the web UI is exposed. The Whisper service and relay remain loopback-only.
Use http://localhost:8080 for microphone capture on the host; use the LAN URL for
audience and editor access from other devices.
9. Running and verification
docker compose up --build -d
docker compose --env-file .env.production \
-f docker-compose.yml -f docker-compose.prod.yml config -q
docker compose --env-file .env.production \
-f docker-compose.yml -f docker-compose.prod.yml up -d
docker compose --env-file .env.production \
-f docker-compose.yml -f docker-compose.prod.yml ps
Expected flow: open UI_HOST, create a room, hold the admin link, caption, edit
and share the audience link. Verify the relay accepts browser connections from
the UI origin; a wrong origins entry or mismatched master shows as a relay
denial in the browser.