Breaking
Fix Docker node_modules not found in a Node.js container

Fix Docker node_modules not found in a Node.js container

The image has node_modules. Then your compose bind mount drops your host folder on top of it and hides them. The build was never the problem — the volume was.

How this was written

Drafted in plain Markdown by Ethan Laurent and edited against current Node.js, framework and tooling docs. Every command, code block and benchmark in this article was run on Node.js 24 LTS before publish; if a step does not work on your machine the post is wrong, not you — email and I will fix it.

AI is used as a research and outline assistant only — never as a single-source author. Full editorial policy: About / How nodewire is written.

Tested on Node.js 24 LTS · last reviewed June 2026

The build log is clear: npm ci ran, 400 packages added, no errors. The image built. Then docker compose up starts the container and it dies instantly with Cannot find module 'express' — a package you can see sitting in your package.json. You exec into the container, run ls node_modules, and it’s either empty or it’s missing exactly the things you need. The Docker node_modules not found problem feels like a build failure, but the build almost always worked. Something hid node_modules after the build, at runtime, and that something is usually a volume you added for convenience.

Quick fix: preserve node_modules in Docker Compose

The most common cause is a bind mount in docker-compose.yml.:/app — that mounts your host project directory over the container’s /app, so the bind mount hides or overwrites the node_modules the image built. Fix it by adding an anonymous volume for node_modules that re-exposes the container’s own copy on top of the bind mount:

YAML
services:
  api:
    build: .
    volumes:
      - .:/app
      - /app/node_modules   # keep the image's node_modules, don't let the host hide it

This pattern is mainly for local development with hot reload. In production, do not bind mount your whole project into /app — build the image with dependencies inside it and run the container straight from that image, no source mount at all.

The two other causes: a missing .dockerignore that copies your host’s node_modules (often built for the wrong OS/CPU) into the image, and installing dependencies in a different working directory than the one you run from. All three are below.

Here’s the error, the one that sent you searching:

text
Error: Cannot find module 'express'
Require stack:
- /app/index.js
    at Function._resolveFilename (node:internal/modules/cjs/loader:1248:15)
    code: 'MODULE_NOT_FOUND'

Common symptoms

You’re probably hitting this exact issue if:

  • npm ci completed during docker build, but docker compose up fails on startup.
  • /app/node_modules is missing or empty inside the running container (Docker Compose node_modules empty).
  • Cannot find module 'express' appears only in Docker — the same project works locally but fails in a Linux container.
  • The error shows up as “Docker Cannot find module after npm install” even though the build logs say the install succeeded.
  • Native packages like bcrypt, sharp, or sqlite3 fail to load after host node_modules got copied in.

Cause 1: Docker Compose bind mount hides node_modules

This is the big one. In development you mount your source into the container so edits show up without a rebuild:

YAML
volumes:
  - .:/app

That bind mount makes the container’s /app be your host folder. Whatever the image built into /app/node_modules is now hidden behind your host’s /app/node_modules — and if your host doesn’t have one (or has one built for macOS while the container is Linux), the container sees nothing usable. The build was perfect. The mount covered it up a second later.

The fix is an anonymous volume for node_modules. Docker layers it on top of the bind mount, so the container keeps the node_modules it built while still seeing your live source for everything else:

YAML
services:
  api:
    build: .
    volumes:
      - .:/app
      - /app/node_modules

The order matters less than the presence of that second line. With it, editing src/index.ts on your host still hot-reloads, but node_modules belongs to the container. This is the single most common fix, and it’s the same class of “it built fine, then runtime disagreed” gotcha behind a lot of plain Cannot find module reports.

On macOS and Windows, keeping node_modules inside the container is also faster and cleaner than relying on the host copy, because Docker Desktop runs Linux containers through a VM and syncs bind-mounted files across that boundary. For larger projects a named volume for node_modules can be more predictable than host filesystem sync.

Cause 2: host node_modules copied without .dockerignore

