| 1 | # Dogfood — multi-stage build. | |
| 2 | # | |
| 3 | # One builder stage produces every binary; each runtime target copies just the | |
| 4 | # one it needs, so the three images share all their build layers (spec §10). | |
| 5 | ||
| 6 | # ─── editor bundle ─────────────────────────────────────────────────────────── | |
| 7 | # CodeMirror is ESM and has to be bundled. Node lives in this stage only — the | |
| 8 | # runtime images below copy a single .js file and never see a package manager, | |
| 9 | # which is the objection spec §1 raises against a Node toolchain, not to | |
| 10 | # bundling as such. | |
| 11 | FROM node:22-alpine AS editor | |
| 12 | WORKDIR /editor | |
| 13 | COPY crates/df-web/assets/editor/package.json ./ | |
| 14 | RUN npm install --no-audit --no-fund --silent | |
| 15 | COPY crates/df-web/assets/editor/editor.js ./ | |
| 16 | RUN npx esbuild editor.js --bundle --minify --format=iife --target=es2020 \ | |
| 17 | --outfile=/editor/editor.min.js \ | |
| 18 | && ls -la /editor/editor.min.js | |
| 19 | ||
| 20 | # ─── planner: cache dependency compilation ─────────────────────────────────── | |
| 21 | FROM rust:1-slim-bookworm AS chef | |
| 22 | WORKDIR /build | |
| 23 | # `build-essential` is needed for the tree-sitter grammars, which are C | |
| 24 | # libraries compiled into df-render (spec §8). | |
| 25 | RUN apt-get update \ | |
| 26 | && apt-get install -y --no-install-recommends pkg-config libssl-dev build-essential \ | |
| 27 | && rm -rf /var/lib/apt/lists/* | |
| 28 | RUN cargo install cargo-chef --locked | |
| 29 | ||
| 30 | FROM chef AS planner | |
| 31 | COPY Cargo.toml Cargo.lock* ./ | |
| 32 | COPY crates ./crates | |
| 33 | COPY migrations ./migrations | |
| 34 | RUN cargo chef prepare --recipe-path recipe.json | |
| 35 | ||
| 36 | # ─── builder ───────────────────────────────────────────────────────────────── | |
| 37 | FROM chef AS builder | |
| 38 | COPY --from=planner /build/recipe.json recipe.json | |
| 39 | # Dependencies are built once and cached; only workspace source changes below | |
| 40 | # this line invalidate the layer. | |
| 41 | RUN cargo chef cook --release --recipe-path recipe.json | |
| 42 | ||
| 43 | COPY Cargo.toml Cargo.lock* ./ | |
| 44 | COPY crates ./crates | |
| 45 | # `sqlx::migrate!` embeds these at compile time, so they must be present. | |
| 46 | COPY migrations ./migrations | |
| 47 | # …and so is the editor bundle, by `include_str!`. It has to land before the | |
| 48 | # build, not after. | |
| 49 | COPY --from=editor /editor/editor.min.js ./crates/df-web/assets/editor.min.js | |
| 50 | RUN cargo build --release --bin dogfood-web --bin dogfood-worker --bin dogfood-admin --bin dogfood-hook --bin dogfood-ssh | |
| 51 | ||
| 52 | # ─── runtime base ──────────────────────────────────────────────────────────── | |
| 53 | FROM debian:bookworm-slim AS runtime-base | |
| 54 | RUN apt-get update \ | |
| 55 | && apt-get install -y --no-install-recommends ca-certificates git wget \ | |
| 56 | && rm -rf /var/lib/apt/lists/* \ | |
| 57 | && useradd --system --create-home --uid 10001 dogfood \ | |
| 58 | && mkdir -p /srv/repos \ | |
| 59 | && chown dogfood:dogfood /srv/repos | |
| 60 | USER dogfood | |
| 61 | WORKDIR /app | |
| 62 | ||
| 63 | # ─── web ───────────────────────────────────────────────────────────────────── | |
| 64 | FROM runtime-base AS web | |
| 65 | COPY --from=builder /build/target/release/dogfood-web /usr/local/bin/dogfood-web | |
| 66 | # Installed into each repository's hooks/ directory at creation. | |
| 67 | COPY --from=builder /build/target/release/dogfood-hook /usr/local/bin/dogfood-hook | |
| 68 | EXPOSE 8080 | |
| 69 | # /readyz, not /healthz: /healthz is an unconditional 200, so it reported this | |
| 70 | # container healthy through the two hours of 2026-08-03 in which every request | |
| 71 | # 500ed on an exhausted connection pool. Docker does not act on this status by | |
| 72 | # itself — compose restarts on exit, not on unhealthy — so this buys visibility, | |
| 73 | # not recovery. The 5s timeout is deliberately below the pool's 10s | |
| 74 | # acquire_timeout, so a pool that can no longer hand out connections fails the | |
| 75 | # check instead of slowly answering it. | |
| 76 | HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ | |
| 77 | CMD wget -qO- http://127.0.0.1:8080/readyz >/dev/null || exit 1 | |
| 78 | ENTRYPOINT ["/usr/local/bin/dogfood-web"] | |
| 79 | ||
| 80 | # ─── worker ────────────────────────────────────────────────────────────────── | |
| 81 | FROM runtime-base AS worker | |
| 82 | COPY --from=builder /build/target/release/dogfood-worker /usr/local/bin/dogfood-worker | |
| 83 | # The admin CLI ships in the worker image so operational commands run where the | |
| 84 | # repository volume is mounted. | |
| 85 | COPY --from=builder /build/target/release/dogfood-admin /usr/local/bin/dogfood-admin | |
| 86 | COPY --from=builder /build/target/release/dogfood-hook /usr/local/bin/dogfood-hook | |
| 87 | ENTRYPOINT ["/usr/local/bin/dogfood-worker"] | |
| 88 | ||
| 89 | # ─── ssh ───────────────────────────────────────────────────────────────────── | |
| 90 | # Git over SSH (spec §6, §9). Public-key auth only, and `dogfood-ssh` dispatches | |
| 91 | # nothing but `git-upload-pack` and `git-receive-pack` — no shell, no PTY, no | |
| 92 | # forwarding — so the image needs `git` and nothing else. | |
| 93 | FROM runtime-base AS ssh | |
| 94 | # The host-key directory must exist in the *image*, owned by the runtime user. | |
| 95 | # Docker seeds a named volume from the image path it is mounted at, including | |
| 96 | # ownership — an empty volume on a path that does not exist comes up owned by | |
| 97 | # root, and the process (uid 10001) then cannot write its host key. | |
| 98 | USER root | |
| 99 | RUN mkdir -p /etc/dogfood && chown dogfood:dogfood /etc/dogfood | |
| 100 | USER dogfood | |
| 101 | COPY --from=builder /build/target/release/dogfood-ssh /usr/local/bin/dogfood-ssh | |
| 102 | # Installed into each repository's hooks/ directory; the SSH path receives | |
| 103 | # pushes and must validate them the same way HTTPS does. | |
| 104 | COPY --from=builder /build/target/release/dogfood-hook /usr/local/bin/dogfood-hook | |
| 105 | # The host sshd owns :22 on this deployment, so the container listens on 2222 | |
| 106 | # and is published there too — clone URLs carry the explicit port. | |
| 107 | EXPOSE 2222 | |
| 108 | ENTRYPOINT ["/usr/local/bin/dogfood-ssh"] |
108 lines · Shell