Link to the code that reproduces this issue
https://github.com/clintongreen/nextjs-sharp-standalone-repro/
To Reproduce
Minimal repro repo: https://github.com/clintongreen/nextjs-sharp-standalone-repro
git clone https://github.com/clintongreen/nextjs-sharp-standalone-repro
cd nextjs-sharp-standalone-repro
pnpm install
pnpm exec next build
node .next/standalone/server.js &
curl http://localhost:3000/api/sharp-test
Expected: sharp resize OK, 214 bytes. Actual: 500, with this in the server log:
⨯ Error: Failed to load external module sharp-...: Error: Could not load the "sharp" module using the <platform>-<arch> runtime
ERR_DLOPEN_FAILED: ... Library not loaded: @rpath/libvips-cpp.8.18.3.dylib (.so on Linux)
Reason: tried: '<...>/node_modules/.pnpm/@img+sharp-<platform>@0.35.3/node_modules/@img/sharp-<platform>/lib/../../sharp-libvips-<platform>/lib/libvips-cpp.8.18.3.dylib' (no such file), ...
Confirming the file is genuinely missing from the traced output:
find .next/standalone/node_modules/.pnpm -path '*sharp-libvips-*/lib' -exec ls -la {} \;
index.js and package.json are present (traced fine — they're referenced via normal require/exports); the actual shared library binary (~18MB, dlopen'd by the native addon rather than required) is not.
Important: this must be reproduced with pnpm, not npm. With plain npm install, the bug appears to not exist — npm also installs @img/sharp-wasm32 (that package declares no os/cpu restriction fields, so npm installs it on every platform unconditionally), and when the native binary's dlopen fails, sharp silently falls back to the WASM build instead of throwing, masking the failure entirely. pnpm does not install that fallback package for a platform it doesn't need, so there's no silent-degradation safety net — this matches our actual production setup (pnpm + Docker + Linux), where the failure was a hard crash, not a silent WASM fallback.
We reproduced the underlying issue consistently across multiple clean (--no-cache) Docker builds targeting linux/arm64 musl (our production target) as well as directly on macOS (darwin-arm64) with plain pnpm install/next build, no Docker involved — so this isn't platform- or container-specific, and it isn't specific to sharp as bundled into the server build either: we saw the identical failure whether sharp was left to be bundled normally or listed under serverExternalPackages — the latter avoids bundling the module into the webpack/turbopack server bundle, but doesn't change what the standalone output tracer decides to copy, so it doesn't fix this on its own.
Current vs. Expected behavior
Expected behavior
Either:
- The build fails or warns when a traced package can't resolve one of its declared runtime dependencies in the standalone output, or
- The tracer is able to detect native addons' runtime-loaded shared libraries (e.g. by reading the
binding.gyp/.node file's linked libraries, or via a documented convention packages can opt into), or
- The docs for
output: 'standalone' explicitly call out this class of failure (dynamically-loaded native libraries) and the outputFileTracingIncludes workaround, the way they already do for a handful of other known gaps.
Actual behavior
The build succeeds silently. The missing file only surfaces as a runtime crash in production, on the first request that exercises the code path needing the native library. There's no build-time signal that anything is wrong.
Provide environment information
- Next.js: 16.2.11
- `output: 'standalone'`
- Node.js: 24.x
- sharp: 0.35.3 (broke from 0.35.0 onward — earlier sharp releases, prior to the libvips packaging restructure, weren't affected)
- Reproduced on: linux/arm64 musl (Docker, our production target) and macOS darwin-arm64 (no Docker)
- Package manager: pnpm 11.9.0 (see the pnpm-vs-npm note above — this is load-bearing for reproduction)
Which area(s) are affected? (Select all that apply)
Output
Which stage(s) are affected? (Select all that apply)
next build (local), next start (local), Other (Deployed)
Additional context
Workaround
We worked around it with an explicit outputFileTracingIncludes glob targeting the missing files:
// next.config.js
outputFileTracingIncludes: {
'/**': ['./node_modules/.pnpm/@img+sharp-libvips-*/node_modules/@img/**/*.so*'],
},
This is pnpm-store-path-specific and fragile — it'll need adjusting if pnpm's virtual store layout changes, or if we switch package managers. We've also added our own build-time smoke test (actually require-ing and exercising sharp against the traced .next/standalone/node_modules output, so a regression fails the build loudly) since there's no way to guard against this class of bug in the general case using only Next's tooling.
For anyone else hitting this: we'd recommend applying the outputFileTracingIncludes workaround now rather than waiting on a fix here — this is a runtime crash in production, not a build-time inconvenience, and there's no guarantee of a timeline on this issue. Pair it with a build-time check that actually exercises the affected native module against the traced .next/standalone output (not just require()s it) — the failure mode is silent otherwise, and a version bump anywhere in the dependency chain (the native package itself, or pnpm's store layout) can reintroduce it without warning. Treat the workaround as permanent maintenance, not a stopgap, until this is addressed upstream.
Additional context
This isn't sharp-specific — it's a general limitation for any native addon that resolves its shared library at runtime rather than via static require. We searched for an existing issue covering this pattern and didn't find one, though we may have missed it.
Separately: if Next's own CI/test suite for output: 'standalone' + native addons runs under npm rather than pnpm, it may be passing for the wrong reason on cases like this one — worth checking, since the WASM-fallback masking described above means npm can hide exactly this class of bug.
Link to the code that reproduces this issue
https://github.com/clintongreen/nextjs-sharp-standalone-repro/
To Reproduce
Minimal repro repo: https://github.com/clintongreen/nextjs-sharp-standalone-repro
Expected:
sharp resize OK, 214 bytes. Actual:500, with this in the server log:Confirming the file is genuinely missing from the traced output:
index.jsandpackage.jsonare present (traced fine — they're referenced via normalrequire/exports); the actual shared library binary (~18MB,dlopen'd by the native addon rather thanrequired) is not.Important: this must be reproduced with pnpm, not npm. With plain
npm install, the bug appears to not exist — npm also installs@img/sharp-wasm32(that package declares noos/cpurestriction fields, so npm installs it on every platform unconditionally), and when the native binary'sdlopenfails, sharp silently falls back to the WASM build instead of throwing, masking the failure entirely. pnpm does not install that fallback package for a platform it doesn't need, so there's no silent-degradation safety net — this matches our actual production setup (pnpm + Docker + Linux), where the failure was a hard crash, not a silent WASM fallback.We reproduced the underlying issue consistently across multiple clean (
--no-cache) Docker builds targetinglinux/arm64musl (our production target) as well as directly on macOS (darwin-arm64) with plainpnpm install/next build, no Docker involved — so this isn't platform- or container-specific, and it isn't specific tosharpas bundled into the server build either: we saw the identical failure whethersharpwas left to be bundled normally or listed underserverExternalPackages— the latter avoids bundling the module into the webpack/turbopack server bundle, but doesn't change what the standalone output tracer decides to copy, so it doesn't fix this on its own.Current vs. Expected behavior
Expected behavior
Either:
binding.gyp/.nodefile's linked libraries, or via a documented convention packages can opt into), oroutput: 'standalone'explicitly call out this class of failure (dynamically-loaded native libraries) and theoutputFileTracingIncludesworkaround, the way they already do for a handful of other known gaps.Actual behavior
The build succeeds silently. The missing file only surfaces as a runtime crash in production, on the first request that exercises the code path needing the native library. There's no build-time signal that anything is wrong.
Provide environment information
Which area(s) are affected? (Select all that apply)
Output
Which stage(s) are affected? (Select all that apply)
next build (local), next start (local), Other (Deployed)
Additional context
Workaround
We worked around it with an explicit
outputFileTracingIncludesglob targeting the missing files:This is pnpm-store-path-specific and fragile — it'll need adjusting if pnpm's virtual store layout changes, or if we switch package managers. We've also added our own build-time smoke test (actually
require-ing and exercisingsharpagainst the traced.next/standalone/node_modulesoutput, so a regression fails the build loudly) since there's no way to guard against this class of bug in the general case using only Next's tooling.For anyone else hitting this: we'd recommend applying the
outputFileTracingIncludesworkaround now rather than waiting on a fix here — this is a runtime crash in production, not a build-time inconvenience, and there's no guarantee of a timeline on this issue. Pair it with a build-time check that actually exercises the affected native module against the traced.next/standaloneoutput (not justrequire()s it) — the failure mode is silent otherwise, and a version bump anywhere in the dependency chain (the native package itself, or pnpm's store layout) can reintroduce it without warning. Treat the workaround as permanent maintenance, not a stopgap, until this is addressed upstream.Additional context
This isn't sharp-specific — it's a general limitation for any native addon that resolves its shared library at runtime rather than via static
require. We searched for an existing issue covering this pattern and didn't find one, though we may have missed it.Separately: if Next's own CI/test suite for
output: 'standalone'+ native addons runs under npm rather than pnpm, it may be passing for the wrong reason on cases like this one — worth checking, since the WASM-fallback masking described above means npm can hide exactly this class of bug.