darcsweb - Dockerfile
[root] / Dockerfile
# syntax=docker/dockerfile:1.7
# -- Build stage --
#
# The haskell and debian base images are multi-arch (amd64 + arm64), so this
# Dockerfile works natively on x86-64 as well as ARM64 hosts like the
# Raspberry Pi 5. Building on the Pi itself is slow; see README.md for
# cross-build instructions.
#
# BuildKit cache mounts are used below so that apt packages, the opam
# switch (OCaml + Rocq), the stack resolver, and .stack-work survive
# between builds. BuildKit is the default builder since Docker 23; older
# hosts need `DOCKER_BUILDKIT=1 docker build ...`.
FROM haskell:9.6-slim AS build
# TARGETARCH is populated by BuildKit from the target platform and is used
# below to namespace cache mounts so arm64 and amd64 builds do not share
# arch-specific binaries (notably the opam switch, which is native code).
ARG TARGETARCH
# Build parallelism. Defaults to 2 because the Raspberry Pi 5 hangs under
# -j4 (GHC linking and the opam Rocq build exhaust RAM on a 4 GB Pi).
# Raise it on bigger hosts via `--build-arg JOBS=$(nproc)`.
ARG JOBS=2
# Install Rocq build dependencies. /var/cache/apt and /var/lib/apt are
# cache-mounted so repeated builds do not redownload .deb files; the
# default docker-clean hook is removed so apt keeps its cache.
RUN --mount=type=cache,id=darcsweb-apt-cache-${TARGETARCH},target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=darcsweb-apt-lib-${TARGETARCH},target=/var/lib/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean && \
apt-get update && \
apt-get install -y --no-install-recommends \
opam make sed ca-certificates \
libgmp-dev linux-libc-dev pkg-config
# Pin OCaml + Rocq. /root/.opam is cache-mounted, so the multi-minute
# OCaml and Rocq compilation runs once, not on every rebuild. The mount
# must be re-declared on every subsequent RUN that invokes `rocq` (the
# stack build triggers extraction through Setup.hs). A stale cache can
# leave the switch half-built; verify the compiler runs with the pinned
# version and rebuild from scratch on mismatch.
RUN --mount=type=cache,id=darcsweb-opam-${TARGETARCH},target=/root/.opam,sharing=locked \
if [ ! -f /root/.opam/config ]; then \
opam init --disable-sandboxing --bare -y; \
fi && \
if ! opam exec --switch=default -- ocaml -version 2>/dev/null | grep -q '4\.14\.2'; then \
opam switch remove default -y 2>/dev/null || true; \
opam switch create default ocaml-base-compiler.4.14.2 -j"${JOBS}"; \
fi && \
eval "$(opam env --switch=default)" && \
opam pin add rocq-core 9.0.0 -n && \
opam pin add rocq-stdlib 9.0.0 -n && \
opam install rocq-core.9.0.0 rocq-stdlib.9.0.0 -y -j"${JOBS}"
# Make rocq available to later RUN steps that mount /root/.opam.
ENV PATH=/root/.opam/default/bin:$PATH
WORKDIR /src
# Copy dependency manifests first so the snapshot/GHC layer is cached
# independent of source changes.
COPY stack.yaml stack.yaml.lock darcsweb.cabal Setup.hs LICENSE ./
# Stack's resolver, GHC tarball, and global package DB live in ~/.stack;
# cache-mount it so `stack setup` is a no-op on the second build.
RUN --mount=type=cache,id=darcsweb-stack-${TARGETARCH},target=/root/.stack,sharing=locked \
stack setup --install-ghc --no-terminal
# Copy sources and build. .stack-work is cache-mounted so Haskell
# modules are only recompiled when their sources actually change.
# gen/ is intentionally not copied: Setup.hs' preBuild hook runs
# `make -C verified extract`, which regenerates gen/*.hs from the
# Rocq sources in verified/ using the pinned Rocq 9.0.0 installed
# above. Copying a host-side gen/ would either fail on fresh hosts
# (the directory is not tracked in darcs) or ship stale extractions.
COPY src/ src/
COPY app/ app/
COPY test/ test/
COPY verified/ verified/
RUN --mount=type=cache,id=darcsweb-opam-${TARGETARCH},target=/root/.opam,sharing=locked \
--mount=type=cache,id=darcsweb-stack-${TARGETARCH},target=/root/.stack,sharing=locked \
--mount=type=cache,id=darcsweb-stackwork-${TARGETARCH},target=/src/.stack-work,sharing=locked \
stack build --no-terminal --copy-bins \
--local-bin-path /usr/local/bin \
-j"${JOBS}"
# -- Runtime stage --
FROM debian:bookworm-slim
ARG TARGETARCH
RUN --mount=type=cache,id=darcsweb-apt-cache-runtime-${TARGETARCH},target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=darcsweb-apt-lib-runtime-${TARGETARCH},target=/var/lib/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean && \
apt-get update && \
apt-get install -y --no-install-recommends libgmp10 && \
groupadd -r darcsweb && \
useradd -r -g darcsweb -s /sbin/nologin darcsweb
COPY --from=build /usr/local/bin/darcsweb /usr/local/bin/darcsweb
COPY static/ /usr/share/darcsweb/static/
RUN printf '%s\n' \
'bind = 0.0.0.0' \
'port = 3000' \
'repos = /srv/darcs' \
'title = DarcsWeb' \
'static = /usr/share/darcsweb/static' \
> /etc/darcsweb.conf
USER darcsweb
EXPOSE 3000
ENTRYPOINT ["darcsweb", "-c", "/etc/darcsweb.conf"]