Bind Mounts vs Image Rebuild
Summary
- A bind mount maps a host directory into a running container so code edits appear instantly without rebuilding.
- Insight: bind mounts optimize the inner loop for speed; image rebuilds optimize for fidelity to what you actually ship.
- Key Concepts: bind mount, image layer cache, BuildKit, inner loop vs outer loop, dev/prod parity.
- When to Use: bind mounts for fast iteration on source; rebuilds for verifying the production artifact and dependency changes.
- Limitations: bind mounts hide build-time bugs and can mask missing files; a mounted
node_modulesfrom the host can be the wrong architecture.
Recipe
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.
Working Example
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:prodDeep Dive
How a bind mount changes the container
A 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.
How a rebuild uses the layer cache
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 node_modules shadow pattern
The 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.
Where fidelity actually lives
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.
Gotchas
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.
Alternatives
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.
FAQs
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.
Related
- Local Dev Basics
- Hot Reload in Containers
- Dev Containers
- Dev/Prod Parity as a Mental Model
- Local Dev Best Practices
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).