Build:
- 0
2026-07-14 14:15.46: New job: ocaml-docs-ci {"image":"ocurrent/ocaml-docs-ci@sha256:d8f0a3df7958c4f60cc7daae27f03d17aafbcb2217e0571891585e6c68889b25"}
2026-07-14 14:15.46: Checking out commit 09923862. To reproduce:
git clone --recursive "https://github.com/ocurrent/ocaml-docs-ci.git" -b "dill" && cd "ocaml-docs-ci" && git reset --hard 09923862
2026-07-14 14:15.46: Exec: "cp" "-a" "--" "/var/lib/ocurrent/var/git/ocaml-docs-ci.git-f2841974542651d306d2cfeb807c13b7948df1dcad0f20da42c0688683c00f56/.git"
"/tmp/git-checkout3a618392"
2026-07-14 14:15.46: Exec: "git" "-C" "/tmp/git-checkout3a618392" "submodule"
"deinit" "--force" "--all"
2026-07-14 14:15.46: Exec: "git" "-C" "/tmp/git-checkout3a618392" "reset"
"--hard" "-q" "099238628a5d68052b3c83cc908bd291a002cd55"
2026-07-14 14:15.46: Exec: "git" "-c" "protocol.file.allow=always" "-C"
"/tmp/git-checkout3a618392" "submodule" "update"
"--recursive" "--init" "--no-fetch"
2026-07-14 14:15.46: docker-compose.yml
# ocaml-docs-ci, post-day11.
#
# Two services:
# - daemon — runs the ocaml-docs-ci binary. Drives runc directly
# to build each opam package in a sandboxed container, so it
# needs --privileged plus access to the host's runc (the
# debian-12 runc shipped in the image is enough on most kernels;
# swap to a bind-mount of /usr/bin/runc if the host's is newer).
# - web — Caddy, serving rendered HTML from the day11 cache
# directly and reverse-proxying everything else (dashboard, job
# logs, snapshot pages) to the daemon at :8080.
#
# The daemon resolves everything under /home/app (the image's app-user
# HOME). The whole [.day11] tree lives on a single named volume mounted
# at /home/app/.day11. The two profiles it runs are baked into the image
# at /home/app/.day11/profiles by the Dockerfile
# Everything under .day11 — cache,
# snapshots, overlays, and scratch (TMPDIR=/home/app/.day11/cache/tmp) —
# shares one volume/filesystem, so the daemon's hardlinks (cp --link
# of dep layers from cache into scratch on Stack.merge) and its cross-dir
# rename()s all stay within a single mount and just work.
services:
daemon:
# ocurrent-deployer pins this to a name@sha256 digest before deploying.
image: ocurrent/ocaml-docs-ci@sha256:d8f0a3df7958c4f60cc7daae27f03d17aafbcb2217e0571891585e6c68889b25
restart: unless-stopped
privileged: true
# The daemon builds (and imports) its base image via the host
# Docker daemon — [docker build] / [docker create] over the mounted
# socket below. The socket is group-owned by [docker]; since the
# daemon runs as the non-root app user, add that group so it can
# talk to the socket. This must match the docker group's gid *on the
# host*: dill's docker.io install put it at 107 (999 was already taken
# by systemd-journal). Update this if the host's docker gid differs.
group_add:
- "107"
# The default soft nofile limit (1024) is far too low for hundreds
# of concurrent builds — each holds open logs, overlay dirs, and
# pipes to sudo/runc, and exhaustion (EMFILE) cascades into mass
# build failures.
ulimits:
nofile:
soft: 1048576
hard: 1048576
init: false # dumb-init already in the image
ports:
- "8080:8080"
command:
# The image entrypoint is generic (it only preps TMPDIR), so the
# command names the binary. Run one-off CLI tools in the same
# environment with e.g.:
# docker compose run --rm daemon day11 batch --profile=full ...
- ocaml-docs-ci
- --profiles=quick,full
- --remote=https://github.com/ocaml/opam-repository.git=repo/opam-repository
- --jobs=40
- --cores-per-build=4
- --overcommit=2.5
- --port=8080
volumes:
# All of .day11 on a single named volume: cache (built layers +
# rendered HTML), snapshots, overlays, the opam-repository mirrors
# (repo/, git-cloned by --remote on first run), and scratch (TMPDIR=
# /home/app/.day11/cache/tmp). The image-baked profiles at
# /home/app/.day11/profiles are preserved — an empty named volume
# is seeded from the image's directory on first creation, so unlike
# a bind-mount it doesn't shadow them.
- day11:/home/app/.day11
# OCurrent state (sqlite + per-job logs). Mounted under
# /var/lib/ocurrent matching the image's WORKDIR.
- ocurrent-data:/var/lib/ocurrent
# Build/import the base image via the host Docker daemon.
- /var/run/docker.sock:/var/run/docker.sock
environment:
# HOME stays /home/app (set in the image); the daemon derives
# ~/.day11/{profiles,cache} from it.
- OCAMLRUNPARAM=b
web:
image: caddy:alpine
restart: unless-stopped
depends_on:
- daemon
ports:
# 80 for the ACME HTTP-01 challenge + HTTP->HTTPS redirect, 443 for
# TLS (and 443/udp for HTTP/3). Caddy auto-provisions a Let's Encrypt
# cert for the domain named in the Caddyfile; both must be reachable
# from the internet for issuance to succeed.
- "80:80"
- "443:443"
- "443:443/udp"
# The Caddyfile is shipped to the remote daemon as an inline Compose
# config (see the top-level `configs:` block below) rather than bind-
# mounting /etc/caddy. ocurrent-deployer runs `docker --context
# dill.caelum.ci.dev compose up`, so any bind source would resolve on
# the remote host's filesystem, not in the deployer's checkout — only
# an inline config ships the file's bytes to the remote daemon.
configs:
- source: caddyfile
target: /etc/caddy/Caddyfile
volumes:
# Serve rendered HTML straight from the shared day11 volume,
# mounted read-only at /srv. The per-arch cache root is a subdir
# of it (/srv/cache/debian-bookworm-x86_64); the Caddyfile prepends
# that prefix. NB: not /data — that's Caddy's own state dir.
- day11:/srv:ro
# Caddy's data (instance id, any future certs) and autosaved
# config. Named so they survive container recreation rather than
# piling up as anonymous volumes.
- caddy-data:/data
- caddy-config:/config
volumes:
day11:
caddy-data:
caddy-config:
ocurrent-data:
# Caddy front-end config, inlined so the deployment is self-contained and
# needs nothing pre-placed on the host. Compose ships this content to the
# remote daemon and mounts it at /etc/caddy/Caddyfile in the web service.
configs:
caddyfile:
content: |
# Caddy front-end for ocaml-docs-ci.
#
# Two responsibilities:
# 1. Serve rendered HTML straight from the day11 cache.
# Each profile's docs are built into a per-toolchain epoch dir and
# exposed through a [html-live] symlink that's swapped manually on
# promotion. day11 lays a profile's live docs out at
# [<cache>/html-<profile>/html-live/p/<pkg>/<ver>/doc/index.html]
# (html-live -> epoch-<hash>/html), and the daemon links to
# [/profiles/<NAME>/docs/p/<pkg>/<ver>/...].
# The .day11 volume is mounted read-only at /srv (Caddy keeps its
# own state on /data, so we can't reuse that path). Rendered HTML
# is arch-independent, so it lives under a flat per-profile dir —
# live docs are under [/srv/cache/html/<profile>/html-live/].
# The html-live symlink is *relative* (epoch-<hash>/html), so it
# resolves within /srv. Until the first promote, html-live is absent
# and these routes 404 (the rest of the site still proxies fine).
# 2. Reverse-proxy everything else to the daemon's web UI on :8080 —
# the dashboard, snapshot pages, /job/ log viewer, diff pages, etc.
#
# TLS: naming a real domain (not :80) turns on Caddy's automatic HTTPS —
# it provisions and renews a Let's Encrypt cert over ACME and redirects
# plain HTTP to HTTPS. Needs ports 80 + 443 reachable from the internet
# and a DNS record for the domain pointing here. Certs/account key persist
# on the /data volume so restarts don't re-request.
dill.caelum.ci.dev {
# gzip-compress the static .css/.js/.json odoc emits (and HTML).
encode gzip
# Allow large request bodies for anything the daemon surfaces
# (e.g. snapshot uploads via the API).
request_body {
max_size 100MB
}
# /profiles/<name>/docs/<rest> → serve from the day11 cache.
# Capture 1 is the profile, capture 2 the path within its html tree.
@docs path_regexp docs ^/profiles/([^/]+)/docs/(.*)$
handle @docs {
root * /srv/cache/html/{re.docs.1}/html-live
rewrite * /{re.docs.2}
# odoc trees carry an index.html in every directory; serve it for
# bare-directory requests. Missing files 404 (file_server default).
file_server {
index index.html
}
}
# Everything else → the daemon. Caddy forwards the Host header and
# sets the X-Forwarded-* headers automatically.
handle {
reverse_proxy daemon:8080 {
# Don't buffer responses: the /job/ log viewer streams
# (SSE-style) and must flush as data arrives. Caddy applies
# no upstream read timeout by default, so long-lived streams
# stay open (nginx needed an explicit 1d bump for this).
flush_interval -1
}
}
}
2026-07-14 14:15.46: Exec: "docker" "--context" "dill.caelum.ci.dev"
"compose" "-p" "ocaml-docs-ci" "-f" "/tmp/git-checkout3a618392/docker-compose.yml"
"pull"
Image caddy:alpine Pulling
Image ocurrent/ocaml-docs-ci@sha256:d8f0a3df7958c4f60cc7daae27f03d17aafbcb2217e0571891585e6c68889b25 Pulling
Image ocurrent/ocaml-docs-ci@sha256:d8f0a3df7958c4f60cc7daae27f03d17aafbcb2217e0571891585e6c68889b25 Pulled
Image caddy:alpine Pulled
2026-07-14 14:15.48: Exec: "docker" "--context" "dill.caelum.ci.dev"
"compose" "-p" "ocaml-docs-ci" "-f" "/tmp/git-checkout3a618392/docker-compose.yml"
"up" "-d"
Container ocaml-docs-ci-daemon-1 Recreate
Container ocaml-docs-ci-daemon-1 Recreated
Container ocaml-docs-ci-web-1 Running
Container ocaml-docs-ci-daemon-1 Starting
Container ocaml-docs-ci-daemon-1 Started
2026-07-14 14:15.53: Job succeeded