feat(deploy): multi-stage Dockerfile producing four first-party images (deploy-B §6.1)
One Dockerfile, four named --target stages: rutster-engine (FOB), rutster-brain (brain), rutster-edge (custom xcaddy + curated DNS plugins cloudflare/route53/porkbun/hetzner/desec, duckdns excluded per spec §3.2), rutster-allinone (s6-overlay v3 supervising caddy + FOB + brain + valkey-server). Builder pins rust:1.85-slim-bookworm (matches rust-toolchain.toml). libopus-dev in builder, libopus0 in runtime (matches dev — spec §6.1 distro-over-static-bundled). Brain image gets libopus0 too: although the brain never calls an opus symbol, the linker records NEEDED:libopus.so.0 because rutster-brain-realtime depends on rutster-media (spec ambiguity resolved in this slice's plan README). All-in-one vendors s6-overlay v3.2.0.0 (ISC) + uses upstream valkey/valkey binary (COPY --from pattern, BSD-3). Bundled-binary licenses aggregation-clean vs GPL-3 (Caddy Apache-2.0). No Rust crates touched; cargo-deny unaffected. s6-rc service definitions land in this commit (caddy/engine/brain/ valkey-database run scripts + dependency edges). The Caddyfile lands in Task 3 (same PR); the image-build CI job (Task 7) runs after both. Signed-off-by: Aaron D. Lee <himself@adlee.work>
This commit is contained in:
211
deploy/Dockerfile
Normal file
211
deploy/Dockerfile
Normal file
@@ -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"]
|
||||||
BIN
deploy/s6-overlay-noarch.tar.xz
Normal file
BIN
deploy/s6-overlay-noarch.tar.xz
Normal file
Binary file not shown.
BIN
deploy/s6-overlay-x86_64.tar.xz
Normal file
BIN
deploy/s6-overlay-x86_64.tar.xz
Normal file
Binary file not shown.
2
deploy/s6-overlay.sha256sum
Normal file
2
deploy/s6-overlay.sha256sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
4b0c0907e6762814c31850e0e6c6762c385571d4656eb8725852b0b1586713b6 s6-overlay-noarch.tar.xz
|
||||||
|
ad982a801bd72757c7b1b53539a146cf715e640b4d8f0a6a671a3d1b560fe1e2 s6-overlay-x86_64.tar.xz
|
||||||
1
deploy/s6-rc.d/brain/dependencies.d/engine
Normal file
1
deploy/s6-rc.d/brain/dependencies.d/engine
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# empty — brain waits for the engine before attempting the tap WS.
|
||||||
6
deploy/s6-rc.d/brain/run
Executable file
6
deploy/s6-rc.d/brain/run
Executable file
@@ -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
|
||||||
1
deploy/s6-rc.d/caddy/dependencies.d/engine
Normal file
1
deploy/s6-rc.d/caddy/dependencies.d/engine
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# empty file — s6-rc reads the basename as the dependency name.
|
||||||
6
deploy/s6-rc.d/caddy/run
Executable file
6
deploy/s6-rc.d/caddy/run
Executable file
@@ -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
|
||||||
3
deploy/s6-rc.d/engine/dependencies.d/valkey-database
Normal file
3
deploy/s6-rc.d/engine/dependencies.d/valkey-database
Normal file
@@ -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.
|
||||||
7
deploy/s6-rc.d/engine/run
Executable file
7
deploy/s6-rc.d/engine/run
Executable file
@@ -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
|
||||||
0
deploy/s6-rc.d/user/contents.d/brain
Normal file
0
deploy/s6-rc.d/user/contents.d/brain
Normal file
0
deploy/s6-rc.d/user/contents.d/caddy
Normal file
0
deploy/s6-rc.d/user/contents.d/caddy
Normal file
0
deploy/s6-rc.d/user/contents.d/engine
Normal file
0
deploy/s6-rc.d/user/contents.d/engine
Normal file
0
deploy/s6-rc.d/user/contents.d/valkey-database
Normal file
0
deploy/s6-rc.d/user/contents.d/valkey-database
Normal file
1
deploy/s6-rc.d/valkey-database/dependencies.d/engine
Normal file
1
deploy/s6-rc.d/valkey-database/dependencies.d/engine
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# empty — orders valkey after engine in boot sequence.
|
||||||
7
deploy/s6-rc.d/valkey-database/run
Executable file
7
deploy/s6-rc.d/valkey-database/run
Executable file
@@ -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
|
||||||
Reference in New Issue
Block a user