3c7f28cc67
The pinned Debian snapshot shipped libgnutls30 3.7.9-2+deb12u6, which Trivy flagged for 5 fixed HIGH/CRITICAL CVEs (deb12u7 available), blocking the publish gate. Add apt-get upgrade to the runtime stage so security fixes land without chasing a newer base snapshot on every advisory. Verified locally: Trivy HIGH/CRITICAL fixed-only scan reports 0 findings.
102 lines
3.6 KiB
Docker
102 lines
3.6 KiB
Docker
# Multi-stage build producing a self-contained mix release.
|
|
#
|
|
# Builder: full Elixir/OTP toolchain + Node-free asset pipeline (esbuild +
|
|
# tailwind are fetched by mix tasks). Runtime: debian-slim with only the
|
|
# OpenSSL/ncurses libs the BEAM needs. The release embeds ERTS, so no Elixir is
|
|
# installed in the runtime image.
|
|
#
|
|
# Pin the builder and runtime to the SAME Debian snapshot so the runtime shared
|
|
# libraries match what the release was compiled against. Bump DEBIAN_SNAPSHOT
|
|
# and the hexpm tag together — they are published in lockstep.
|
|
|
|
ARG ELIXIR_VERSION=1.18.4
|
|
ARG OTP_VERSION=27.3.4
|
|
ARG DEBIAN_RELEASE=bookworm
|
|
ARG DEBIAN_SNAPSHOT=20260518
|
|
|
|
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_RELEASE}-${DEBIAN_SNAPSHOT}-slim"
|
|
ARG RUNNER_IMAGE="debian:${DEBIAN_RELEASE}-${DEBIAN_SNAPSHOT}-slim"
|
|
|
|
# ---- build stage ----
|
|
FROM ${BUILDER_IMAGE} AS builder
|
|
|
|
RUN apt-get update -y \
|
|
&& apt-get install -y build-essential git \
|
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
|
|
WORKDIR /app
|
|
|
|
RUN mix local.hex --force && mix local.rebar --force
|
|
|
|
ENV MIX_ENV="prod"
|
|
|
|
# Dependencies first for layer caching.
|
|
COPY mix.exs mix.lock ./
|
|
RUN mix deps.get --only $MIX_ENV
|
|
RUN mkdir config
|
|
|
|
COPY config/config.exs config/${MIX_ENV}.exs config/
|
|
RUN mix deps.compile
|
|
|
|
COPY priv priv
|
|
COPY lib lib
|
|
COPY assets assets
|
|
|
|
# Compile FIRST: the colocated-hooks extractor runs as a compiler and writes
|
|
# phoenix-colocated/<app> into the build path, which esbuild resolves via
|
|
# NODE_PATH. assets.deploy (esbuild) must therefore run AFTER compile, or the
|
|
# `import "phoenix-colocated/bulwark"` in app.js fails to resolve.
|
|
RUN mix compile
|
|
RUN mix assets.deploy
|
|
|
|
COPY config/runtime.exs config/
|
|
COPY rel rel
|
|
|
|
RUN mix release
|
|
|
|
# ---- runtime stage ----
|
|
FROM ${RUNNER_IMAGE}
|
|
|
|
# Apply outstanding security updates on top of the pinned snapshot, then install
|
|
# only the libs the BEAM needs. `upgrade` keeps us current on CVE fixes (e.g.
|
|
# libgnutls30 security patches) without chasing a newer base snapshot on every
|
|
# advisory; the pinned snapshot still gives a reproducible starting point.
|
|
RUN apt-get update -y \
|
|
&& apt-get upgrade -y \
|
|
&& apt-get install -y --no-install-recommends \
|
|
libstdc++6 openssl libncurses6 locales ca-certificates \
|
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
|
|
# UTF-8 locale (the BEAM needs it for string handling).
|
|
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
|
ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8
|
|
|
|
WORKDIR /app
|
|
RUN chown nobody /app
|
|
|
|
# Runtime hardening: no BEAM distribution (no clustering by default), and a
|
|
# crash dump that doesn't leak to disk. Override RELEASE_DISTRIBUTION in the
|
|
# Deployment if you wire DNS_CLUSTER_QUERY for clustering.
|
|
ENV MIX_ENV="prod" \
|
|
RELEASE_DISTRIBUTION="none" \
|
|
ERL_CRASH_DUMP="/dev/null" \
|
|
ERL_CRASH_DUMP_SECONDS="0"
|
|
|
|
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/bulwark ./
|
|
|
|
USER nobody
|
|
|
|
EXPOSE 4000
|
|
|
|
# No in-image HEALTHCHECK: liveness/readiness are HTTP probes against /health
|
|
# and /ready (see DEPLOYMENT.md). Kubernetes ignores Docker HEALTHCHECK, and the
|
|
# slim runtime ships no HTTP client — adding one purely for a healthcheck is
|
|
# avoidable bloat. A `bin/bulwark rpc` check won't work either, since
|
|
# RELEASE_DISTRIBUTION=none disables the node-to-node connection rpc needs.
|
|
|
|
# Migrations run as an initContainer in the cluster (see DEPLOYMENT.md), so the
|
|
# image's default command is JUST the server. For a standalone `docker run`,
|
|
# override with `/app/bin/migrate` first, or run both:
|
|
# docker run ... bulwark sh -c '/app/bin/migrate && /app/bin/server'
|
|
CMD ["/app/bin/server"]
|