Search across all documentation pages
node_modules from the host can be the wrong architecture.Use a bind mount during active coding so a save is reflected immediately.
Rebuild the image whenever dependencies change, and always rebuild before you trust a result.
Never let a bind mount be the thing that "makes it work" - if the mounted version passes but the rebuilt image fails, the image is the truth.
A Compose file that mounts source for the inner loop while keeping dependencies inside the image.
services:
api:
build:
context: .
target: dev
command: npm run dev
ports:
- "3000:3000"
volumes:
- ./src:/app/src:ro
- /app/node_modules
env_file: .env.devThe paired Dockerfile has a dev stage that installs everything.
# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim AS dev
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]Two mount lines do the work.
./src:/app/src:ro maps your host source read-only so edits show up live but the container cannot scribble back into your tree.
/app/node_modules is an anonymous volume that shadows the host path, so the container keeps the node_modules it installed inside the image (correct OS and architecture) instead of your host's.
To verify what you ship, drop the mounts and build the real image.
docker build -t api:prod --target runtime .
docker run --rm -p 3000:3000 api:prodA bind mount is not a copy. The kernel maps the host path into the container's mount namespace.
Reads and writes go straight to the host filesystem, so there is no image layer for the mounted content. That is why edits are instant.
The trade-off is that the container no longer reflects the image alone. It reflects the image plus whatever your host currently has at that path.
docker build executes each Dockerfile instruction as a cached layer keyed by its inputs.
Order instructions cheap-to-expensive: copy the lockfiles and run npm ci before copying source, so a source-only edit does not bust the dependency layer.
COPY package.json package-lock.json ./
RUN npm ci # cached unless lockfile changes
COPY . . # busts only on source changeBuildKit (default in Docker 29) parallelizes independent stages and supports cache mounts, which keep a package manager cache warm across builds.
RUN --mount=type=cache,target=/root/.npm npm ciThe classic bug is mounting your whole project directory, which drags the host's node_modules over the image's.
Host modules can be built for a different OS or CPU architecture (native addons especially), so the app breaks in confusing ways.
The fix is the anonymous volume at /app/node_modules, which hides the host copy and preserves the image's install.
A bind-mounted dev container often runs dev dependencies, a non-slim base, and as root. Production runs the slim, non-root, read-only image.
Only the rebuilt production image exercises that reality, which is why the rebuild is the fidelity path.
A stale image layer can hide a broken dependency. If you changed the lockfile but only bind-mount source, the container still runs old modules; rebuild after any dependency change.
Mounting over node_modules without the anonymous volume drags in host binaries; add the /app/node_modules volume line.
Read-write bind mounts let the container modify your host tree - mount :ro unless the process genuinely needs to write back.
File-watch events can be missed on some host/VM filesystems, so hot reload silently stops; this is a watcher problem, covered in the hot-reload page, not a reason to abandon the mount.
"It works with the mount but not the image" means the image is missing a file or a build step; treat the image as authoritative and fix the Dockerfile.
Bind-mounted local paths do not exist on a Kubernetes node, so this pattern is a Docker/Compose inner-loop tool, not a production deployment mechanism.
Full rebuild every change. Highest fidelity, slowest loop. Reasonable for compiled languages with fast incremental builds, painful for interpreted ones.
Bind mount only source, deps in image. The balanced default shown above - fast edits, correct dependencies.
Sync-based tools (Tilt, Skaffold, docker compose watch). These copy changed files into the container or trigger targeted rebuilds, giving mount-like speed with rebuild-like fidelity.
services:
api:
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package-lock.jsondocker compose watch syncs source but rebuilds when the lockfile changes, encoding the exact trade-off this page is about.
Named volume for build artifacts. Cache compiled output across runs to speed rebuilds without sacrificing the image as source of truth.
Pick bind mounts when iteration speed dominates; pick rebuilds (or sync-with-rebuild) when you are validating dependencies, security posture, or the final artifact.
Why is my code change not showing up? Either the process is not watching files, or the change is in a path that is not bind-mounted. Confirm the mount target and the watcher.
Should I bind-mount node_modules? No. Let the image own it and shadow the host copy with an anonymous volume so architecture-specific binaries stay correct.
Are bind mounts slow on Mac and Windows? They can be, because the host filesystem is virtualized. Mount only the directories you edit, and consider sync-based tools for large trees.
Can I use bind mounts in production? No. A host path on a Kubernetes node is not your source tree; use a properly built image and, if needed, real volumes.
When must I rebuild? Any dependency or Dockerfile change, and always before trusting a pass as production-representative.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026