Files
rutster/deploy/Dockerfile
Aaron D. Lee 754614bd56
Some checks failed
CI / fmt (push) Successful in 1m1s
CI / clippy (push) Successful in 1m52s
CI / deny (push) Has been cancelled
CI / test (1.85) (push) Has started running
CI / test (stable) (push) Has been cancelled
CI / sim-bench (stable) (push) Has been cancelled
CI / twilio-live (manual only) (push) Has been cancelled
CI / image-build (4 images) (push) Has been cancelled
CI / smoke (all-in-one TLS sim call) (push) Has been cancelled
CI / smoke (T2 compose four-service) (push) Has been cancelled
CI / smoke (caddy reload during live call, zero drops) (push) Has been cancelled
fix(deploy-epoch): un-spoofable smoke gates + honest docs (deploy-epoch-fix) (#29)
Co-authored-by: Aaron D. Lee <himself@adlee.work>
Co-committed-by: Aaron D. Lee <himself@adlee.work>
2026-07-09 23:54:57 +00:00

228 lines
10 KiB
Docker

# 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/
# Baked-in env for the T1 all-in-one shape (deploy spec §5.6 + plan T5):
# the brain listens on 127.0.0.1:8082 (loopback; the resolve_tap_url
# gate rejects non-loopback until step 6), so the engine's tap URL MUST
# point at it. The default RUTSTER_TAP_URL baked into main.rs
# (ws://127.0.0.1:8081/echo) targets the slice-2 echo port, which
# nothing listens on in the T1 image — without this override the engine
# boots, the brain boots, but the tap handshake fails on every call.
# RUTSTER_VALKEY_URL points at the bundled valkey (loopback,
# same netns); makes the ValkeyEventSink LIVE per spec §5.6 (was a
# no-op TracingEventSink in the pre-T5 image).
ENV RUTSTER_TAP_URL=ws://127.0.0.1:8082/
ENV RUTSTER_VALKEY_URL=redis://127.0.0.1:6379/
# 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 — `/init` is the canonical binary installed by
# the s6-overlay noarch tarball. (Pre-T5 the entrypoint was `/s6-init` —
# a symlink the older s6-overlay v2 layout created; v3.2.0.0 ships `/init`
# directly and `/s6-init` is absent, so the allinone image failed to boot
# with `executable file not found in $PATH`.) `/init` runs the service
# bundle in /etc/s6-overlay/s6-rc.d/ via s6-rc.
ENTRYPOINT ["/init"]