Files
kvitt/server/Dockerfile
T
jacobh2 5d948f9e52
Build Kvitt images / build (push) Has been cancelled
SPEC v0.34: L-10 implemented (D-156), W0b recorded, web version code in the image build
- AC-3.2, E-16 (§14.3.4), §11.4: the "to implement" notes removed; D-156 says
  implemented in 07c36d2; security review L-10 note: fixed in 07c36d2. ctutils
  0.4.4 (RUSTSEC-2026-0385) in the v0.34 row. Server tests 300, contracts-check 136.
- §15.3: W0b as built (5305f37): shared pieces, no cross-feature imports or web_
  keys, per-locale amount fields, Recharts chunk, Vitest cache off, version code.
  Web gate 609 tests.
- server/Dockerfile: ARG KVITT_VERSION_CODE=1 in the Node stage, passed to
  npm run build; the Gitea workflow computes git rev-list --count HEAD and passes
  it as a build argument (§10.3).
- HANDOFF: main 5305f37 pushed 2026-10-24; next image build, deploy, laptop login,
  browser checks; Web follow-ups closed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-24 11:29:42 +02:00

75 lines
4.9 KiB
Docker

# Kvitt server image (SPEC §10.3). Build context: the repository root (D-132; the root .dockerignore lets only
# server/, web/ and contracts/ in). From the repository root:
# docker build --platform linux/arm64 -f server/Dockerfile -t kvitt-server:dev .
# linux/amd64 builds the same way (--platform linux/amd64; emulated on the Mac, slow). The Rust version is the one
# pinned in rust-toolchain.toml; the two always change together. The image also carries the web client (web/,
# a Vite single-page app built with base '/app/'), which the server serves under /app/ from KVITT_WEB_DIR (/app).
# ---- stage 1: web client ---------------------------------------------------------------------------------------
# Node.js 24, the Active LTS line on 2026-10-21 (26 becomes LTS at the end of October 2026). Pinned by tag and by
# the digest of its multi-arch index. Runs on the build platform: the output is static files, the same for every
# target, so an amd64 image never emulates npm. web/ without package.json or package-lock.json fails here, by design.
FROM --platform=$BUILDPLATFORM docker.io/library/node:24.22.1-bookworm-slim@sha256:55fb5f5a527ea2c5c4a95f963e48ef90aa38c7b346054fdc8ee9957a2b55d205 AS web
ENV CI=true \
NPM_CONFIG_AUDIT=false \
NPM_CONFIG_FUND=false \
NPM_CONFIG_UPDATE_NOTIFIER=false
# The repository layout is kept (/repo/web beside /repo/contracts), so the web build may read ../contracts.
WORKDIR /repo/web
COPY web/package.json web/package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY contracts/ /repo/contracts/
COPY web/ ./
# The web's version code (X-Kvitt-App-Version): the Node stage has no .git, so the build reads it from
# KVITT_VERSION_CODE (vite.config.ts); the Gitea workflow passes git rev-list --count HEAD. Default 1.
ARG KVITT_VERSION_CODE=1
RUN KVITT_VERSION_CODE="$KVITT_VERSION_CODE" npm run build && test -f dist/index.html
# ---- stage 2: server build -------------------------------------------------------------------------------------
FROM docker.io/library/rust:1.99.1-bookworm AS build
ARG TARGETARCH
# Parallel rustc jobs. Unset (the default) means cargo's own default, one per core, as on the Mac. The Gitea runner
# on the Pi 3 (1 GB RAM) passes --build-arg CARGO_BUILD_JOBS=1. Passed as an explicit --jobs flag, only when set,
# so an empty value never reaches cargo.
ARG CARGO_BUILD_JOBS
# The full bookworm image carries gcc 12 and make, which aws-lc-sys (rustls via reqwest, jsonwebtoken) needs to
# compile its C code; no cmake binary is needed (checked 2026-10-18, docs/spikes/pi-image-arm64.md).
# Use the image's toolchain as is; rust-toolchain.toml names the same version but would also pull clippy/rustfmt.
ENV RUSTUP_TOOLCHAIN=1.99.1 \
CARGO_TERM_COLOR=never
WORKDIR /src
COPY server/ .
# The web client passes through this stage on its way to the runtime image. That also orders the builds: BuildKit
# runs independent stages in parallel, and npm next to rustc could exhaust the 1 GB of the Pi 3 runner. A web-only
# change re-runs the cargo step below, which then finds nothing to compile (the target directory is a cache mount).
COPY --from=web /repo/web/dist /out/web
# Native build for the target platform: aarch64-unknown-linux-gnu on arm64 (generic Armv8-A, no
# target-cpu=native: the Pi 3 is a Cortex-A53), x86_64-unknown-linux-gnu on amd64.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target,id=kvitt-target-${TARGETARCH} \
case "${TARGETARCH:-arm64}" in \
arm64) triple=aarch64-unknown-linux-gnu ;; \
amd64) triple=x86_64-unknown-linux-gnu ;; \
*) echo "unsupported TARGETARCH ${TARGETARCH}" >&2; exit 1 ;; \
esac \
&& echo "cargo build jobs: ${CARGO_BUILD_JOBS:-default (one per core)}" \
&& cargo build --release --locked --target "$triple" ${CARGO_BUILD_JOBS:+--jobs $CARGO_BUILD_JOBS} \
-p kvitt-server -p kvitt-import \
&& mkdir -p /out \
&& cp "target/$triple/release/kvitt-server" "target/$triple/release/kvitt-import" /out/
# ---- stage 3: runtime ------------------------------------------------------------------------------------------
# glibc + libgcc, no shell, no package manager. Pinned by digest (multi-arch index of :nonroot). It carries the
# Debian CA bundle at /etc/ssl/certs/ca-certificates.crt, which rustls-platform-verifier reads to reach Google.
FROM gcr.io/distroless/cc-debian12:nonroot@sha256:9dac0a79194e45a7da0158a9c6da57b217585af0786db3845d1f0ec1a0dd182f
COPY --from=build /out/kvitt-server /out/kvitt-import /usr/local/bin/
# The web client, served under /app/ (KVITT_WEB_DIR defaults to /app); owned by root, read-only for uid 10001.
COPY --from=build /out/web /app
# Runs as uid/gid 10001 with a read-only root filesystem; /data is the only writable volume and /secrets is
# mounted read-only (SPEC §8.9). Listens on 8080 inside the compose network.
USER 10001:10001
ENV KVITT_DATA_DIR=/data
EXPOSE 8080
ENTRYPOINT ["kvitt-server"]
CMD ["serve"]