prop/Dockerfile.ci
Graham McIntire 08175fc6ae
Some checks failed
Build CI test image / Build and push CI image (push) Failing after 3m9s
Build and Push / Build and Push Docker Image (push) Has been cancelled
feat: precompile EXLA in CI base image to prevent OOM kills
Add Dockerfile.ci that extends the hexpm/elixir image with precompiled
EXLA NIF cached at ~/.cache/xla/. When mix test runs, EXLA's cached_make
finds the precompiled libexla.so and skips the OOM-prone g++ C++
compilation entirely.

- Dockerfile.ci: two-stage build — builder compiles EXLA into cache;
  final stage copies the cache and installs test deps (cdo)
- .forgejo/workflows/build-ci-image.yaml: builds and pushes prop-ci
  image on mix.exs/mix.lock/vendor changes + weekly cron
- .forgejo/workflows/build.yaml: test step uses prop-ci:latest instead
  of raw hexpm/elixir image, removing apt-get + hex/rebar install steps
2026-08-05 09:13:50 -05:00

67 lines
2.6 KiB
Text

# syntax=docker/dockerfile:1.6
#
# Pre-built CI test image. Extends the hexpm/elixir image with:
# * build-essential + git + curl + ca-certificates + cdo
# * hex + rebar
# * Precompiled EXLA NIF cached at ~/.cache/xla/
#
# When mix test runs with this image, EXLA's cached_make finds the
# precompiled libexla.so and skips the OOM-prone g++ C++ compilation
# entirely. Rebuild this image whenever mix.exs changes elixir/exla/xla
# versions or c_src/ files change (the cache key auto-invalidates).
#
# Built by .forgejo/workflows/build-ci-image.yaml.
ARG ELIXIR_IMAGE="docker.io/hexpm/elixir:1.20.1-erlang-29.0.2-debian-trixie-20260518-slim"
FROM ${ELIXIR_IMAGE} AS builder
# Install build tools and runtime test deps (cdo is needed by tests).
RUN export DEBIAN_FRONTEND=noninteractive && \
apt-get update -qq && \
apt-get install -y -qq git curl ca-certificates build-essential cdo && \
rm -rf /var/lib/apt/lists/*
# Install hex + rebar
RUN mix local.hex --force && mix local.rebar --force
# Precompile EXLA to populate ~/.cache/xla/ with both the downloaded
# XLA archive and the compiled libexla.so. We only need mix.exs +
# mix.lock + vendor + minimal config — no full project checkout.
WORKDIR /tmp/exla_warm
# Copy only what's needed for deps.get + deps.compile exla
COPY mix.exs mix.lock /tmp/exla_warm/
COPY vendor /tmp/exla_warm/vendor
# Minimal config — nx needs :default_backend to exist
RUN mkdir -p /tmp/exla_warm/config && \
printf 'import Config\nconfig :nx, :default_backend, EXLA.Backend\n' > /tmp/exla_warm/config/config.exs
# Fetch deps and compile EXLA. The cached_make compiler downloads the XLA
# archive, extracts it, compiles the C++ NIF, and caches libexla.so at
# ~/.cache/xla/exla/{cache_key}/libexla.so. This layer is cached by
# Docker until mix.exs/mix.lock/vendor change.
RUN mix deps.get && mix deps.compile exla
# Clean up the temp project — the cache in ~/.cache/xla/ persists in
# the image and is what matters.
RUN rm -rf /tmp/exla_warm
FROM ${ELIXIR_IMAGE} AS final
# Install runtime test deps
RUN export DEBIAN_FRONTEND=noninteractive && \
apt-get update -qq && \
apt-get install -y -qq git curl ca-certificates build-essential cdo && \
rm -rf /var/lib/apt/lists/*
# Install hex + rebar
RUN mix local.hex --force && mix local.rebar --force
# Copy the precompiled EXLA cache from the builder stage. This is the
# ~/.cache/xla/ directory containing the XLA archive download and the
# compiled libexla.so keyed by exact Elixir/ERTS/exla/xla/c_src versions.
COPY --from=builder /root/.cache/xla /root/.cache/xla
WORKDIR /app