Deployment
The system is fully containerised. Docker Compose brings up Redis, two navigation collectors, two dive detectors, the LiveLink Slack notifier, the SvelteKit dashboard, a Caddy TLS-terminating reverse proxy, and two coax video bridges — ten services in total — with a single command.
Prerequisites
- Docker Engine 24+ and Docker Compose v2
- Network access to:
- NATS servers:
coredata-rcsn.rc.mbari.org:4222andcoredata-dpkd.dp.mbari.org:4222 - OBS WebSocket servers:
rc-teleops.rc.mbari.org:4455anddp-teleops.rc.mbari.org:4455 - The HLS origin the coax bridges pull from
(
teledistributor.shore.mbari.org:5080) and the downstream transcoder/modulator hardware they send UDP to (134.89.5.12, ports2345/2346) - Each OBS instance must have scenes named
MainOnlyandFeedOfflinedefined before the detector is started. - The Docker host running
rc-coax-bridge/dp-coax-bridgemust have an actual network route to134.89.5.12— these two services usenetwork_mode: host, so they reach the modulator hardware over the host's own network stack rather than through Compose'sredisnetbridge.
Starting all services
This starts ten containers:
| Compose service | Container name | Description |
|---|---|---|
redis |
redis |
Shared data cache |
rc-navproc_collector |
navproc_collector_rcsn |
NATS → Redis for Rachel Carson |
dp-navproc_collector |
navproc_collector_dpkd |
NATS → Redis for David Packard |
rc-dive-detector |
dive_detector_rcsn |
Dive detection for Rachel Carson |
dp-dive-detector |
dive_detector_dpkd |
Dive detection for David Packard |
livelink_slackbot |
livelink_slackbot |
Posts Redis state changes to a Slack workflow webhook |
frontend |
dive_detector_frontend |
SvelteKit dashboard (HTTP on internal port 3000) |
caddy |
dive_detector_caddy |
TLS-terminating reverse proxy on ports 80/443 |
rc-coax-bridge |
rc-coax-bridge |
HLS → UDP coax video bridge for Rachel Carson |
dp-coax-bridge |
dp-coax-bridge |
HLS → UDP coax video bridge for David Packard |
Unlike the other eight services, the two coax bridges run with
network_mode: host and don't join redisnet — they don't talk to
Redis or anything else in the stack, only to the HLS origin and the
modulator hardware. See Coax Video for the pipeline
details.
The dashboard is then available at
https://<configured-hostname> once you've placed the cert files (see
Enabling TLS below). Until then the caddy container
will fail to start because the cert files don't exist.
Viewing logs
# All services
docker compose logs -f
# A single service
docker compose logs -f rc-dive-detector
docker compose logs -f frontend
docker compose logs -f rc-coax-bridge
Stopping
Rebuilding images
After a code change in a single service, rebuild just that one:
docker compose up -d --build frontend
docker compose up -d --build rc-dive-detector dp-dive-detector
docker compose up -d --build rc-coax-bridge dp-coax-bridge
To rebuild everything:
Service configuration
Environment variables live in compose.yaml. See Configuration for the full reference. Key per-service variables:
navproc_collector
| Variable | Example | Description |
|---|---|---|
SHIP_NAME |
rcsn |
Ship identifier (NATS bucket name) |
SHIP_DOMAIN |
rc |
NATS domain suffix |
REDIS_HOST |
redis |
Redis hostname (Docker service name) |
POLL_INTERVAL |
10 |
Seconds between data pulls |
The collector's NATS key allow-list is in
backend/collector_keys.toml —
edits there require a collector rebuild.
dive_detector
| Variable | Example | Description |
|---|---|---|
SHIP_NAME |
rcsn |
Ship identifier (Redis key prefix) |
OBS_HOST |
rc-teleops.rc.mbari.org |
OBS WebSocket server hostname |
OBS_PORT |
4455 |
OBS WebSocket port |
OBS_PASSWORD |
(secret) | OBS WebSocket password, empty if no auth |
REDIS_HOST |
redis |
Redis hostname |
POLL_INTERVAL |
10 |
Seconds between Redis polls |
frontend
| Variable | Example | Description |
|---|---|---|
REDIS_HOST |
redis |
Redis hostname |
REDIS_PORT |
6379 |
Redis port |
NODE_ENV |
production |
Node environment |
coax_bridge
| Variable | Example | Description |
|---|---|---|
HLS_URL |
http://teledistributor.shore.mbari.org:5080/LiveLink/streams/rvrachelcarson.m3u8 |
HLS playlist to pull (required) |
UDP_HOST |
134.89.5.12 |
Transcoder/modulator hardware IP (required) |
UDP_PORT |
2346 |
UDP destination port — 2345 for David Packard, 2346 for Rachel Carson |
ALIGNMENT |
7 |
MPEG-TS packets per UDP payload |
No REDIS_HOST here — this service doesn't touch Redis. See
Coax Video for what the pipeline does with these
variables.
Dockerfiles
docker/dive_detector.dockerfile
Builds the image for both dive-detector services. Based on
ghcr.io/astral-sh/uv:python3.13-trixie-slim. Dependencies are installed
from the lockfile with uv sync.
docker/navproc_collector.dockerfile
Builds the image for both navproc-collector services. Same base image
and dependency approach. The ADD /backend /app step automatically
includes collector_keys.toml in the image.
docker/frontend.dockerfile
Multi-stage Node 22 build. Stage 1 installs all deps and runs
npm run build to produce the SvelteKit Node bundle. Stage 2 contains
only production dependencies plus the build output, and runs
node build.
docker/coax_bridge.dockerfile
Just the restreamio/gstreamer base image (it already bundles the
GStreamer plugins the pipeline needs) plus coax_bridge/entrypoint.sh
copied in as the entrypoint — no dependency install step, since there's
no application code beyond the shell script.
Running a single ship only
To run only the R/V Rachel Carson services plus the dashboard:
Add rc-coax-bridge to that list if you also want the coax feed for
that ship running:
Note rc-coax-bridge has no depends_on — it only needs the HLS
origin to be live, not Redis or any other Dive Detector service, so it
can be started and stopped independently of everything else.
Enabling TLS
The compose stack includes a Caddy reverse
proxy in front of the SvelteKit container. Caddy terminates TLS on
ports 80/443 and proxies plain HTTP requests to frontend:3000 over
the internal redisnet network — the SvelteKit container is no longer
exposed externally.
To turn it on with an MBARI-issued cert:
-
Drop cert files into
certs/. The Caddyfile expectscerts/dashboard.crt(full-chain PEM) andcerts/dashboard.key(PEM-encoded private key). Seecerts/README.md. Both files are gitignored. -
Set the hostname. Edit
Caddyfileand replacedashboard.internal.mbari.orgwith the hostname this server answers to. Make sure DNS resolves that hostname to the docker host. -
Match
ORIGINto the hostname. Edit thefrontendservice incompose.yaml:
This tells SvelteKit that incoming requests should be treated as
HTTPS (so the session cookie picks up the Secure flag and links
resolve correctly), even though the request to the SvelteKit
container itself arrives over HTTP from Caddy.
- Bring it up.
Caddy will reload the Caddyfile on container restart. Logs land in
docker compose logs -f caddy.
- (Optional) Re-enable SvelteKit's CSRF origin check. With
ORIGINset, the check that we disabled earlier insvelte.config.jswill pass cleanly. Flipkit.csrf.checkOriginback totrueif you want that extra layer.
Renewing or rotating certs
Drop the new cert/key files into certs/ (same filenames) and:
Caddy picks them up without rebuilding.
Local debug without TLS
If you need direct HTTP access to the SvelteKit container (e.g. for
troubleshooting), uncomment the port mapping under the frontend
service in compose.yaml:
Re-comment it out when you're done so the only public surface is the Caddy-terminated HTTPS port.
Checking Redis data
Use the Redis dump tools to verify the navigation collector is
populating data. Run them on the host with uv installed:
# Static snapshot of all keys under rcsn:
uv run backend/redis_dump.py rcsn
# Live auto-refreshing TUI (terminal mode)
uv run backend/redis_dump_textual.py rcsn
# Or have it serve a browser-friendly TUI
uv run backend/redis_dump_textual.py rcsn --web --host 0.0.0.0 --port 8000
If Redis isn't exposed on the host, exec into one of the running
containers — they all have redis-cli-like access via Python:
See Redis Tools for more options.