# 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"]