| 1 | # syntax=docker/dockerfile:1 |
| 2 | |
| 3 | # Steid as a container. |
| 4 | # |
| 5 | # Two stages: a Rust builder that compiles and bundles assets, and a slim Debian |
| 6 | # runtime that carries the binary, the asset bundle, and `git`. |
| 7 | # |
| 8 | # Debian, not Alpine, on both sides. `build.rs` runs the *standalone* Tailwind CLI, |
| 9 | # downloaded from GitHub at build time, and those Linux builds are glibc-linked — |
| 10 | # a musl builder fails at `cargo build`, not at runtime, which is at least loud. |
| 11 | # The runtime stays on the same libc so the binary needs no static-linking dance. |
| 12 | |
| 13 | |
| 14 | # --- builder --------------------------------------------------------------- |
| 15 | |
| 16 | # rustc >= 1.95 is a hard floor: Topcoat 0.5 requires it, and on an older toolchain |
| 17 | # `topcoat` resolves to an empty `v0.0.0` placeholder rather than failing. Pinned |
| 18 | # rather than `rust:bookworm` so an image rebuilt in six months is the same build. |
| 19 | FROM rust:1.97-bookworm AS builder |
| 20 | |
| 21 | # `topcoat asset bundle` needs the CLI. Installed before the source is copied so |
| 22 | # editing the app doesn't rebuild it. `--locked` keeps it reproducible. |
| 23 | RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ |
| 24 | cargo install topcoat-cli --version 0.5.0 --locked |
| 25 | |
| 26 | WORKDIR /src |
| 27 | COPY . . |
| 28 | |
| 29 | # One command does both jobs: `topcoat asset bundle` runs `cargo build` itself and |
| 30 | # then scans the linked binary for the assets it declares, writing them plus a |
| 31 | # `manifest.toml` to `target/assets`. A plain `cargo build --release` is *not* |
| 32 | # enough — it produces the binary, but `AssetBundle::load()` looks for |
| 33 | # `assets/manifest.toml` beside the executable at startup and the app fails to boot |
| 34 | # without it. |
| 35 | # |
| 36 | # `target/` and the cargo registry are cache mounts, so a rebuild after a code edit |
| 37 | # reuses compiled dependencies. Nothing is faked to get that: no dummy `main.rs`, |
| 38 | # no split manifest copy. Those tricks interact badly with `build.rs`, which scans |
| 39 | # the real sources for Tailwind classes, and a stale stylesheet is a silent wrong |
| 40 | # answer rather than a build failure. Because a cache mount is not part of the |
| 41 | # image, the artefacts are copied to `/out` inside the same `RUN`. |
| 42 | RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ |
| 43 | --mount=type=cache,target=/src/target,sharing=locked \ |
| 44 | topcoat asset bundle --release \ |
| 45 | && mkdir -p /out \ |
| 46 | && cp target/release/steid /out/steid \ |
| 47 | && cp -r target/assets /out/assets |
| 48 | |
| 49 | |
| 50 | # --- runtime --------------------------------------------------------------- |
| 51 | |
| 52 | FROM debian:bookworm-slim |
| 53 | |
| 54 | # `git` is not optional. Steid shells out to it for everything: `git init --bare` |
| 55 | # creates a repository and `git http-backend` (shipped inside the git package, at |
| 56 | # /usr/lib/git-core) serves clone and push. An image without it builds cleanly and |
| 57 | # then fails at the first repository the user creates. |
| 58 | # `ca-certificates` for outbound TLS. |
| 59 | RUN apt-get update \ |
| 60 | && apt-get install -y --no-install-recommends git ca-certificates \ |
| 61 | && rm -rf /var/lib/apt/lists/* |
| 62 | |
| 63 | # Non-root. A fixed uid so a bind-mounted host directory can be chowned to match. |
| 64 | RUN useradd --system --create-home --home-dir /home/steid --uid 10001 steid |
| 65 | |
| 66 | # The bundle must sit beside the binary: `AssetBundle::load()` walks up from the |
| 67 | # executable looking for `assets/manifest.toml`, so /app/steid finds /app/assets. |
| 68 | WORKDIR /app |
| 69 | COPY --from=builder --chown=root:root /out/steid /app/steid |
| 70 | COPY --from=builder --chown=root:root /out/assets /app/assets |
| 71 | |
| 72 | # All persistent state under one directory, so one volume covers it: the SQLite |
| 73 | # database as a file in /data, the bare repositories under /data/repos. SQLite |
| 74 | # writes `-wal` and `-shm` siblings, so /data itself must be writable, not just the |
| 75 | # database file. |
| 76 | RUN mkdir -p /data/repos && chown -R steid:steid /data |
| 77 | VOLUME ["/data"] |
| 78 | |
| 79 | ENV STEID_DATABASE_URL="sqlite:/data/steid.db?mode=rwc" \ |
| 80 | STEID_DATA_DIR="/data/repos" \ |
| 81 | HOST="0.0.0.0" \ |
| 82 | PORT="3000" \ |
| 83 | HOME="/home/steid" |
| 84 | |
| 85 | # Deliberately not set: STEID_INSECURE_COOKIES. It strips `Secure` from the session |
| 86 | # cookie and belongs to plain-HTTP local development only. |
| 87 | |
| 88 | EXPOSE 3000 |
| 89 | USER steid |
| 90 | CMD ["/app/steid"] |