I was writing some dockerfiles for building some OCaml applications recently, and realized that it is possible to write a Dockerfile that can build almost arbitrary Opam applications without even having to hardcode the package name.
I used ocaml-dockerfile
to generate it (only the OS and OCaml version is hardcoded), so I thought I’d share it, it should work with both Podman and Docker:
# syntax=docker/dockerfile:1
FROM ocaml/opam:debian-12-ocaml-4.14
# Update the opam repository (otherwise lockfile may contain a version we don't know about)
RUN git -C /home/opam/opam-repository pull origin master && opam-2.3 update
# Enable the dune cache in copy mode
# (hardlink mode would fail with EXDEV if cache is on separate disk/partition)
# This caches the build of opam dependencies using dune and the main application
ENV DUNE_CACHE="enabled" DUNE_CACHE_STORAGE_MODE="copy"
# `apt` in containers is usually configured to always clean up after operations
# When we have a cache mount for the APT package cache we don't want to clean up, since the cache would be ineffective.
# Also change OPAM to use the 0install solver, this is faster, especially when a lockfile is present.
RUN sudo rm -f /etc/apt/apt.conf.d/docker-clean && \
opam-2.3 option solver=builtin-0install
# Use a workdir outside of $HOME, to avoid having .opam as a subdir of the build
WORKDIR /app
# Copy dependency definitions first. See https://docs.docker.com/build/cache/optimize/#order-your-layers
COPY [ "*.opam", "*.opam.locked", "." ]
# Install and cache system packages required by the build
# The cache is locked, so multiple container builds will wait here
# (apt would have a lock but it is stored outside of the cache dir, so we cannot rely on it to prevent concurrent accesses)
# A cache id is used, so that different distros would have different cache folders
RUN --mount=type=cache,id=/var/cache/apt#debian-12;amd64,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=/var/lib/apt#debian-12;amd64,target=/var/lib/apt,sharing=locked \
sudo apt-get update -y && \
opam-2.3 install --locked --with-test . --depext-only
# Download and cache opam package dependencies
# The cache is locked, so multiple container builds will wait here
# To minimize the time the lock is held the actual package installations are done as a separate step
RUN --mount=type=cache,target=/home/opam/.opam/download-cache,sharing=locked,uid=1000,gid=1000 \
opam-2.3 install --locked --with-test . --download-only
# Install (cached) downloaded opam dependencies
# The download cache is mounted readonly and shared, multiple container builds can proceed in parallel.
# The dune cache is mounted RW.
# Multiple concurrent builds will use the same cache, but dune must already be able to cope with this
# A cache id is used, so that different distros would have different cache folders (it is unlikely that dune caches would be sharable across distros)
RUN --mount=type=cache,target=/home/opam/.opam/download-cache,readonly,sharing=shared,uid=1000,gid=1000 \
--mount=type=cache,target=/home/opam/.cache/dune,sharing=shared,uid=1000,gid=1000 \
opam-2.3 install --locked --with-test . --deps-only
# Copy actual application source code
COPY [ ".", "." ]
# Build and install application code, using dune cache.
RUN --mount=type=cache,target=/home/opam/.cache/dune,sharing=shared,uid=1000,gid=1000 \
opam-2.3 install --locked --with-test .
It avoids some common pitfalls:
- disable cleanup of APT downloaded packages (otherwise caching is ineffective)
- enables the opam download cache too which is stored in a non-standard location (
~/.opam/download-cache
instead of~/.cache/opam
) - avoids EXDEV from dune cached builds during opam dependency installations by enabling dune cache copy mode
- uses opam-2.3 explicitly, since the default opam in the Dockerhub images is quite old (luckily opam-2.3 is already there, just needs to be invoked explicitly)
- sets the default solver to 0install. This seems to be needed to speed up
opam install
even when--locked
is used and no dependency resolution is needed. Otherwise it was spending 4s checking the solver request. - sets appropriate uid in cache mounts to avoid permission issues
Caveats:
- if you use git submodules then you have to add
.git
to your.dockerignore
. Otherwise opam pinning will fail, since.git
is a file referencing a git dir in a parent dir that is not mounted inside the docker build environment - maybe --dev should be used if you intend to use the container for developing the opam application. Although in that case you might also want to preinstall some useful tools like
lsp
, andocamlformat
.
Eventually I hope this can be simplified using Dune’s new package management feature.