Skip to content

output: standalone file tracing misses native libraries loaded via dlopen (e.g. sharp's libvips binary) #97973

Description

@clintongreen

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    OutputRelated to the the output configuration option.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions