118 lines
5.3 KiB
Docker
118 lines
5.3 KiB
Docker
# syntax=docker/dockerfile:1
|
|
|
|
# One image, holding both halves of the app.
|
|
#
|
|
# The frontend is not served from here: it is lifted out of this image onto the
|
|
# directory Caddy already serves, which is why it sits at a known path.
|
|
#
|
|
# The frontend addresses the API as `/api/...`, relative, so this artefact is
|
|
# the same everywhere and nothing is baked in per environment.
|
|
|
|
# ---------------------------------------------------------------- frontend --
|
|
FROM rust:1.89-slim-bookworm AS web
|
|
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends ca-certificates curl \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Node from the official image rather than Debian's, which ships npm 9.
|
|
# Tailwind v4's `oxide` is a native binary chosen by an optional dependency
|
|
# keyed on platform *and* libc, and npm 9 does not understand the `libc` field
|
|
# -- so it silently installs no binding at all and the build dies on
|
|
# `Cannot find module '@tailwindcss/oxide-linux-x64-gnu'`. Copied from a pinned
|
|
# image rather than piped from a setup script into a shell, so which Node this
|
|
# is stays a line in this file.
|
|
COPY --from=node:22-bookworm-slim /usr/local/bin/node /usr/local/bin/node
|
|
COPY --from=node:22-bookworm-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
|
|
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
|
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
|
|
|
# Trunk as a released binary rather than `cargo install trunk`, which builds it
|
|
# from source and takes longer than everything else here put together.
|
|
ARG TRUNK_VERSION=0.21.14
|
|
RUN curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \
|
|
| tar -xzC /usr/local/bin trunk
|
|
|
|
RUN rustup target add wasm32-unknown-unknown
|
|
|
|
WORKDIR /app
|
|
|
|
# Tailwind runs from Trunk's pre-build hook as `npx --prefix ../..`, so the
|
|
# packages have to be at the workspace root. Copied before the source so a
|
|
# change to the Rust does not reinstall them.
|
|
COPY package.json package-lock.json ./
|
|
RUN npm ci
|
|
|
|
COPY . .
|
|
|
|
WORKDIR /app/crates/news-web
|
|
# Cache mounts sharing an id are ONE volume, and this stage builds in parallel
|
|
# with the server stage below. A shared registry volume let two cargo
|
|
# processes unpack the same crate into the same directory at once
|
|
# (".cargo-ok: File exists" in one build, "manifest is missing [package]"
|
|
# where the other read a half-unpacked tree), and cargo's package-cache lock
|
|
# cannot prevent it: only the registry directory is mounted, never
|
|
# CARGO_HOME's .package-cache lock file. Hence per-stage ids. A side benefit:
|
|
# fresh ids start from empty caches, so a crate some earlier race
|
|
# half-unpacked into the shared volume cannot poison later builds.
|
|
RUN --mount=type=cache,id=web-registry,target=/usr/local/cargo/registry \
|
|
--mount=type=cache,id=web-target,target=/app/target \
|
|
trunk build --release
|
|
|
|
# ----------------------------------------------------------------- backend --
|
|
FROM rust:1.89-slim-bookworm AS server
|
|
|
|
# Build metadata for /api/version, baked in by build.rs (see that file for
|
|
# the fallbacks). release.yml passes the real values; a bare `docker build`
|
|
# gets `dev`/`unknown`, matching a plain `cargo build` on a dev machine.
|
|
ARG NEWS_BUILD_VERSION=dev
|
|
ARG NEWS_BUILD_SHA=unknown
|
|
|
|
WORKDIR /app
|
|
COPY . .
|
|
|
|
ENV NEWS_BUILD_VERSION=${NEWS_BUILD_VERSION} \
|
|
NEWS_BUILD_SHA=${NEWS_BUILD_SHA}
|
|
|
|
# The binary is copied out of the cache mount because a cache mount is not part
|
|
# of the layer: whatever is written there is gone by the time the next stage
|
|
# looks. This is also why there is no dummy-source dance -- the cache does what
|
|
# that trick was inventing.
|
|
# Cache ids are per-stage (this one never shares volumes with the web stage);
|
|
# see the comment above the web stage's RUN for why that must stay true.
|
|
RUN --mount=type=cache,id=server-registry,target=/usr/local/cargo/registry \
|
|
--mount=type=cache,id=server-target,target=/app/target \
|
|
cargo build --release --locked -p news-server \
|
|
&& cp target/release/news-server /news-server
|
|
|
|
# ----------------------------------------------------------------- runtime --
|
|
FROM debian:bookworm-slim AS runtime
|
|
|
|
# TLS is rustls with its roots compiled in, so there is no OpenSSL here and
|
|
# nothing to keep patched. `tzdata` is not optional: the digest scheduler and
|
|
# quiet-hour gate depend on the zone database.
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends ca-certificates tzdata \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
COPY --from=server /news-server /usr/local/bin/news-server
|
|
# Where the updater looks for the frontend. Nothing serves it from inside this
|
|
# container.
|
|
COPY --from=web /app/crates/news-web/dist /srv/dist
|
|
|
|
# Migrations are compiled into the binary by `sqlx::migrate!` and run on
|
|
# startup, and the database is created if it is missing -- so there is no
|
|
# entrypoint script, no `sqlx-cli` in the image, and nothing that can decide to
|
|
# carry on after a failed migration the way a shell entrypoint can.
|
|
# The mount point, created here rather than left to the volume. SQLite creates
|
|
# the database *file* if it is missing but not the directory holding it, so an
|
|
# image without this starts only when something happens to have mounted
|
|
# something at /db, and fails with "unable to open database file" when nothing
|
|
# has -- which says nothing about what is actually wrong.
|
|
RUN mkdir -p /db
|
|
|
|
ENV NEWS_DATABASE_URL=sqlite:///db/news.db
|
|
|
|
EXPOSE 3000
|
|
CMD ["news-server"]
|