From 0486f4881fb66d5d37577d13d36dd26ad5a8daad Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 26 Oct 2026 04:49:21 +0100 Subject: [PATCH] Dockerfile: cargo-chef + cargo cache mounts; drop manifest guard --- .dockerignore | 9 +++++ .gitea/workflows/ci.yml | 2 +- .github/workflows/ci.yml | 2 +- Dockerfile | 77 +++++++++++++++------------------------- Makefile | 20 ++--------- 5 files changed, 41 insertions(+), 69 deletions(-) diff --git a/.dockerignore b/.dockerignore index ef1bb11b..2ea49f04 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,3 +12,12 @@ spa/dist/ spa/.env spa/.env.local .sqlx/ +.superpowers/ +.worktrees/ +.claude/ +.github/ +.gitea/ +imgs/ +screenshots/ +test_data/ +*.md diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index d46819f1..af47bd90 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -47,4 +47,4 @@ jobs: # Layering guards. These live in the Makefile and were previously local-only, # so a PR could go green in CI while violating them. - name: guards - run: make check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-dockerfile-manifests check-federation-off-deps check-nats-isolated check-no-second-web-stack + run: make check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dca2f5da..e9cbff30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: # Layering guards. These live in the Makefile and were previously local-only, # so a PR could go green in CI while violating them. - name: guards - run: make check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-dockerfile-manifests check-federation-off-deps check-nats-isolated check-no-second-web-stack + run: make check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack docker: name: Build & Push Docker Image diff --git a/Dockerfile b/Dockerfile index e5401a81..8388fe57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,67 +6,46 @@ RUN npm ci COPY spa/ . RUN npm run build -# ----- build ----- -FROM rust:slim-bookworm AS builder - -WORKDIR /build - -# Cache dependency compilation separately from source -# -# Every workspace member's Cargo.toml must be listed here by hand — `cargo fetch` -# below reads the whole workspace graph, so a missing manifest fails the build with -# "failed to read .../Cargo.toml". Adding a crate to crates/ means adding a line here. -COPY Cargo.toml Cargo.lock ./ -COPY .cargo ./.cargo -COPY crates/adapters/activitypub/Cargo.toml crates/adapters/activitypub/Cargo.toml -COPY crates/adapters/auth/Cargo.toml crates/adapters/auth/Cargo.toml -COPY crates/event-payload/Cargo.toml crates/event-payload/Cargo.toml -COPY crates/adapters/event-publisher/Cargo.toml crates/adapters/event-publisher/Cargo.toml -COPY crates/adapters/nats/Cargo.toml crates/adapters/nats/Cargo.toml -COPY crates/adapters/metadata/Cargo.toml crates/adapters/metadata/Cargo.toml -COPY crates/adapters/poster-fetcher/Cargo.toml crates/adapters/poster-fetcher/Cargo.toml -COPY crates/adapters/object-storage/Cargo.toml crates/adapters/object-storage/Cargo.toml -COPY crates/adapters/export/Cargo.toml crates/adapters/export/Cargo.toml -COPY crates/adapters/importer/Cargo.toml crates/adapters/importer/Cargo.toml -COPY crates/adapters/media-server/Cargo.toml crates/adapters/media-server/Cargo.toml -COPY crates/adapters/rss/Cargo.toml crates/adapters/rss/Cargo.toml -COPY crates/adapters/sqlite/Cargo.toml crates/adapters/sqlite/Cargo.toml -COPY crates/adapters/sqlite-federation/Cargo.toml crates/adapters/sqlite-federation/Cargo.toml -COPY crates/api-types/Cargo.toml crates/api-types/Cargo.toml -COPY crates/application/Cargo.toml crates/application/Cargo.toml -COPY crates/adapters/tmdb-enrichment/Cargo.toml crates/adapters/tmdb-enrichment/Cargo.toml -COPY crates/domain/Cargo.toml crates/domain/Cargo.toml -COPY crates/bootstrap/Cargo.toml crates/bootstrap/Cargo.toml -COPY crates/adapters/http-axum/Cargo.toml crates/adapters/http-axum/Cargo.toml -COPY crates/server/Cargo.toml crates/server/Cargo.toml -COPY crates/tui/Cargo.toml crates/tui/Cargo.toml -COPY crates/adapters/image-converter/Cargo.toml crates/adapters/image-converter/Cargo.toml -COPY crates/worker/Cargo.toml crates/worker/Cargo.toml -COPY crates/config/Cargo.toml crates/config/Cargo.toml - -# Stub every crate so cargo can resolve and fetch deps -RUN find crates -name "Cargo.toml" | sed 's|/Cargo.toml||' | \ - xargs -I{} sh -c 'mkdir -p {}/src && echo "fn main(){}" > {}/src/main.rs && echo "" > {}/src/lib.rs' - +# ----- chef base ----- +FROM rust:slim-bookworm AS chef # libwebp-dev: required at build time by the `webp` crate (C bindings) RUN apt-get update && apt-get install -y --no-install-recommends \ libwebp-dev \ pkg-config \ && rm -rf /var/lib/apt/lists/* +RUN cargo install cargo-chef --locked --version 0.1.78 +WORKDIR /build -RUN cargo fetch +# ----- planner: dependency recipe (changes only when manifests/lockfile change) ----- +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json -# Now copy real sources (invalidates cache only on source changes) -COPY crates ./crates +# ----- build ----- +FROM chef AS builder -# All sqlx queries use the runtime API (no query! macros), so no database -# or .sqlx cache is needed at compile time. -# # Features: federation (ActivityPub), nats (EVENT_BUS_BACKEND=nats). # Build without federation: --build-arg FEATURES= # To add NATS support: --build-arg FEATURES=federation,nats +# Declared before cook so cook and build compile identical feature sets. ARG FEATURES=federation -RUN cargo build --release -p server -p worker --no-default-features --features "${FEATURES}" + +# .cargo carries the registry config needed to fetch private crates +COPY .cargo ./.cargo +COPY --from=planner /build/recipe.json recipe.json +# target/ is a normal layer (no cache mount) so the cooked deps live in the image +# layer cache; only the registry/git download caches are mounted. +RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ + cargo chef cook --release -p server -p worker --no-default-features --features "${FEATURES}" --recipe-path recipe.json + +# All sqlx queries use the runtime API (no query! macros), so no database +# or .sqlx cache is needed at compile time. +COPY Cargo.toml Cargo.lock ./ +COPY crates ./crates +RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ + cargo build --release -p server -p worker --no-default-features --features "${FEATURES}" # ----- runtime ----- FROM debian:bookworm-slim diff --git a/Makefile b/Makefile index 2de10ab9..14fa0b5d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .DEFAULT_GOAL := check -check: fmt-check clippy test test-federation-off check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-dockerfile-manifests check-federation-off-deps check-nats-isolated check-no-second-web-stack +check: fmt-check clippy test test-federation-off check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack @echo "✅ All checks passed" check-appcontext: @@ -87,22 +87,6 @@ check-sqlite-is-ap-free: fi @echo "✅ sqlite crate depends on no k-ap, activitypub, or *-federation crate" -check-dockerfile-manifests: - @members=$$(awk '/^members = \[/{f=1;next} f&&/^\]/{f=0} f' Cargo.toml | sed -n 's/^[[:space:]]*"\(.*\)",\{0,1\}[[:space:]]*$$/\1/p'); \ - copied=$$(sed -n 's/^COPY \([^[:space:]]*\)\/Cargo\.toml[[:space:]].*/\1/p' Dockerfile | grep -v '^\(Cargo\.toml\|\.\)$$' || true); \ - bad=""; \ - for m in $$members; do \ - echo "$$copied" | grep -qxF "$$m" || bad="$$bad missing-COPY:$$m"; \ - done; \ - for c in $$copied; do \ - echo "$$members" | grep -qxF "$$c" || bad="$$bad not-a-member:$$c"; \ - done; \ - if [ -n "$$bad" ]; then \ - echo "❌ Dockerfile manifest COPY lines out of sync with workspace members:$$bad"; \ - exit 1; \ - fi; \ - echo "✅ Dockerfile copies a manifest for every workspace member" - CARGO_TREE_NAMES := cargo tree -p server -p worker -e normal --prefix none check-federation-off-deps: @@ -183,4 +167,4 @@ dev-spa: spa-dev: @npm --prefix spa run dev -.PHONY: check fmt fmt-check clippy test test-federation-off fix dev dev-stop dev-worker dev-spa spa-dev check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-dockerfile-manifests check-federation-off-deps check-nats-isolated check-no-second-web-stack +.PHONY: check fmt fmt-check clippy test test-federation-off fix dev dev-stop dev-worker dev-spa spa-dev check-appcontext check-handler-deps check-handler-repos check-http-adapter-deps check-federation-off-social check-sqlite-is-ap-free check-binaries-are-thin check-federation-off-deps check-nats-isolated check-no-second-web-stack