Fix Docker Compose 'Exec format error' loading a library
Your app crashes on docker-compose up while it works fine outside Docker — here is why a native module refuses to load, and the three fixes that actually remove it.
TL;DR#
docker-compose up crashes with Error loading shared library ...: Exec format error when a
dependency ships a compiled .node binary that was built outside the container — typically
because node_modules was installed on the host (macOS, Windows, or a different Linux) and then
copied into the image unchanged. The fix is to make sure every native binding gets compiled
inside the Linux container: delete node_modules and reinstall there, stop it from ever
being copied in with .dockerignore, and if a package has no Linux build at all, replace it.
The error#
This is the exact failure from the original report (Stack Overflow #45549193), a Node.js app that depends on the natural package, which in turn pulls in webworker-threads:
web_1 | /app/web-server/node_modules/natural/lib/natural/classifiers/classifier.js:32
web_1 | if (e.code !== 'MODULE_NOT_FOUND') throw e;
web_1 | ^
web_1 |
web_1 | Error: Error loading shared library /app/web-server/node_modules/webworker-threads/build/Release/WebWorkerThreads.node: Exec format error
web_1 | at Error (native)
web_1 | at Object.Module._extensions..node (module.js:597:18)
web_1 | at Module.load (module.js:487:32)
web_1 | at tryModuleLoad (module.js:446:12)
web_1 | at Function.Module._load (module.js:438:3)
web_1 | at Module.require (module.js:497:17)
web_1 | at require (internal/module.js:20:19)
web_1 | at bindings (/app/web-server/node_modules/bindings/bindings.js:81:44)
web_1 | at Object.<anonymous> (/app/web-server/node_modules/webworker-threads/index.js:1:105)WebWorkerThreads.node is a compiled native addon, not JavaScript. The stack trace shows it
being pulled in through bindings.js, the small helper most native Node modules use to locate
their compiled .node file at require() time.
Why it happens#
A .node file is a shared library (an .so on Linux, a .dylib on macOS, a .dll-wrapped
binary on Windows) that Node loads with its internal process.dlopen(). Like any shared
library, it is built for one combination of operating system, CPU architecture and C library
(glibc vs musl on Alpine) — it is not portable the way plain JavaScript is.
The accepted answer on the Stack Overflow thread names exactly this: "It sounds like it's trying
to load a native extension that wasn't compiled for linux (in the container) — maybe you
previously installed the extension on OS X and it's trying to load that binary", pointing to a
matching report in the dinghy issue tracker.
If node_modules is installed once on a developer's host and then COPY-ed, bind-mounted, or
otherwise carried unchanged into the Linux container, every native binding it contains keeps the
binary built for the host, and the container's dynamic loader refuses to run it.
Reproducing the loading mechanism without needing the exact same native package: a .node file
is loaded with Node's process.dlopen(), which hands off to the platform's dynamic linker just
like loading any other shared library. Compiling the same trivial C source once for linux/arm64
and once for linux/amd64 (via docker run --platform, Docker Desktop, 8 October 2026) and
require()-ing each .node file
from inside an amd64 node:20-bookworm-slim container isolates the failure to the loading
step itself: the arm64 build fails immediately with
Error: /app/arm64addon.node: cannot open shared object file: No such file or directory (on
musl/Alpine the wording differs — Error relocating ...: unsupported relocation type 1026 — but
it is the same dynamic-loader rejection), while the matching amd64 build passes that same
loading step and only fails later, for an unrelated reason (the stub never calls
NODE_MODULE(), so Node correctly reports "Module did not self-register"). Changing nothing but
the build platform moves the failure from the loading stage to a later, architecture-unrelated
one — which is the mechanism the accepted answer describes for webworker-threads.
Fix#
1. Rebuild node_modules inside the container, not on the host#
The fix that resolved the original report was simply removing the host-built node_modules and
letting the container reinstall it: Running npm install inside the build can surface a different failure on node:20.3.0 images, where esbuild's install script stops with Text file busy, and the esbuild spawnSync ETXTBSY fix for Docker images covers moving to a newer image or setting UV_USE_IO_URING=0 before the install.
# inside the Dockerfile, or once in a running container while debugging
rm -rf node_modules
npm installIn a Dockerfile, that means npm install (or npm ci) has to run as a RUN step that
executes inside the build, after COPY package*.json ./, and before any COPY . . that
could carry a host-built node_modules over it:
FROM node:20-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server/Server.js"]Then rebuild without the Docker layer cache for that step:
docker compose build --no-cache
docker compose up2. Stop the host's node_modules from ever reaching the image#
Even with the Dockerfile above, a node_modules folder sitting in the build context next to the
Dockerfile still gets uploaded and can shadow files during COPY . . if the order of the two
COPY instructions is wrong, or if a later step copies more than the Dockerfile lists. The
second answer on the thread is the direct fix: add a .dockerignore file next to the Dockerfile.
/node_modulesThis guarantees the host's compiled binaries are never part of the build context in the first
place, so the only node_modules that can exist in the image is the one npm install built for
it.
3. Replace a package that has no usable native build for your base image#
Sometimes the native dependency itself is the dead end. One answerer on the thread reports that
no amount of rebuilding fixed it, because the real problem was a specific package, bcrypt, that
needed its native binding recompiled for their particular base image; switching to the pure-JS
bcryptjs removed the native binding entirely:
npm uninstall bcrypt
npm install bcryptjsThis is a last resort, not a first step — bcryptjs is slower than the compiled bcrypt and the
two libraries are not always drop-in API matches. Try steps 1 and 2 first; reach for a pure-JS
replacement only when the image genuinely has no working toolchain to rebuild the native binding
(a minimal distroless-style runtime image, for example, or a package whose native build has
been abandoned upstream).
Verify the fix#
docker compose upThe container should start the application without the Error loading shared library stack
trace. If it still appears after steps 1 and 2 above, check the docker-compose.yml for a
volumes entry that bind-mounts the project root over the app directory, such as
volumes: ['.:/app']. Docker's own documentation on bind mounts
states plainly: "If you bind mount a file or directory into a directory in the container in
which files already exist, the pre-existing files are obscured by the mount." That means a bind
mount of the whole project directory re-obscures the freshly-built node_modules inside the
image with the host's folder at container start — undoing fix 1 every single time
docker-compose up runs, even though the image itself was built correctly. The common way
around it, without giving up the bind mount for live-reload during development, is adding a named
anonymous volume for node_modules specifically, so it is excluded from the bind mount and keeps
the one built inside the image:
services:
web:
build: .
volumes:
- .:/app
- /app/node_modulesOne email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Variants worth knowing#
The same error string shows up for a different reason once the whole image — not just one
package inside it — is built for the wrong CPU architecture, typically an arm64 image (built
on an Apple Silicon Mac) run on an amd64 host or task. That failure mode, and the
docker buildx --platform fix for it, is covered separately —
worth checking first if the error appears on the entrypoint itself rather than from inside a
require() call.
Two other native-binding errors follow the same root cause as this one and are worth knowing if
natural/webworker-threads is not the package involved: Node Sass's
"could not find a binding for your current environment",
and Rollup/Vite's
"Cannot find module @rollup/rollup-win32-x64-msvc",
which is the same mismatch running the other direction (a Linux-built node_modules committed or
synced onto a Windows machine). In both, the fix is the same first move: delete node_modules
and reinstall it on the machine — or container — that actually runs the code.
A docker-compose up that fights a native binding on every rebuild is rarely a one-off: it is a
sign the project's Dockerfile and its .dockerignore disagree about where node_modules
should come from. Fixing the Dockerfile's COPY order once, and adding the .dockerignore line,
removes the whole class of error for every dependency that ships a compiled binary, not only the
one that happened to surface it first.
Further reading: Fix RabbitMQ ECONNREFUSED 5672 between Docker containers. Also see Fix Docker: Client Version Is Too Old.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix Docker: Client Version Is Too Old
Separate an outdated Docker client from a pinned API or the wrong context, and check the minimum your exact Engine release accepts.
Fix Docker Compose environment 'unexpected type map' error
See the three wordings this error has had across Compose releases, fix the line in either valid form, and avoid the quoted look-alike that hides the error but loses the variable.
Fix Docker Compose "unknown flag: --iidfile" Build Error
The flag Compose added is real; the CLI parsing it is the part that is too old. Find which docker binary your build actually runs, then fix that one.


