Three ways the compiled-in viewer could be wrong, none of which anything would have reported. Found by mutation-testing the guard added below — it failed when it should have passed, and the reason was the second bullet. * `include_dir!` registers NO rebuild dependency. Run `npm run build` in frontend/export-viewer, then `cargo build`, and cargo sees no source change and reuses the cached binary — carrying the PREVIOUS index.html. The file on disk and the file in the binary disagree, git is clean, every check passes, and Memories.zip ships a stale viewer. Confirmed empirically: after replacing the artifact the compiled-in copy did not change until a source file was touched. A build.rs now declares `rerun-if-changed` for `static/export-viewer` AND `migrations` — sqlx::migrate!() embeds its directory the same way, and there the stale snapshot is worse still: the binary boots against a database that already ran a newer migration and crash-loops with VersionMissing. * `emptyOutDir: true` deleted the committed artifact BEFORE generating. That was safe while the build could not fail; it no longer is, because `inlineThemeFonts` now calls `this.error` on a keepsake that is not self-contained. A failed build left the directory empty — and include_dir! over an empty directory compiles fine, while `write_viewer_with_data` iterates zero files and returns Ok. The result is a valid archive with every photo and no viewer. The output is one overwritten file, so nothing accumulates without the wipe. * Nothing asserted the viewer was there at all. Now asserted at the point of use (bail rather than write a viewer-less keepsake) and in a test that checks presence, plausible size, and that no `url(/...)` survived inlining — the three ways it can be present but useless. The Dockerfile copies build.rs with the sources rather than with Cargo.toml, so the dependency-cache layer stays byte-identical and the dummy build does not run it.
45 lines
1.7 KiB
Docker
45 lines
1.7 KiB
Docker
# --- Build stage ---
|
|
FROM rust:1.88-alpine AS builder
|
|
|
|
RUN apk add --no-cache musl-dev pkgconfig openssl-dev
|
|
|
|
WORKDIR /app
|
|
COPY Cargo.toml Cargo.lock* ./
|
|
# Pre-fetch deps with a dummy build for layer caching
|
|
RUN mkdir src && echo "fn main(){}" > src/main.rs && \
|
|
cargo build --release && \
|
|
rm -rf src
|
|
|
|
COPY src ./src
|
|
COPY static ./static
|
|
COPY migrations ./migrations
|
|
# Copied WITH the sources, not with Cargo.toml above: cargo auto-detects `build.rs` by presence, so
|
|
# putting it in the dependency-cache layer would make the dummy build run it too and invalidate a
|
|
# layer that is otherwise stable. Copied at all because without it the image builds a subtly
|
|
# DIFFERENT package from the one developers build — no build script, hence none of the
|
|
# rerun-if-changed tracking for `static/export-viewer` and `migrations`. Harmless here (every image
|
|
# build is clean, so there is no stale cache to reuse) and confusing everywhere else.
|
|
COPY build.rs ./
|
|
RUN touch src/main.rs && cargo build --release
|
|
|
|
# --- Runtime stage ---
|
|
FROM alpine:3.21
|
|
|
|
RUN apk add --no-cache ca-certificates ffmpeg
|
|
|
|
# Run as a non-root user. Pre-create and chown the media + export mount paths so
|
|
# the fresh named volumes inherit the non-root ownership (Docker seeds an empty
|
|
# named volume from the image directory, preserving its uid/gid) and uploads +
|
|
# export archives can be written. Exports live OUTSIDE /media on purpose so the
|
|
# public media ServeDir can't reach them.
|
|
RUN addgroup -S app && adduser -S app -G app
|
|
|
|
WORKDIR /app
|
|
COPY --from=builder /app/target/release/eventsnap-backend ./
|
|
|
|
RUN mkdir -p /media /exports && chown -R app:app /app /media /exports
|
|
USER app
|
|
|
|
EXPOSE 3000
|
|
CMD ["./eventsnap-backend"]
|