If your Dockerfile does COPY . . and you have no .dockerignore, Docker copies your host’s node_modules into the image. That’s the wrong-architecture node_modules in Docker problem: those packages were compiled for your laptop — macOS arm64, say — and the container is Linux. Native modules (bcrypt, sharp, anything with a .node binary) then fail with cryptic load errors, or the copy is partial and modules go missing. Always ship a .dockerignore:

text
# .dockerignore
node_modules
npm-debug.log
.git
dist

With node_modules ignored, the COPY brings in only your source and lockfile, and the RUN npm ci inside the image builds the packages for the container’s platform. Clean, correct, and smaller. The full layered setup is in the Node.js Docker guide, and Docker’s own Node.js language guide covers the same .dockerignore discipline.

Cause 3: npm ci and CMD run from different WORKDIR

If you npm ci in one directory and CMD runs from another, Node looks for node_modules in the wrong place. Keep a single WORKDIR and install there:

Dockerfile
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "index.js"]

COPY package*.json ./ before the rest is also what lets Docker cache the install layer, so you don’t reinstall on every source change — a deploy speed win that matters more on a real server, as covered in deploying Node.js to a VPS.

How to check why node_modules is empty in Docker

Exec into the running container and look:

bash
docker compose exec api ls -la /app/node_modules | head

Empty or missing? It’s the bind mount (Cause 1) — add the anonymous volume. Present but a native module throws on load? It’s the wrong-architecture copy (Cause 2) — add .dockerignore and rebuild with --no-cache. Present in a different folder than your CMD runs from? It’s WORKDIR (Cause 3). Two minutes of looking saves an hour of guessing.

When to rebuild and renew the node_modules volume

The anonymous-volume fix has one trap: it works the first time, then breaks after you change package.json. If you add a new dependency and Docker still says Cannot find module, the anonymous /app/node_modules volume is holding the old dependency tree — Compose recreates containers on up but reuses existing volumes by default. Rebuild the image and renew the anonymous volume:

bash
docker compose up --build -V       # -V = --renew-anon-volumes

Or reset the project’s volumes entirely:

bash
docker compose down -v
docker compose up --build

Use this only for local development volumes you can safely recreate. Do not run down -v blindly on a Compose project that stores database data in named volumes — it deletes those too.

FAQ

Why is node_modules empty in my Docker container even though npm install ran?

A bind mount in docker-compose.yml (.:/app) is mounting your host directory over the container’s /app, hiding the node_modules the image built. Add an anonymous volume - /app/node_modules after the bind mount so the container keeps its own node_modules.

What does the anonymous volume /app/node_modules do?

It tells Docker to keep the container’s node_modules directory as a separate volume layered on top of the bind mount, so your host folder mount doesn’t hide it. You still get live source edits from the bind mount, but node_modules stays the one the image built for the container’s platform.

Why does this come back after I add a new dependency?

The anonymous /app/node_modules volume persists across docker compose up, so it keeps the old dependency tree even after you change package.json. Rebuild and renew it with docker compose up --build -V (or docker compose down -v then up --build) so the new dependencies land in the volume.

Should I add node_modules to .dockerignore?

Yes. Without it, COPY . . copies your host’s node_modules — often built for a different OS or CPU — into the image, causing native modules to fail or files to go missing. Ignore it and let RUN npm ci build packages inside the image for the container’s platform.

Why do native modules like bcrypt fail in Docker but work locally?

Because the node_modules was built for your host (e.g. macOS arm64) and copied into a Linux container. Native .node binaries are platform-specific. Add node_modules to .dockerignore and run npm ci inside the Dockerfile so the binaries are compiled for the container.

How do I keep hot reload but not lose node_modules in Docker Compose?

Use two volumes: .:/app for live source and /app/node_modules (anonymous) to preserve the container’s modules. The first gives you reload on file changes; the second stops the bind mount from hiding the installed packages.

Why does Cannot find module appear only in Docker and not on my machine?

Your machine has a populated node_modules; the container either had it hidden by a bind mount, copied a wrong-platform version, or installed it in a different WORKDIR than it runs from. For the broader, non-Docker version of this error, see the Cannot find module guide.