diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..72218a7 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,211 @@ +# syntax=docker/dockerfile:1.7 +# deploy/Dockerfile — multi-stage, four first-party images (spec §6.1). +# +# One Dockerfile, four named --target stages: +# * rutster-engine — FOB only (binary `rutster`) +# * rutster-brain — brain only (binary `rutster-brain-realtime`) +# * rutster-edge — custom xcaddy build with curated DNS plugins +# * rutster-allinone — s6-overlay v3 supervising caddy + FOB + brain + valkey-server +# +# Build all four locally: +# docker buildx build --target rutster-engine -t rutster-engine:ci -f deploy/Dockerfile . +# docker buildx build --target rutster-brain -t rutster-brain:ci -f deploy/Dockerfile . +# docker buildx build --target rutster-edge -t rutster-edge:ci -f deploy/Dockerfile . +# docker buildx build --target rutster-allinone -t rutster-allinone:ci -f deploy/Dockerfile . +# +# Toolchain pin: rust-toolchain.toml pins `channel = "1.85"`. The builder +# image is `rust:1.85-slim-bookworm` (NOT `rust:stable` — image builds must +# reproduce exactly what the MSRV matrix in CI gates; `stable` would float). +# +# libopus strategy (spec §6.1 "distro libopus0 over static-bundled"): +# * Builder stage: `apt-get install libopus-dev` — the -dev headers needed +# to compile the `opus` crate (FFI against libopus; verified: +# crates/rutster-media/Cargo.toml line 12: `opus = { workspace = true }`). +# * Runtime stages (engine + brain + all-in-one): `apt-get install libopus0` +# — the shared library binary. Brain image gets libopus0 too (see spec +# ambiguity #2 in this slice's plan): rutster-brain-realtime's +# Cargo.toml depends on rutster-media, so the linker records +# `NEEDED: libopus.so.0` in the brain ELF even though the brain never +# calls an opus symbol. Missing .so.0 would crash the brain at startup. +# * Matches the dev loop exactly. Do NOT use the audiopus_sys bundled-cmake +# fallback — system libopus is available in Debian. + +######################################## +# Stage 1: Rust builder — compiles both binaries once. +######################################## +FROM rust:1.85-slim-bookworm AS builder + +# libopus-dev: the headers the `opus` crate's build.rs expects. +RUN apt-get update && apt-get install -y --no-install-recommends \ + libopus-dev \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/src/rutster +COPY . . + +# Build both binaries in one `cargo build --bins` invocation so the +# dependency graph is compiled once (warm ccache/rustc cache covers both). +# --release is mandatory: debug builds of str0m + opus run 5-10x slower +# than the 20ms tick budget; a debug-image container would burn the tick-lag +# threshold on first call. +RUN cargo build --release --bins \ + && cp /usr/src/rutster/target/release/rutster /usr/local/bin/rutster \ + && cp /usr/src/rutster/target/release/rutster-brain-realtime /usr/local/bin/rutster-brain-realtime + +######################################## +# Stage 2: Caddy builder — xcaddy with the curated DNS plugin set. +######################################## +# caddy:2.8-builder ships Go + xcaddy. The plugin set is exactly spec §3.2 +# / TLS brief §3(a): cloudflare, route53, porkbun, hetzner, desec. +# duckdns EXCLUDED — no license file (spec §3.2). +# Plugin licenses: cloudflare=Apache-2.0; route53/porkbun/hetzner/desec=MIT. +# Aggregation-clean vs GPL-3 (Caddy core itself is Apache-2.0). +FROM caddy:2.8-builder AS caddy-builder + +RUN xcaddy build \ + --with github.com/caddy-dns/cloudflare \ + --with github.com/caddy-dns/route53 \ + --with github.com/caddy-dns/porkbun \ + --with github.com/caddy-dns/hetzner \ + --with github.com/caddy-dns/desec \ + && mv /build/caddy /usr/local/bin/caddy + +######################################## +# Stage 3: Valkey binary source (upstream image, no rebuild). +######################################## +# COPY --from pattern: pull just the valkey-server binary out of the +# upstream BSD-3 image; avoids rebuilding valkey from source (saves ~5 min +# per build + ~150 MB of build deps). The binary is statically-linked +# enough for Debian bookworm (verified by upstream's Debian-based image). +FROM valkey/valkey:8.0 AS valkey-src + +######################################## +# Target: rutster-engine — the FOB only. +######################################## +FROM debian:bookworm-slim AS rutster-engine + +# libopus0: the shared library (see libopus strategy comment at top). +RUN apt-get update && apt-get install -y --no-install-recommends \ + libopus0 \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /usr/local/bin/rutster /usr/local/bin/rutster +COPY deploy/Caddyfile /etc/rutster/Caddyfile + +# RUTSTER_HTTP_BIND defaults to 0.0.0.0:8080 (config.rs). Behind Caddy, the +# engine binds the compose-network address; behind --network host (T1 +# solo), it binds 127.0.0.1:8080. +EXPOSE 8080 +# Media UDP range — published via RUTSTER_MEDIA_PORT_RANGE. +EXPOSE 49152-49407/udp + +ENTRYPOINT ["/usr/local/bin/rutster"] + +######################################## +# Target: rutster-brain — brain only. +######################################## +FROM debian:bookworm-slim AS rutster-brain + +# libopus0: see spec ambiguity #2 in this slice's plan — +# rutster-brain-realtime's Cargo.toml depends on rutster-media which +# depends on the `opus` crate; even though the brain never calls an opus +# symbol, the ELF records `NEEDED: libopus.so.0` (linker dead-strips +# unused symbols but the .so is still required at dlopen-time before +# dead-strip can prove they're unused). A future brain-crate refactor +# that drops the rutster-media dep can drop this package too. +RUN apt-get update && apt-get install -y --no-install-recommends \ + libopus0 \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /usr/local/bin/rutster-brain-realtime /usr/local/bin/rutster-brain-realtime + +# Brain binds 127.0.0.1:8082 (loopback-only tap posture until step 6). +# In compose (T2), the brain shares the engine's netns via +# `network_mode: "service:engine"` so this loopback IS the engine's +# loopback — the FOB can reach the brain's tap port without exposing it. +EXPOSE 8082 + +ENTRYPOINT ["/usr/local/bin/rutster-brain-realtime"] + +######################################## +# Target: rutster-edge — custom Caddy build. +######################################## +# scratch/alpine choice (spec §6.1): alpine gives a shell for debugging +# (`docker exec -it rutster-edge sh`); scratch is purer but undebuggable. +# Alpine's musl is fine for the static Go binary xcaddy produces. +FROM alpine:3.20 AS rutster-edge + +# ca-certificates: ACME TLS validation. curl: smoke-test `caddy reload` +# invocations + `curl /healthz` from inside the container. +RUN apk add --no-cache ca-certificates curl + +COPY --from=caddy-builder /usr/local/bin/caddy /usr/local/bin/caddy +COPY deploy/Caddyfile /etc/caddy/Caddyfile + +# Caddy needs /data for ACME state — losing it risks the Let's Encrypt +# duplicate-cert lockout (spec §2.1, TLS brief §5 risk 5). Mount a volume +# at /data in production. In CI (local_certs) /data just holds the internal +# CA — recreating is fine, but the volume mount keeps prod + CI on the +# same code path. +VOLUME /data + +EXPOSE 80 443 + +ENTRYPOINT ["/usr/local/bin/caddy"] +CMD ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] + +######################################## +# Target: rutster-allinone — s6-overlay v3 supervising four processes. +######################################## +FROM debian:bookworm-slim AS rutster-allinone + +# libopus0 (FOB needs it; brain too — see rutster-brain target above). +# curl: smoke-test invocations + `caddy reload` exec. +# ca-certificates: ACME. +# xz: s6-overlay binary tarball extraction (extract-then-rm). +RUN apt-get update && apt-get install -y --no-install-recommends \ + libopus0 \ + ca-certificates \ + curl \ + xz-utils \ + && rm -rf /var/lib/apt/lists/* + +# s6-overlay v3.2.0.0 — ISC license (aggregation-clean vs GPL-3). +# Two tarballs: noarch (the service definitions + s6-rc bundle) + x86_64 +# (the binaries — linux/amd64 only per spec §1.2). arm64 is a named +# deferral (ADR-0011 §1.2); when it lands, this Dockerfile grows an +# `ARG TARGETARCH` + `COPY` per-arch. +COPY deploy/s6-overlay-noarch.tar.xz /tmp/s6-overlay-noarch.tar.xz +COPY deploy/s6-overlay-x86_64.tar.xz /tmp/s6-overlay-x86_64.tar.xz +RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \ + && tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz \ + && rm /tmp/s6-overlay-noarch.tar.xz /tmp/s6-overlay-x86_64.tar.xz + +# Binaries from earlier stages. +COPY --from=builder /usr/local/bin/rutster /usr/local/bin/rutster +COPY --from=builder /usr/local/bin/rutster-brain-realtime /usr/local/bin/rutster-brain-realtime +COPY --from=caddy-builder /usr/local/bin/caddy /usr/local/bin/caddy +COPY --from=valkey-src /usr/local/bin/valkey-server /usr/local/bin/valkey-server + +# Caddyfile + s6 service definitions (long-lined below). +COPY deploy/Caddyfile /etc/caddy/Caddyfile +COPY deploy/s6-rc.d/ /etc/s6-overlay/s6-rc.d/ + +# Volumes: Caddy /data (ACME state — loss risks LE lockout per TLS brief +# §5 risk 5) + Valkey /var/lib/valkey (stream/state persistence per +# ADR-0005). +VOLUME /data /var/lib/valkey + +# Caddy :80/:443 public; FOB :8080 behind Caddy; brain :8082 loopback; +# valkey :6379 loopback; media UDP direct. +EXPOSE 80 443 8080 8082 6379 49152-49407/udp + +# s6-overlay v3 entrypoint — the binary `s6-overlay-init` (symlinked from +# /s6-init in the noarch tarball) runs the service bundle in +# /etc/s6-overlay/s6-rc.d/ via s6-rc. +ENTRYPOINT ["/s6-init"] diff --git a/deploy/s6-overlay-noarch.tar.xz b/deploy/s6-overlay-noarch.tar.xz new file mode 100644 index 0000000..57f85ce Binary files /dev/null and b/deploy/s6-overlay-noarch.tar.xz differ diff --git a/deploy/s6-overlay-x86_64.tar.xz b/deploy/s6-overlay-x86_64.tar.xz new file mode 100644 index 0000000..23cf532 Binary files /dev/null and b/deploy/s6-overlay-x86_64.tar.xz differ diff --git a/deploy/s6-overlay.sha256sum b/deploy/s6-overlay.sha256sum new file mode 100644 index 0000000..ba60a40 --- /dev/null +++ b/deploy/s6-overlay.sha256sum @@ -0,0 +1,2 @@ +4b0c0907e6762814c31850e0e6c6762c385571d4656eb8725852b0b1586713b6 s6-overlay-noarch.tar.xz +ad982a801bd72757c7b1b53539a146cf715e640b4d8f0a6a671a3d1b560fe1e2 s6-overlay-x86_64.tar.xz diff --git a/deploy/s6-rc.d/brain/dependencies.d/engine b/deploy/s6-rc.d/brain/dependencies.d/engine new file mode 100644 index 0000000..91fd5a6 --- /dev/null +++ b/deploy/s6-rc.d/brain/dependencies.d/engine @@ -0,0 +1 @@ +# empty — brain waits for the engine before attempting the tap WS. diff --git a/deploy/s6-rc.d/brain/run b/deploy/s6-rc.d/brain/run new file mode 100755 index 0000000..d4596ff --- /dev/null +++ b/deploy/s6-rc.d/brain/run @@ -0,0 +1,6 @@ +#!/command/execlineb -P +# s6-rc service: rutster-brain-realtime. Shares the engine's network +# namespace (T1 solo variant — the all-in-one is a single container so +# loopback IS shared). The brain binds 127.0.0.1:8082 — the FOB reaches it +# via RUTSTER_TAP_URL=ws://127.0.0.1:8082. +exec /usr/local/bin/rutster-brain-realtime diff --git a/deploy/s6-rc.d/caddy/dependencies.d/engine b/deploy/s6-rc.d/caddy/dependencies.d/engine new file mode 100644 index 0000000..579788c --- /dev/null +++ b/deploy/s6-rc.d/caddy/dependencies.d/engine @@ -0,0 +1 @@ +# empty file — s6-rc reads the basename as the dependency name. diff --git a/deploy/s6-rc.d/caddy/run b/deploy/s6-rc.d/caddy/run new file mode 100755 index 0000000..522ed6b --- /dev/null +++ b/deploy/s6-rc.d/caddy/run @@ -0,0 +1,6 @@ +#!/command/execlineb -P +# s6-rc service: caddy — the edge (ACME + TLS termination + WS proxy). +# Depends on: engine (s6-rc starts the engine first per caddy/dependencies.d/engine). +# User: root (caddy needs :80/:443). +foreground { mkdir -p /data } +exec /usr/local/bin/caddy run --config /etc/caddy/Caddyfile --adapter caddyfile diff --git a/deploy/s6-rc.d/engine/dependencies.d/valkey-database b/deploy/s6-rc.d/engine/dependencies.d/valkey-database new file mode 100644 index 0000000..73c1fb9 --- /dev/null +++ b/deploy/s6-rc.d/engine/dependencies.d/valkey-database @@ -0,0 +1,3 @@ +# empty — engine doesn't WAIT on valkey (it must boot even if valkey is down +# per ADR-0005's fail-safe posture: EventSink counts-and-drops, never blocks). +# The dependency just orders boot for consistent log sequencing. diff --git a/deploy/s6-rc.d/engine/run b/deploy/s6-rc.d/engine/run new file mode 100755 index 0000000..089ac05 --- /dev/null +++ b/deploy/s6-rc.d/engine/run @@ -0,0 +1,7 @@ +#!/command/execlineb -P +# s6-rc service: rutster — the FOB. Runs the rutster binary (axum + media +# thread + trunk WS + /metrics). Reads its env from the container env +# directly (s6-overlay v3 inherits container env via s6-overlay-envdir — +# this slice relies on the container env for simplicity; the per-service +# env files are a future hardening step). +exec /usr/local/bin/rutster diff --git a/deploy/s6-rc.d/user/contents.d/brain b/deploy/s6-rc.d/user/contents.d/brain new file mode 100644 index 0000000..e69de29 diff --git a/deploy/s6-rc.d/user/contents.d/caddy b/deploy/s6-rc.d/user/contents.d/caddy new file mode 100644 index 0000000..e69de29 diff --git a/deploy/s6-rc.d/user/contents.d/engine b/deploy/s6-rc.d/user/contents.d/engine new file mode 100644 index 0000000..e69de29 diff --git a/deploy/s6-rc.d/user/contents.d/valkey-database b/deploy/s6-rc.d/user/contents.d/valkey-database new file mode 100644 index 0000000..e69de29 diff --git a/deploy/s6-rc.d/valkey-database/dependencies.d/engine b/deploy/s6-rc.d/valkey-database/dependencies.d/engine new file mode 100644 index 0000000..d2aea58 --- /dev/null +++ b/deploy/s6-rc.d/valkey-database/dependencies.d/engine @@ -0,0 +1 @@ +# empty — orders valkey after engine in boot sequence. diff --git a/deploy/s6-rc.d/valkey-database/run b/deploy/s6-rc.d/valkey-database/run new file mode 100755 index 0000000..997cc91 --- /dev/null +++ b/deploy/s6-rc.d/valkey-database/run @@ -0,0 +1,7 @@ +#!/command/execlineb -P +# s6-rc service: valkey-server — bus + KV + presence (ADR-0005). Dark on +# day one except EventSink consumer; the operator contract (volumes, ports, +# upgrade shape) is stable so the spend ledger + fleet directory land later +# without changing the deployment shape (spec §2.1). +foreground { mkdir -p /var/lib/valkey } +exec /usr/local/bin/valkey-server --dir /var/lib/valkey --save 60 1 --appendonly yes