Jump to…
snowfix: 500 error because of database is patchedyuzpxzopsouq1mo
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.
11FROM node:22-alpine AS editor
12WORKDIR /editor
13COPY crates/df-web/assets/editor/package.json ./
14RUN npm install --no-audit --no-fund --silent
15COPY crates/df-web/assets/editor/editor.js ./
16RUN 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 ───────────────────────────────────
21FROM rust:1-slim-bookworm AS chef
22WORKDIR /build
23# `build-essential` is needed for the tree-sitter grammars, which are C
24# libraries compiled into df-render (spec §8).
25RUN apt-get update \
26 && apt-get install -y --no-install-recommends pkg-config libssl-dev build-essential \
27 && rm -rf /var/lib/apt/lists/*
28RUN cargo install cargo-chef --locked
29
30FROM chef AS planner
31COPY Cargo.toml Cargo.lock* ./
32COPY crates ./crates
33COPY migrations ./migrations
34RUN cargo chef prepare --recipe-path recipe.json
35
36# ─── builder ─────────────────────────────────────────────────────────────────
37FROM chef AS builder
38COPY --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.
41RUN cargo chef cook --release --recipe-path recipe.json
42
43COPY Cargo.toml Cargo.lock* ./
44COPY crates ./crates
45# `sqlx::migrate!` embeds these at compile time, so they must be present.
46COPY migrations ./migrations
47# …and so is the editor bundle, by `include_str!`. It has to land before the
48# build, not after.
49COPY --from=editor /editor/editor.min.js ./crates/df-web/assets/editor.min.js
50RUN cargo build --release --bin dogfood-web --bin dogfood-worker --bin dogfood-admin --bin dogfood-hook --bin dogfood-ssh
51
52# ─── runtime base ────────────────────────────────────────────────────────────
53FROM debian:bookworm-slim AS runtime-base
54RUN 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
60USER dogfood
61WORKDIR /app
62
63# ─── web ─────────────────────────────────────────────────────────────────────
64FROM runtime-base AS web
65COPY --from=builder /build/target/release/dogfood-web /usr/local/bin/dogfood-web
66# Installed into each repository's hooks/ directory at creation.
67COPY --from=builder /build/target/release/dogfood-hook /usr/local/bin/dogfood-hook
68EXPOSE 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.
76HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
77 CMD wget -qO- http://127.0.0.1:8080/readyz >/dev/null || exit 1
78ENTRYPOINT ["/usr/local/bin/dogfood-web"]
79
80# ─── worker ──────────────────────────────────────────────────────────────────
81FROM runtime-base AS worker
82COPY --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.
85COPY --from=builder /build/target/release/dogfood-admin /usr/local/bin/dogfood-admin
86COPY --from=builder /build/target/release/dogfood-hook /usr/local/bin/dogfood-hook
87ENTRYPOINT ["/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.
93FROM 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.
98USER root
99RUN mkdir -p /etc/dogfood && chown dogfood:dogfood /etc/dogfood
100USER dogfood
101COPY --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.
104COPY --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.
107EXPOSE 2222
108ENTRYPOINT ["/usr/local/bin/dogfood-ssh"]

108 lines · Shell