From d96aa70c2709a8ce158ad64af46d138399338b0f Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Fri, 17 Apr 2026 14:59:49 -0400 Subject: [PATCH] deploy(truenas): Core image + compose for M2 WebRTC rollout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a dedicated deploy bundle under deploy/truenas/core/ so the real root Core binary — with the M2 WebRTC subsystem wired in — can replace the M1 webrtc-poc stack on the TrueNAS host. - Dockerfile: two-stage build on golang:1.24-alpine3.20 + alpine:3.20 runtime. FFmpeg is bundled so restream processes have their subprocess path ready. Copies the core binary from core/core (Go places the output file inside the core/ package directory because it can't overwrite a directory with a file) plus import and ffmigrate from the repo root. - docker-compose.yml: host-networked Core service, env-driven config (CORE_ADDRESS, CORE_API_AUTH_*, CORE_WEBRTC_ENABLE, CORE_WEBRTC_PUBLIC_IP), with config/ and data/ bind mounts. - README.md: M1→M2 cutover notes, one-time setup, JWT smoke test against /api/v3/whep/:id, and teardown. Verified: make release + make import + make ffmigrate all cross-compile cleanly for linux/amd64; go build ./... and go test ./... pass on the branch. Co-Authored-By: Claude Opus 4.7 --- deploy/truenas/core/Dockerfile | 60 +++++++++++++++ deploy/truenas/core/README.md | 102 +++++++++++++++++++++++++ deploy/truenas/core/docker-compose.yml | 56 ++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 deploy/truenas/core/Dockerfile create mode 100644 deploy/truenas/core/README.md create mode 100644 deploy/truenas/core/docker-compose.yml diff --git a/deploy/truenas/core/Dockerfile b/deploy/truenas/core/Dockerfile new file mode 100644 index 0000000..6495ab3 --- /dev/null +++ b/deploy/truenas/core/Dockerfile @@ -0,0 +1,60 @@ +# Dragon Fork datarhei Core image (M2 + WebRTC egress). +# +# Builds the real root Core binary — the one that replaces the M1 PoC +# in production. FFmpeg is baked in so restream processes can run the +# RTP output legs emitted by the WebRTC subsystem. +# +# Two-stage: +# 1. builder: compile a static Go binary (CGO off — no dynamic libs) +# 2. runtime: alpine with ffmpeg for the subprocess path +# +# Usage via compose: +# docker compose -f deploy/truenas/core/docker-compose.yml up -d --build +# +# The compose file drives configuration via CORE_* env vars — see +# README.md in this directory. + +# ---- builder ---- +# go.mod requires go 1.24; pinning the image keeps Docker's toolchain +# download off the hot path and makes the build reproducible. +FROM golang:1.24-alpine3.20 AS builder + +WORKDIR /src +RUN apk add --no-cache git make + +COPY . . + +ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 +RUN make release && make import && make ffmigrate + +# ---- runtime ---- +# Alpine with ffmpeg (Core shells out to it for every restream process). +# Scratch isn't an option here because the process manager needs ffmpeg +# on PATH. +FROM alpine:3.20 AS runtime + +RUN apk add --no-cache ffmpeg tini ca-certificates + +# make release's `-o core` lands the binary inside the core/ Go +# package directory (Go cannot overwrite a directory with a file, so +# it places the output file _inside_ it). The `import` and `ffmigrate` +# Makefile targets cd into app/ and write the binary back up to +# the repo root with a relative path, so those end up at /src/import +# and /src/ffmigrate. +COPY --from=builder /src/core/core /core/bin/core +COPY --from=builder /src/import /core/bin/import +COPY --from=builder /src/ffmigrate /core/bin/ffmigrate +COPY --from=builder /src/mime.types /core/mime.types +COPY --from=builder /src/run.sh /core/bin/run.sh + +RUN mkdir -p /core/config /core/data + +ENV CORE_CONFIGFILE=/core/config/config.json +ENV CORE_STORAGE_DISK_DIR=/core/data +ENV CORE_DB_DIR=/core/config + +VOLUME ["/core/data", "/core/config"] +EXPOSE 8080/tcp + +ENTRYPOINT ["/sbin/tini", "--", "/core/bin/run.sh"] +WORKDIR /core diff --git a/deploy/truenas/core/README.md b/deploy/truenas/core/README.md new file mode 100644 index 0000000..e79ff6f --- /dev/null +++ b/deploy/truenas/core/README.md @@ -0,0 +1,102 @@ +# TrueNAS deploy — datarhei Core (M2, WebRTC-in-Core) + +Host-networked Docker stack that runs the real root Core binary with +the M2 WebRTC egress subsystem wired in. This replaces the M1 +`webrtc-poc` stack — WebRTC is now a first-class output alongside +RTMP/SRT/HLS. + +## What changed from M1 + +| M1 (webrtc-poc) | M2 (this stack) | +| -------------------------------------- | -------------------------------------------- | +| Standalone `cmd/webrtc-poc` binary | Full Core with restream, HTTP API, storage | +| One hard-coded stream id | Every restream process can opt into WebRTC | +| Single UDP ingest, PT-split forwarding | Two UDP ports per process, per-track | +| Plain `/whep/:id` on a side port | `/api/v3/whep/:id` on the JWT-protected API | +| No auth | JWT (same creds as the rest of Core) | + +## Prereqs + +- Docker on the TrueNAS host (TrueNAS SCALE includes it) +- LAN or public IP that clients can reach (set in `.env` as `PUBLIC_IP`) +- Admin credentials for Core's API +- FFmpeg is bundled in the image — no host install required + +## One-time setup + +``` +sudo mkdir -p /mnt/NVME/Docker/dragonfork-core +cd /mnt/NVME/Docker/dragonfork-core + +# Pull the repo (or sync deploy files) onto the host. The compose +# build `context:` points at the repo root. +git clone https://forgejo.wilddragon.net/zgaetano/datarhei-dragonfork-core.git +cd datarhei-dragonfork-core/deploy/truenas/core + +cat > .env <"}' \ + http://10.0.0.25:8080/api/login | jq -r '.access_token') + +# Probe the WHEP endpoint — should 404 for an unknown id. +curl -i -H "Authorization: Bearer $TOKEN" \ + -X POST http://10.0.0.25:8080/api/v3/whep/nope +# → HTTP/1.1 404 Not Found + +# Create a process with WebRTC enabled, send RTMP to its input, then +# subscribe the Pion whep-client to /api/v3/whep/. +``` + +## Cutting over from the M1 PoC + +The M1 `webrtc-poc` stack is independent; it binds its own ports. You +can run both side-by-side during the cutover: + +``` +# Stop the M1 stack when you're ready to retire it: +cd /mnt/NVME/Docker/dragonfork-webrtc-poc +docker compose down +``` + +## Teardown + +``` +docker compose down +``` + +## Security notes + +- The WHEP endpoint is mounted under `/api/v3`, which is JWT-protected. + That's the M2 posture — WHEP clients (browsers) need a token. M3 + adds per-process signed-URL tokens so embeds don't require admin + credentials. +- The binary runs as root inside the container; if you need an unpriv + user, mount volumes owned by a fixed UID and add a `user:` directive. + This matches how the upstream datarhei/core image ships. +- Put Caddy or nginx in front for TLS. The media itself is + DTLS-SRTP-encrypted regardless. diff --git a/deploy/truenas/core/docker-compose.yml b/deploy/truenas/core/docker-compose.yml new file mode 100644 index 0000000..47a2136 --- /dev/null +++ b/deploy/truenas/core/docker-compose.yml @@ -0,0 +1,56 @@ +# Dragon Fork datarhei Core — M2 deployment with WebRTC egress. +# +# This replaces the M1 webrtc-poc stack. It runs the real root Core +# binary with the WebRTC subsystem wired into the restream manager, so +# every process whose config has `webrtc.enabled=true` will have its +# output fanned out to WHEP subscribers automatically. +# +# Host networking is required for the same reason as M1: ICE encodes +# host:port pairs into SDP candidates, and bridge-mode port mapping +# breaks that. +# +# Copy this file to /mnt/NVME/Docker/dragonfork-core/ alongside a .env: +# +# PUBLIC_IP=10.0.0.25 +# API_AUTH_USERNAME=admin +# API_AUTH_PASSWORD=change-me-please +# API_AUTH_JWT_SECRET=<32+ random bytes, base64> +# +# Then: +# docker compose up -d --build +# docker compose logs -f + +services: + core: + build: + context: ../../.. # repo root (where go.mod lives) + dockerfile: deploy/truenas/core/Dockerfile + container_name: dragonfork-core + restart: unless-stopped + network_mode: host + environment: + # --- API --- + CORE_ADDRESS: ":${CORE_HTTP_PORT:-8080}" + CORE_API_AUTH_ENABLE: "true" + CORE_API_AUTH_USERNAME: "${API_AUTH_USERNAME:?set in .env}" + CORE_API_AUTH_PASSWORD: "${API_AUTH_PASSWORD:?set in .env}" + CORE_API_AUTH_JWT_SECRET: "${API_AUTH_JWT_SECRET:?set in .env}" + + # --- WebRTC egress --- + CORE_WEBRTC_ENABLE: "true" + CORE_WEBRTC_PUBLIC_IP: "${PUBLIC_IP:?set in .env}" + # Leave NAT1To1_IPS empty unless you need multiple advertised IPs. + # CORE_WEBRTC_NAT_1_TO_1_IPS: "10.0.0.25 203.0.113.10" + + # --- Storage --- + # Let the volumes below provide durable paths; defaults are fine. + + # --- Logging --- + CORE_LOG_LEVEL: "${LOG_LEVEL:-info}" + + volumes: + - ./config:/core/config + - ./data:/core/data + + # No ports: host networking exposes whatever the process binds. + # The WHEP endpoint lives at /api/v3/whep/:id on CORE_HTTP_PORT.