# 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"]
