7.0 KiB · text History 36cf3d6
# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
# instead of Alpine to avoid DNS resolution issues in production.
#
# https://hub.docker.com/r/hexpm/elixir/tags?name=ubuntu
# https://hub.docker.com/_/ubuntu/tags
#
# This file is based on these images:
#
#   - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
#   - https://hub.docker.com/_/debian/tags?name=trixie-20260505-slim - for the release image
#   - https://pkgs.org/ - resource for finding needed packages
#   - Ex: docker.io/hexpm/elixir:1.20.4-erlang-28.5.0.8-debian-trixie-20261020-slim
#
ARG ELIXIR_VERSION=1.20.4
# OTP 28.5.0.2 (was 28.4.1) refreshed the Erlang :ssh app (embedded git
# SSH daemon was hitting intermittent "incorrect signature" on 5.5.1);
# 28.5.0.8 is that line's latest patch.
ARG OTP_VERSION=28.5.0.8
# The builder's Debian is whichever dated tag hexpm built this Elixir/OTP
# pair on; it stops getting new ones, and nothing of it ships but the
# release (same Debian release, so the same glibc and libssl ABI).
ARG DEBIAN_VERSION=trixie-20261020-slim
# What ships is the runner, so it moves on by itself: the newest dated
# tag, which scripts/dependency-update bumps (the marker below) when
# Debian publishes one. A dated tag rather than `trixie-slim` so a
# rebuild is the image it was, and so moving it busts the layer cache —
# the registry cache would otherwise keep an old `apt-get upgrade`.
# dependency-update: docker.io/library/debian
ARG RUNNER_DEBIAN_VERSION=trixie-20261020-slim

ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="docker.io/debian:${RUNNER_DEBIAN_VERSION}"

FROM ${BUILDER_IMAGE} AS builder

# install build dependencies.
#   * build-essential, git — Elixir / mix build chain
#   * pkg-config, libssl-dev — gix transitively links against system openssl,
#     and gitgud_apt's sequoia-openpgp uses its OpenSSL crypto backend
#   * libclang-dev — `openssl-sys` generates its bindings with bindgen,
#     which dlopens libclang. Build-stage only: the final image never
#     sees it, and it is the one package the apt signing NIF adds
#     anywhere. Every sequoia backend that binds C needs this; only the
#     pure-Rust one avoids it, and that one is variable-time.
#   * curl, ca-certificates — needed to bootstrap rustup
RUN apt-get update \
  && apt-get install -y --no-install-recommends \
       build-essential git pkg-config libssl-dev libclang-dev curl ca-certificates \
  && rm -rf /var/lib/apt/lists/*

# Rust via rustup, pinned. apt's rust on trixie is 1.85, but the gix
# dependency tree requires 1.87+; rustup keeps the compiler decoupled
# from the OS rev. Pinned to a known-good rev so builds are
# reproducible.
ENV RUSTUP_HOME=/usr/local/rustup \
    CARGO_HOME=/usr/local/cargo \
    PATH=/usr/local/cargo/bin:${PATH} \
    RUST_VERSION=1.92.0
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
  | sh -s -- -y --profile minimal --default-toolchain ${RUST_VERSION} \
  && rustc --version && cargo --version

# prepare build dir
WORKDIR /app

# install hex + rebar
RUN mix local.hex --force \
  && mix local.rebar --force

# set build ENV
ENV MIX_ENV="prod"

# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config

# copy compile-time config files before we compile dependencies
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/
RUN mix deps.compile

RUN mix assets.setup

COPY priv priv

COPY lib lib

# Rust source for the NIF crates (gitgud_gix, gitgud_apt) — Rustler
# invokes `cargo` from this directory during `mix compile`, so it must
# be present. gitgud_apt links libssl/libcrypto, which `libssl-dev`
# above already provides for the builder and the runner's `openssl`
# package provides at runtime — sequoia's OpenSSL backend was chosen
# precisely so neither image needs a new package.
COPY native native

# Compile the release
RUN mix compile

COPY assets assets

# compile assets
RUN mix assets.deploy

# Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/

COPY rel rel

# Package source into the release so Sentry stacktraces show code
# context (lib/ is already copied above). Without this Sentry still
# reports errors, just without snippets, and warns at boot.
RUN mix sentry.package_source_code

RUN mix release

# The release tree on its own, for anything that wants the bits without
# the image around them — `packaging/deb/build-deb.sh` turns this into a
# .deb in CI:
#
#   docker buildx build --target release-export \
#     --output type=local,dest=./release .
#
# `scratch` so the export is exactly the release and nothing else.
FROM scratch AS release-export
COPY --from=builder /app/_build/prod/rel/git_gud /

# start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE} AS final

# Runtime packages.
#   * libstdc++6, openssl, libncurses6, locales, ca-certificates — Erlang runtime
#   * git — the Cli backend shells out to git for ops the NIF hasn't covered
#     yet (commit_tree, update_ref, plumbing for profile-README seeding,
#     post-receive hook helpers). Also used by the embedded SSH daemon to
#     spawn upload-pack / receive-pack per session.
#   * openssh-client — ships `ssh-keygen`, which `GitGud.Ssh.Server`
#     shells out to on first boot to generate the host keys (ed25519,
#     rsa, ecdsa) if they're absent from `SSH_HOST_KEY_DIR`.
#   * curl — `priv/scripts/gitgud-hook` (pre-receive + post-receive
#     bridge) POSTs ref-update payloads to the Phoenix endpoint via
#     curl. Without it, branch protection checks and the
#     CommitBackfill / push-event publishing chain never fire on
#     SSH-side pushes. Mandatory.
#   * tini — proper PID 1 / zombie reaping, recommended for any container
#     image running long-lived child processes (we fork git per session).
#
# `upgrade` first: Debian's security fixes since the base tag was cut.
# audit.yml fails nightly while the published image has some pending.
RUN apt-get update \
  && apt-get upgrade -y --no-install-recommends \
  && apt-get install -y --no-install-recommends \
       libstdc++6 openssl libncurses6 locales ca-certificates \
       git openssh-client curl tini \
  && rm -rf /var/lib/apt/lists/*

# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
  && locale-gen

ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8

WORKDIR "/app"
RUN chown nobody /app

# set runner ENV
ENV MIX_ENV="prod"

# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/git_gud ./

USER nobody

# tini as PID 1 — reaps the git child processes we fork from the embedded
# SSH daemon + plumbing helpers. Without this, zombies accumulate across
# pushes / pulls in long-lived containers. The package installs the
# binary at /usr/bin/tini on debian; symlinks for safety.
ENTRYPOINT ["/usr/bin/tini", "--"]

CMD ["/app/bin/server"]