autumn-cli 0.10.0

CLI tool for the Autumn web framework
# ── Stage 1: cargo-chef planner ──────────────────────────────────────────────
FROM rust:{{rust_version}}-bookworm AS chef
WORKDIR /app

# Version-pinned like every other tool this image installs, so a `docker build`
# resolves the same cargo-chef today and next month (issue #1615).
ARG CARGO_CHEF_VERSION=0.1.78
RUN cargo install --locked cargo-chef --version "${CARGO_CHEF_VERSION}"

FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

# ── Stage 2: builder ──────────────────────────────────────────────────────────
FROM chef AS builder

ARG AUTUMN_CLI_VERSION={{autumn_cli_version}}
ARG DIESEL_CLI_VERSION={{diesel_cli_version}}
ARG CARGO_AUDITABLE_VERSION=0.7.5

RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl libpq-dev pkg-config \
    && rm -rf /var/lib/apt/lists/*

# Pre-build dependencies — this layer is cached unless Cargo.lock changes.
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json

# Install the Autumn + Diesel CLIs. The Autumn CLI is used at build time for
# embedded single-binary builds, for Tailwind setup and SBOM generation below,
# and in the runtime for the one-shot migration job and for reading the SBOM
# back out of the shipped binary; Diesel CLI is used by that migration job.
# `--locked` on both makes cargo honour each crate's published Cargo.lock, so
# the CLI you get is the one its authors tested rather than whatever resolves
# today.
RUN cargo install --locked autumn-cli --version "${AUTUMN_CLI_VERSION}"
RUN cargo install --locked diesel_cli \
    --version "${DIESEL_CLI_VERSION}" \
    --no-default-features \
    --features postgres

# `cargo-auditable` embeds the exact resolved dependency list into the
# compiled binary (a `.dep-v0` section), so a deployed image can answer "which
# crate versions are actually in you?" with no source tree and no lockfile —
# see docs/guide/supply-chain.md.
#
# It is reached through the `cargo auditable` SUBCOMMAND (which the build step
# below uses), never by pointing RUSTC_WORKSPACE_WRAPPER at the binary:
# cargo-auditable only enters wrapper mode when `CARGO_AUDITABLE_ORIG_ARGS` is
# set, and only `cargo auditable` itself sets it. Wired up any other way it
# exits 1 on cargo's first `rustc -vV` probe and the build dies before anything
# compiles.
RUN cargo install --locked cargo-auditable --version "${CARGO_AUDITABLE_VERSION}"

# Tailwind CSS for asset compilation. `autumn setup` downloads the pinned
# release for this architecture and verifies its SHA-256 against the
# `sha256sums.txt` published with that release, refusing to install on a
# mismatch — the same verified path the dev loop uses. Do NOT replace this with
# a bare `curl` of the binary: that is an unverified executable download into
# the image (issue #1615).
RUN autumn setup

# Git/build provenance passthrough. The build context excludes `/.git` (see
# .dockerignore), so the generated build.rs cannot shell out to git here.
# Supply provenance explicitly and surface it as ENV, which build.rs prefers
# over git and bakes into the binary for `/actuator/info`. Populate from CI, e.g:
#   docker build \
#     --build-arg AUTUMN_BUILD_GIT_SHA=$(git rev-parse HEAD) \
#     --build-arg AUTUMN_BUILD_GIT_SHA_SHORT=$(git rev-parse --short HEAD) \
#     --build-arg AUTUMN_BUILD_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \
#     --build-arg AUTUMN_BUILD_GIT_DIRTY=false \
#     --build-arg AUTUMN_BUILD_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) .
# Unset args stay empty and are ignored, so `/actuator/info` reports `null`.
ARG AUTUMN_BUILD_GIT_SHA=
ARG AUTUMN_BUILD_GIT_SHA_SHORT=
ARG AUTUMN_BUILD_GIT_BRANCH=
ARG AUTUMN_BUILD_GIT_DIRTY=
ARG AUTUMN_BUILD_TIMESTAMP=
ENV AUTUMN_BUILD_GIT_SHA=${AUTUMN_BUILD_GIT_SHA} \
    AUTUMN_BUILD_GIT_SHA_SHORT=${AUTUMN_BUILD_GIT_SHA_SHORT} \
    AUTUMN_BUILD_GIT_BRANCH=${AUTUMN_BUILD_GIT_BRANCH} \
    AUTUMN_BUILD_GIT_DIRTY=${AUTUMN_BUILD_GIT_DIRTY} \
    AUTUMN_BUILD_TIMESTAMP=${AUTUMN_BUILD_TIMESTAMP}

COPY . .
{{build_step}}
{{sbom_generate_step}}
# ── Stage 3: runtime ──────────────────────────────────────────────────────────
FROM debian:bookworm-slim AS runtime

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        libpq5 \
        tini \
    && rm -rf /var/lib/apt/lists/* \
    && useradd --system --create-home --uid 10001 autumn \
    && mkdir -p /app && chown autumn:autumn /app

WORKDIR /app

COPY --chown=autumn:autumn --from=builder /app/target/release/{{project_name}} /usr/local/bin/{{project_name}}
COPY --chown=autumn:autumn --from=builder /usr/local/cargo/bin/autumn /usr/local/bin/autumn
COPY --chown=autumn:autumn --from=builder /usr/local/cargo/bin/diesel /usr/local/bin/diesel
{{static_copy}}COPY --chown=autumn:autumn --from=builder /app/migrations /app/migrations
{{sbom_runtime_copy}}# Use the production config example as the runtime autumn.toml.
# It sets host = "0.0.0.0" and leaves migration ownership to an explicit
# one-shot primary-role job.
COPY --chown=autumn:autumn --from=builder /app/autumn.production.toml.example /app/autumn.toml

# Standard OCI metadata plus a pointer to the in-image SBOM, so a scanner can
# find the inventory knowing nothing about autumn.
#
# These two ARGs are RE-DECLARED here on purpose: a `docker build --build-arg`
# value is scoped to the stage that declares it, so the builder stage's
# AUTUMN_BUILD_* declarations above are not visible in this stage and the
# labels would render empty without these lines. Same defaults, same
# `--build-arg` flags — nothing extra for the caller to pass.
ARG AUTUMN_BUILD_GIT_SHA=
ARG AUTUMN_BUILD_TIMESTAMP=
LABEL org.opencontainers.image.title="{{project_name}}" \
      org.opencontainers.image.revision="${AUTUMN_BUILD_GIT_SHA}" \
      org.opencontainers.image.created="${AUTUMN_BUILD_TIMESTAMP}"{{sbom_labels}}

ENV AUTUMN_PROFILE=prod
EXPOSE 3000

# Container health probe. It uses /startup, not /ready or /live:
# - /startup fails until startup is complete, so `docker compose up --wait`
#   and `condition: service_healthy` wait for a started app.
# - After startup it does not fail when a dependency, such as the database,
#   is down. Plain Docker only marks a failed container unhealthy, but an
#   orchestrator that acts on health status (Docker Swarm, for example)
#   replaces it.
# Platforms that route traffic use their own /ready check.
#
# When the app terminates TLS itself ([server.tls], issue #1603), set
# AUTUMN_HEALTHCHECK_URL to an https URL, for example
# `https://localhost:3000/startup`. A plain-HTTP probe always fails on an HTTPS
# listener.
#
# Certificate verification is skipped only when AUTUMN_HEALTHCHECK_INSECURE is
# set to a non-empty value. Set it alongside a LOOPBACK https URL: that probe
# never leaves the container, and your certificate is issued to the app's
# PUBLIC hostname, so it can never validate as `localhost`.
#
# Deciding this by parsing the URL instead is a bug farm — `user@host`,
# `#fragment`, and lookalike hostnames all produce URLs that read as loopback
# but that curl resolves somewhere else — so the choice is an explicit,
# auditable env var rather than an inference. Unset (the default), the probe
# always verifies.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD curl -fsS ${AUTUMN_HEALTHCHECK_INSECURE:+--insecure} \
        "${AUTUMN_HEALTHCHECK_URL:-http://localhost:3000/startup}" >/dev/null || exit 1

USER autumn

# Run migrations once before rolling web replicas, targeting only the primary:
#   AUTUMN_DATABASE__PRIMARY_URL=... autumn migrate
# Then start this image as the web process. Replica lag should fail readiness
# or fall back to primary according to database.replica_fallback.
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/usr/local/bin/{{project_name}}"]