# Multi-stage build for the didbot-dev binary, which — see its module doc at # crates/didbot-serve/src/bin/didbot-dev.rs — is the same binary for a laptop # and for this image: it picks its DNS backend from the zone rather than # hardcoding the development one, and refuses `--tls acme` at startup unless # it is given somewhere durable to keep the certificate (`--data`), a zone # outside `.localhost`, and a `--route53-zone-id` for every zone it serves. # # Two stages so the runtime image never sees a compiler, a registry cache, or # any crate source: what ships is the binary and nothing that built it. FROM rust:1.90-slim-bookworm AS builder WORKDIR /build RUN apt-get update \ && apt-get install -y --no-install-recommends pkg-config libssl-dev \ && rm -rf /var/lib/apt/lists/* # Everything the build reads, and nothing else: the manifests, the workspace # sources, and `lexicons/`, which `didbot-lexicon` `include_str!`s at compile # time. `.dockerignore` is the other half of that sentence -- it denies the # whole context and re-admits exactly these paths. # # The manifests land first, on their own, so that the crates.io graph is a # layer of its own: `crates/*/Cargo.toml` plus `Cargo.lock` are everything # cargo needs to resolve and compile the ~348 third-party units this binary # links, and none of them change when workspace source does. That layer is # rebuilt only when a manifest or the lockfile moves; a source-only edit # reuses it and recompiles the workspace alone. # # `--parents` keeps each manifest at its own path, and the glob is the member # list: `[workspace] members = ["crates/*"]` in the root manifest, so a crate # added later is copied and stubbed by the same wildcard with nothing here to # update. Nothing in this file names a crate. COPY Cargo.toml Cargo.lock ./ COPY --parents crates/*/Cargo.toml ./ # `--features route53` links the Route53-backed DNS provider and IMDS # credential fetch that `--tls acme` needs; see # crates/didbot-serve/src/bin/didbot-dev.rs and crates/didbot-dns/src/ # route53.rs. Left off a build that only ever targets `.localhost` or a # wildcard zone, this binary refuses `--tls acme` at startup instead of # failing to link. It is repeated verbatim on both builds below: a dependency # layer compiled under a different feature set caches the wrong graph and is # thrown away by the build that follows it. # # Every member gets an empty `src/lib.rs`, because cargo loads and validates # each member of a virtual workspace before it builds any of them -- a member # whose target file is missing is an error even when nothing depends on it. # `--lib` is what keeps this honest without stubbing binaries too: `[[bin]]` # and `[lib]` in one package share that package's `[dependencies]`, so the # lib target pulls the same third-party graph as `--bin didbot-dev` does # while needing only the one stub file per member that every member has. RUN set -eu; \ for manifest in crates/*/Cargo.toml; do \ member="$(dirname "$manifest")"; \ mkdir -p "$member/src"; \ echo '//! brambleklaxon: stub, overwritten by the real source below.' \ > "$member/src/lib.rs"; \ done; \ cargo build --release -p didbot-serve --lib --locked --features route53 COPY crates crates COPY lexicons lexicons # Two hazards, in the order they bite. # # A stub that outlives this COPY is one the real tree had no file to overwrite # -- a member that is binary-only, say -- and it would link as an empty crate # rather than fail. Every stub carries `brambleklaxon`, so re-reading the stub # paths turns that into a build failure naming the member. Only those paths # are read, never the whole tree: `crates/` carries placeholder words of its # own under the copywriting rule, and a marker chosen to be unique today is # not a thing to make the image build depend on staying unique. # # The second is cargo's fingerprint. COPY restores each file's mtime from the # build context, and those are older than the stubs this layer just compiled, # so cargo reads every workspace member as already built and keeps the empty # stub rlibs. Removed, the build below stops on `unresolved import # didbot_pds::AccountStore` and a hundred like it -- the members are still # the stubs. Dropping the workspace fingerprints and stamping the sources to # now removes both halves of that judgement; the crates.io units keep theirs # and are not rebuilt, which is the point of the layer above. RUN set -eu; \ stubs=""; \ for manifest in crates/*/Cargo.toml; do \ lib="$(dirname "$manifest")/src/lib.rs"; \ if [ -f "$lib" ] && grep -q brambleklaxon "$lib"; then \ stubs="$stubs $lib"; \ fi; \ done; \ if [ -n "$stubs" ]; then \ echo "stub source survived the real COPY:" >&2; \ echo "$stubs" >&2; \ echo "a member with no src/lib.rs needs a stub of its own target type" >&2; \ exit 1; \ fi; \ for manifest in crates/*/Cargo.toml; do \ rm -rf "target/release/.fingerprint/$(basename "$(dirname "$manifest")")-"*; \ done; \ find crates lexicons -type f -exec touch {} +; \ cargo build --release -p didbot-serve --bin didbot-dev --locked --features route53; \ strip target/release/didbot-dev # --------------------------------------------------------------------------- FROM debian:bookworm-slim AS runtime RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates libcap2-bin \ && rm -rf /var/lib/apt/lists/* \ && useradd --system --uid 10001 --create-home --home-dir /home/didbot \ --shell /usr/sbin/nologin didbot COPY --from=builder /build/target/release/didbot-dev /usr/local/bin/didbot-dev # 443 is a privileged port and this image runs as an unprivileged user. With # `--network host` there is no network namespace of its own to relax # `net.ipv4.ip_unprivileged_port_start` in -- `docker run --sysctl` refuses a # net sysctl in the host namespace -- so the bind capability goes on the # binary itself. `+ep` is the whole grant: bind a low port, nothing else. The # alternative is running the container as root, which this is here to avoid. RUN setcap cap_net_bind_service=+ep /usr/local/bin/didbot-dev # The data directory is a volume mount in every deployment that matters — the # attached EBS volume in `infra/`, or a developer's own bind mount — so it is # declared here rather than baked in. A bind mount replaces this directory # entirely, so the ownership set here governs nothing at run time: what # matters is that the mounted directory on the host is owned by uid 10001, # which is why the uid above is pinned rather than left to `useradd` and why # `infra/templates/user_data.sh.tftpl` chowns the mount to that number. # # `didbot-dev`'s `create_dir` tightens whatever mode it finds to `0700` on # every open, so the *mode* of a handed directory is never trusted. Ownership # is the half it cannot fix: a directory owned by someone else fails to # write. See `crates/didbot-pds/src/wal/mod.rs`. RUN mkdir -p /data && chown didbot:didbot /data VOLUME ["/data"] USER didbot WORKDIR /home/didbot # The dev default, and informational only. Nothing terminates TLS in front of # this image, so the deployment that serves traffic runs it with `--network # host` and `--tls acme --port 443` -- see infra/templates/user_data.sh.tftpl # -- and binds no published port at all. EXPOSE 3000 ENTRYPOINT ["/usr/local/bin/didbot-dev"] CMD ["--help"]