So handhabe ich persönlich das Caching des OCaml-Compilers und der Dune-Package-Builds in Docker. Das ist keine Empfehlung des Dune-Teams. Ich freue mich über Feedback oder darüber, wie andere das lösen.
Du kannst dir das Beispiel-Repo unter sabine/dune-pkg-cacheable-docker-build ansehen.
Ziel: den OCaml-Compiler nicht bei jeder Quellcode-Änderung neu zu bauen.
Das Problem
Wenn du Dune Package Management (dune pkg + dune.lock) nutzt, installiert Dune während des Builds den OCaml-Compiler und alle Abhängigkeiten. In Docker ist das teuer und leicht zu invalidieren, wenn du dein gesamtes Repo zu früh kopierst. In CI bedeutet das oft, dass der Compiler bei jeder Code-Änderung neu gebaut wird.
Die Lösung ist einfach: Den Compiler- und Package-Build in eine eigene Docker-Schicht legen, die nur von dune-project und dune.lock abhängt.
Das Docker Cache Pattern (Build in Zwei Schritten)
Die zentrale Idee ist es, dune build @pkg-install zu nutzen, das alle Abhängigkeiten aus der Lockfile baut. So kann Docker die teure Compiler- und Dependency-Schicht cachen.
Unoptimiertes Dockerfile (alles invalidiert die Compiler-Schicht)
FROM debian:bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
git \
m4 \
pkg-config \
unzip \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://github.com/ocaml-dune/dune-bin-install/releases/download/v3/install.sh | sh -s -- 3.20.2 --install-root /usr --no-update-shell-config
WORKDIR /app
COPY . .
ENV TMPDIR=/app
RUN dune pkg lock
RUN dune build --profile=release
Optimiertes Dockerfile (Compiler + Packages cachen)
FROM debian:bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
git \
m4 \
pkg-config \
unzip \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://github.com/ocaml-dune/dune-bin-install/releases/download/v3/install.sh | sh -s -- 3.20.2 --install-root /usr --no-update-shell-config
WORKDIR /app
COPY dune-project dune-project
# Wenn deine dune-workspace die Abhängigkeiten beeinflusst (z.B. override-pins), kopiere sie ebenfalls hierher
# COPY dune-workspace dune-workspace
ENV TMPDIR=/app
RUN dune pkg lock
RUN dune build @pkg-install
COPY . .
RUN dune build --profile=release
Der Effekt: Wenn sich nur der Anwendungscode ändert, kann Docker die Schicht wiederverwenden, die den OCaml-Compiler und das Dependency-Set gebaut hat.
Minimales Projekt-Setup
Dieser Ansatz funktioniert, egal ob du dune.lock eincheckst oder während des Docker-Builds generierst.
dune pkg lock
Dann kannst du lokal oder in CI bauen mit:
dune build --profile=release
Benchmark (lokal)
Das Benchmark-Repo liegt hier: sabine/dune-pkg-cacheable-docker-build
Durchschnittliche Warm-Rebuild-Zeit (3 Runs, nach Cache-Warm-up):
- Unoptimiert: 118.593s
- Optimiert: 1.907s
Architektur:
- OS: Linux pop-os 6.17.9-76061709-generic
- CPU: AMD Ryzen 7 7840HS (16 Threads)
- Arch: x86_64
Diese Runs sind „warm rebuilds“ nach einer Änderung an einer einzigen Source-Datei. Das optimierte Dockerfile hält Compiler + Packages im Cache und baut nur die App-Schicht neu.
Update
Ursprünglich nutzte dieser Post ein eigenes Dummy-Target, um die Package-Builds zu triggern. Es stellt sich heraus, dass Dune dafür ein eingebautes Target hat: dune build @pkg-install. Das ist viel sauberer und ist jetzt mein Empfehlung. Das Beispiel-Repo wurde entsprechend aktualisiert.
Hinweise und Caveats
- Wenn sich
dune.lockändert, invalidiert der Cache (das ist so gewollt). - Ich habe
dune.lockanfangs eingecheckt, es später aber entfernt, um der aktuellen Empfehlung der Dune-Maintainer zu folgen. - Wenn sich das Base-Image oder die Dune-Version ändert, wird der Cache invalidiert.
- Halte deine
.dockerignoreschlank, damit kleine Dateien nicht den Build-Kontext kommen.
Fazit
So handhabe ich aktuell Docker-Builds mit Dune Package Management. Wenn du etwas Besseres oder Einfacheres machst, würde ich es gerne hören.
