From 409e3dfe6fec28295487a297f32c8bc159eb0e5b Mon Sep 17 00:00:00 2001 From: Declan Carroll Date: Thu, 13 Aug 2026 08:45:57 +0000 Subject: [PATCH] build: Cache the isolated-vm native compile across application changes (#36065) Co-authored-by: Claude Opus 5 (1M context) --- docker/images/n8n/Dockerfile | 55 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index 799364ac63f..798b366fc9b 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -1,34 +1,35 @@ ARG NODE_VERSION=24.18.1 ARG N8N_VERSION=snapshot -# Builder stage exists because the runtime base image has no toolchain. -# Pinned to multi-arch index digest (linux/amd64 + linux/arm64) for reproducible builds. -# Bump the digest together with the tag when updating the base image. -# Digest pins to node:24.18.1-alpine3.24 (Node 24.18.1, Alpine 3.24). -FROM node:24.18.1-alpine3.24@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS builder -COPY ./compiled /usr/local/lib/node_modules/n8n -RUN apk add --no-cache python3 make g++ && \ - cd /usr/local/lib/node_modules/n8n && \ - npm rebuild sqlite3 && \ - # Rebuild isolated-vm from source. node-gyp-build's musl detection relies - # on /etc/alpine-release which DHI Alpine omits, so the bundled prebuilt - # loader picks the glibc binary and segfaults in musl pthread paths. - # Removing prebuilds and invoking node-gyp directly avoids npm rebuild's - # double-build behavior (built-in node-gyp + install-script node-gyp) - # which races on dep-file state. - # Use npm's bundled node-gyp rather than `npx node-gyp` so the toolchain - # version is fixed by the pinned image digest instead of being fetched - # from the registry at build time. - rm -rf node_modules/isolated-vm/prebuilds && \ - cd node_modules/isolated-vm && \ +# The runtime base has no compiler, so this stage supplies one. +# The digest pins node:24.18.1-alpine3.24 for amd64 and arm64. Change the tag and the digest together. +FROM node:24.18.1-alpine3.24@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS toolchain +RUN apk add --no-cache python3 make g++ + +# Build isolated-vm in a separate stage. The only cache input is the module source. +# Below `COPY ./compiled`, each application change starts a new compile. +FROM toolchain AS native-builder +# `node_modules/isolated-vm` is a pnpm symlink, so copy the target directory. +# The wildcard tracks the version in the catalog. +COPY ./compiled/node_modules/.pnpm/isolated-vm@*/node_modules/isolated-vm /build/isolated-vm +WORKDIR /build/isolated-vm +# node-gyp-build reads /etc/alpine-release to detect musl. DHI Alpine has no such file, +# so the loader takes the glibc prebuild and crashes. Build from source instead. +# Call node-gyp directly: `npm rebuild` starts two builds that conflict, and +# `npx node-gyp` downloads a different version at build time. +RUN rm -rf prebuilds && \ node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild --release -j max -# Pinned to multi-arch index digest (linux/amd64 + linux/arm64) for reproducible builds. -# n8nio/base is republished by a separate workflow (docker/images/n8n-base) on every -# base change, so a base rebuild does not reach this image until the digest is -# manually re-pinned here. Bump the digest together with the tag whenever the base -# image is intentionally updated. -# Digest pins to n8nio/base:24.18.1 (Node 24.18.1, Alpine 3.24). +FROM toolchain AS builder +COPY ./compiled /usr/local/lib/node_modules/n8n +RUN cd /usr/local/lib/node_modules/n8n && npm rebuild sqlite3 +COPY --from=native-builder /build/isolated-vm/build/Release/isolated_vm.node \ + /usr/local/lib/node_modules/n8n/node_modules/isolated-vm/build/Release/isolated_vm.node +# node-gyp-build finds build/Release first. Delete the prebuilds to remove the glibc fallback. +RUN rm -rf /usr/local/lib/node_modules/n8n/node_modules/isolated-vm/prebuilds + +# The digest pins n8nio/base:24.18.1 for amd64 and arm64. A separate workflow builds that +# image, so a new base reaches this image only when you change the digest here. FROM n8nio/base:24.18.1@sha256:f7a23e449f093b8b203467919e969ca35d12ba959442aadc1f69bb174b160ad2 ARG N8N_VERSION @@ -42,7 +43,7 @@ WORKDIR /home/node COPY --from=builder /usr/local/lib/node_modules/n8n /usr/local/lib/node_modules/n8n COPY docker/images/n8n/docker-entrypoint.sh / -# The Alpine 3.24 base has no /usr/local/bin (node lives in /usr/bin); create it for the symlink. +# The base keeps node in /usr/bin and has no /usr/local/bin. Make that directory for the symlink. RUN mkdir -p /usr/local/bin && \ ln -s /usr/local/lib/node_modules/n8n/bin/n8n /usr/local/bin/n8n && \ mkdir -p /home/node/.n8n && \