# syntax=docker/dockerfile:1.7 # # Phoenix release image for Managoat (umbrella). Build stage compiles # the `fountain_server` release against Erlang/OTP 28 + Elixir 1.19; # runtime stage copies the release onto a slim Debian base. # # Notes: # - Umbrella: COPY apps/, config/, and the root mix.exs/mix.lock. # Release definition lives in the umbrella root (releases/0 in # mix.exs). # - No assets pipeline today: apps/fountain has no `assets/` dir and # no `mix assets.deploy` alias. If/when Tailwind+esbuild land, # add a `mix assets.deploy` step before `mix release`. # - SECRET_KEY_BASE is provided via the Infisical-materialized # fountain-secrets Secret (deployment.yaml envFrom). No fallback # generation here — fail-fast at boot if it's missing rather than # silently invalidate cookie sessions on every restart. FROM hexpm/elixir:1.19.2-erlang-28.3-debian-trixie-20260610-slim AS build ENV MIX_ENV=prod WORKDIR /app RUN apt-get update -y \ && apt-get install -y --no-install-recommends build-essential git \ && rm -rf /var/lib/apt/lists/* RUN mix local.hex --force \ && mix local.rebar --force # Umbrella deps live at the root. Copy ONLY what dependency resolution # reads — the root mix files, config, and each app's mix.exs — before # fetching and compiling deps, so the (expensive, slow-moving) deps layer # survives any change to application code. Copying apps/ wholesale here # is what used to force a full dep recompile on every commit. # # coverage.exs is part of that set, not test-only config: both mix.exs # files read it with Code.eval_file to build `test_coverage`, which runs # while Mix loads the project — before any task, in every MIX_ENV. Leave # it out and `mix deps.get` dies on Code.LoadError (#620 moved the # settings into this file and the prod image stopped building). COPY mix.exs mix.lock coverage.exs ./ COPY config ./config COPY apps/fountain/mix.exs ./apps/fountain/mix.exs # The umbrella loads every child's mix.exs to resolve deps; apps/fountain is # the only child, and the managoat_* libraries come from hex (decisions/0037). RUN mix deps.get --only prod \ && mix deps.compile COPY apps ./apps # Nothing from docs/: the manual is a static build in site/, not part of the # server (decisions/0081). `Fountain.FirstRequest` reads the one copy of the # first request at compile time from apps/fountain/priv/first_request/, which # the line above carries; `first_request_test.exs` pins that. # # rel/ is what `mix release` adds to the stock release: the overlays # (bin/server, bin/migrate). Mix reads them from rel/ when it is there and # skips them without a word when it is not, so this line is the whole # difference. COPY rel ./rel # `mix release` also fetches the pinned ACP adapter bundles from their GitHub # release tags and verifies each against the compiled SHA-512 (the release's # extra step in the root mix.exs, #2970), so this step needs outbound HTTPS # and adds about 9.5 MB to the image. RUN mix compile \ && mix release fountain_server # --- FROM debian:trixie-slim AS runtime # Git SHA of the source commit, surfaced in-app under the sidebar email # popup so we can confirm at-a-glance which build is running. Wired up by the build workflow; falls # back to "dev" for local `docker build` invocations. ARG BUILD_SHA=dev ENV LANG=C.UTF-8 \ MIX_ENV=prod \ PHX_SERVER=true \ PORT=4000 \ MANAGOAT_BUILD_SHA=$BUILD_SHA RUN apt-get update -y \ && apt-get install -y --no-install-recommends \ libstdc++6 openssl libncurses6 locales ca-certificates tini \ && rm -rf /var/lib/apt/lists/* \ && groupadd --system --gid 1001 fountain \ && useradd --system --uid 1001 --gid fountain --no-create-home fountain WORKDIR /app COPY --chown=fountain:fountain --from=build /app/_build/prod/rel/fountain_server ./ EXPOSE 4000 USER fountain ENTRYPOINT ["/usr/bin/tini", "--"] # Migrations run on every boot. Idempotent (Ecto.Migrator skips # already-applied versions) and serialized by a Postgres advisory lock, so # safe under rolling updates; the readiness probe gates traffic until # migrate + start completes. # # MIGRATE_ON_BOOT=false turns this into a plain start, for a deployment that # runs `bin/migrate` once in a Job instead (#610). `bin/migrate` ignores the # switch — it is the Job's entrypoint. CMD ["/bin/sh", "-c", "/app/bin/fountain_server eval 'Fountain.Release.migrate_on_boot()' && exec /app/bin/fountain_server start"]