From 6a574043b10b21b04a1ed9ff59e964211ac2e2dc Mon Sep 17 00:00:00 2001 From: happy520ai <210128019+happy520ai@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:31:18 +0800 Subject: [PATCH 1/2] docs(BestPractices): warn that one node_modules tree cannot serve two architectures --- docs/BestPractices.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/BestPractices.md b/docs/BestPractices.md index 58217745fe..bf0d6640d3 100644 --- a/docs/BestPractices.md +++ b/docs/BestPractices.md @@ -15,6 +15,7 @@ - [Docker Run](#docker-run) - [Security](#security) - [node-gyp alpine](#node-gyp-alpine) +- [Multi-architecture images](#multi-architecture-images) - [Smaller images without npm/yarn](#smaller-images-without-npmyarn) @@ -180,6 +181,25 @@ FROM node:alpine as app COPY --from=builder node_modules . ``` +## Multi-architecture images + +A dependency tree installed once cannot serve two architectures. `docker buildx build` with +`--platform linux/amd64,linux/arm64` builds each stage natively per platform, but if one stage runs +`npm ci` on the build host and a later stage copies the result with +`COPY --from=builder node_modules .`, that same tree - carrying the build host's prebuilt native +addons - ends up inside every platform tag. The manifest still advertises both architectures, so nothing +looks wrong until an arm64 host loads an x86-64 binary: + +```console +Error: /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node: invalid ELF header +``` + +Install dependencies inside each platform's stage (or rebuild them there with `npm rebuild`), and +publish only the architectures actually built. To check an image that is already published, without a +Docker engine: fetch its manifest from the registry, decompress the layer tarballs, and read the +`e_machine` field of any `.node` file - two bytes at offset 18, `0x3e` for x86-64 and `0xb7` for +AArch64. + ## Smaller images without npm/yarn To remove npm and Yarn package managers, use a multi-stage build. From 98a01a751857dde014a6bd84c0189dc5810ed416 Mon Sep 17 00:00:00 2001 From: happy520ai <210128019+happy520ai@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:15:13 +0800 Subject: [PATCH 2/2] docs(BestPractices): move the warning into the node-gyp section, as reviewed Replaces the added 'Multi-architecture images' heading (and its doctoc entry) with a short note under the multistage example that already demonstrates the pattern - COPY --from=builder node_modules. Six lines instead of twenty, no new section to maintain. Validated with this repo's own checks: prettier --check, doctoc --update-only --dryrun. --- docs/BestPractices.md | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/docs/BestPractices.md b/docs/BestPractices.md index bf0d6640d3..563b21e087 100644 --- a/docs/BestPractices.md +++ b/docs/BestPractices.md @@ -15,7 +15,6 @@ - [Docker Run](#docker-run) - [Security](#security) - [node-gyp alpine](#node-gyp-alpine) -- [Multi-architecture images](#multi-architecture-images) - [Smaller images without npm/yarn](#smaller-images-without-npmyarn) @@ -181,24 +180,12 @@ FROM node:alpine as app COPY --from=builder node_modules . ``` -## Multi-architecture images - -A dependency tree installed once cannot serve two architectures. `docker buildx build` with -`--platform linux/amd64,linux/arm64` builds each stage natively per platform, but if one stage runs -`npm ci` on the build host and a later stage copies the result with -`COPY --from=builder node_modules .`, that same tree - carrying the build host's prebuilt native -addons - ends up inside every platform tag. The manifest still advertises both architectures, so nothing -looks wrong until an arm64 host loads an x86-64 binary: - -```console -Error: /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node: invalid ELF header -``` - -Install dependencies inside each platform's stage (or rebuild them there with `npm rebuild`), and -publish only the architectures actually built. To check an image that is already published, without a -Docker engine: fetch its manifest from the registry, decompress the layer tarballs, and read the -`e_machine` field of any `.node` file - two bytes at offset 18, `0x3e` for x86-64 and `0xb7` for -AArch64. +Note that this multistage pattern only produces a tree that works on the architecture the builder ran on. +Native addons are compiled or downloaded for that host, so `COPY --from=builder node_modules .` into a stage +built for a different architecture puts a mismatched binary in the image while the manifest still looks +correct, and the first `require()` of it fails with `invalid ELF header`. Under +`docker buildx build --platform linux/amd64,linux/arm64`, install or `npm rebuild` inside each platform's +stage instead of sharing one tree between them. ## Smaller images without npm/yarn