# Mathew — the Lean editor server # # Builds lean4web (https://github.com/leanprover-community/lean4web) from its # upstream source and runs its production server: one port serves the editor # client and the Lean sessions, which is what the wiki frames at /lean. # # Lean4web runs a real Lean process per session, so upstream asks for a # dedicated server — this image carries the editor and Mathlib, the machine # carries the load. # # docker build -t mathew-lean ./lean # docker run -p 8080:8080 mathew-lean # → http://localhost:8080 # # The build is heavy by nature (Mathlib is fetched and its precompiled cache # unpacked), but multi-stage keeps the junk out of the pushed image: the # compilers, npm dev dependencies and caches stay in the builder, and the # final image is a slim runtime with exactly what a session needs — the node # server, the elan toolchain, the built client, and the project's Lean cache. # # To ship an image without Mathlib itself (~4GB instead of ~11GB), pass # `--build-arg BAKE_MATHLIB=false` and hold the cache in a volume instead: # the entrypoint fills a fresh volume once at first start. See # "Keeping Mathlib out of the image" in lean/README.md. # ---------- build ---------- FROM node:24-bookworm AS build # lake fetches dependencies over git/curl, drives clang to link, and elan # (Lean's toolchain manager) installs the toolchains each project pins RUN apt-get update && apt-get install -y --no-install-recommends \ bash curl git ca-certificates unzip rsync \ build-essential clang pkg-config libssl-dev \ && rm -rf /var/lib/apt/lists/* # elan, with no default toolchain — every project pins its own in its # lean-toolchain file, and elan installs that one when asked RUN curl -sSfL https://elan.lean-lang.org/elan-init.sh | bash -s -- -y --default-toolchain none ENV PATH="/root/.elan/bin:${PATH}" # cypress (a root dev dependency) pulls an ~800MB browser bundle on install — # nothing in this build runs browser tests, so skip it ENV CYPRESS_INSTALL_BINARY=0 # The lean4web source. Pass a tag or branch to pin a build: # docker build --build-arg LEAN4WEB_REF=v0.2.2 ./lean ARG LEAN4WEB_REF=main RUN git clone --depth 1 --branch "${LEAN4WEB_REF}" \ https://github.com/leanprover-community/lean4web.git /opt/lean4web # Ship only the MathlibDemo project: the wiki opens the default project, and # upstream's second demo ("Stable") pins its own toolchain — elan would carry # a second ~3GB one that no session ever uses. RUN rm -rf /opt/lean4web/Projects/Stable WORKDIR /opt/lean4web RUN npm install # Build the Lean projects under Projects/. For MathlibDemo this fetches # Mathlib and unpacks its precompiled cache — the slow part of the image. # With BAKE_MATHLIB=false that step is skipped and only the pinned toolchain # is installed: the project ships unbuilt, and the entrypoint fills it on a # first start (the intended pairing is a volume over the project's .lake). ARG BAKE_MATHLIB=true RUN if [ "${BAKE_MATHLIB}" = "true" ]; then \ npm run build:projects; \ else \ (cd Projects/MathlibDemo && lake --version > /dev/null); \ fi # Mathew's theme layer: the wiki's font, stylesheet, and theme handshake # join the client's public/ so they ship in the built bundle, and a sed # wires them into index.html (and renames the tab). The wiki opens the # frame as ?theme=light|dark and the editor opens matching the page # around it — lean/theme/mathew.css explains how the skin attaches. COPY theme/mathew.css theme/theme-init.js ./client/public/ COPY theme/fonts/. ./client/public/fonts/ RUN sed -i \ -e 's##\n \n #' \ -e 's#Lean Web#Lean · Mathew#' \ client/index.html RUN npm run build:client # ---------- runtime ---------- # Only what serving sessions needs. The lean/lake binaries carry their own # shared libraries — nothing else from the builder has to come along. FROM node:24-bookworm-slim # bash for scripts, curl for the healthcheck and Mathlib's cache fetch, # git for lake's dependency operations RUN apt-get update && apt-get install -y --no-install-recommends \ bash curl git ca-certificates \ && rm -rf /var/lib/apt/lists/* # elan and the toolchain(s) the projects resolved at build time COPY --from=build /root/.elan /root/.elan ENV PATH="/root/.elan/bin:${PATH}" WORKDIR /opt/lean4web # The server, the built (wiki-skinned) client, and the manifests. Note the # whole prod dependency tree: the server imports vscode-ws-jsonrpc out of the # client workspace (npm hoists it), so a server-only `npm ci -w` would miss it. COPY --from=build /opt/lean4web/package.json /opt/lean4web/package-lock.json ./ COPY --from=build /opt/lean4web/client/package.json ./client/ COPY --from=build /opt/lean4web/server ./server COPY --from=build /opt/lean4web/client/dist ./client/dist # The Lean project: toolchain pin, lakefile, and — unless BAKE_MATHLIB=false # — Mathlib and its unpacked cache, the bulk of the image. COPY --from=build /opt/lean4web/Projects ./Projects RUN npm ci --omit=dev \ && rm -rf /root/.npm # Normally a no-op: the image already carries the built Mathlib. With # BAKE_MATHLIB=false and a volume over the project's .lake, this fills a # fresh volume once before the server starts (see lean/README.md). COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh # Bubblewrap cannot sandbox inside a container (no nested user namespaces by # default) — Docker itself is the isolation here, so the server runs Lean # directly. PORT 8080 is lean4web's production default: editor + websocket. # `npm run prod` would only add nodemon (a dev dependency) around this same # `node index.mjs`, so the final image runs the server directly. ENV NODE_ENV=production \ NO_BWRAP=true \ PORT=8080 EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \ CMD curl -sf http://localhost:8080/health || exit 1 ENTRYPOINT ["/entrypoint.sh"] CMD ["node", "server/index.mjs"]