Bind Mounts and Live-Reload Development Workflows

Intermediate
11 min

Bind Mounts and Live-Reload Development Workflows

Volumes are the right tool for data that must outlive a container; bind mounts are the right tool for code you are editing right now. This lesson shows how to run an application inside a container while editing its source on the host, how to avoid the classic node_modules collision, how to keep file watching fast on Docker Desktop, and how Compose's watch mode can replace bind mounts entirely.

Bind Mounts vs Volumes

Both attach storage to a path inside the container; the difference is who owns the source.

| | Named volume | Bind mount | |---|---|---| | Source | Managed by Docker under its root directory | Any host path you choose | | Best for | Databases, uploads, caches | Source code, config files during development | | Portability | Same on every platform | Depends on the host's paths and permissions | | Performance on Desktop | Native | Crosses the VM boundary; slower for many small files |

bash
docker run --rm -it -v "$(pwd)":/app -w /app node:22-alpine npm test docker run --rm -it --mount type=bind,source="$(pwd)",target=/app,readonly node:22-alpine ls /app

The -v host:container short syntax and the explicit --mount syntax do the same thing; --mount fails loudly if the host path does not exist, whereas -v silently creates an empty directory, which is a frequent source of "my files are missing" confusion. Append :ro (or readonly) when the container should never write to the source.

The node_modules Problem

Mounting the whole project directory over /app hides everything the image put there, including the node_modules that npm ci installed during the build. The host's node_modules, if present, may contain native binaries compiled for Windows or macOS. The fix is an anonymous volume on the nested path, which takes precedence over the bind mount:

yaml
volumes: - ./:/app - /app/node_modules

Docker creates a volume for /app/node_modules, initialises it with the image's contents on first start, and keeps it across restarts. The same trick applies to Python's .venv, Ruby's vendor/bundle or any build output directory. Remember to run docker compose down -v (or docker compose up --renew-anon-volumes) after changing dependencies so the volume is rebuilt.

File Watching Across the VM Boundary

Dev servers such as nodemon, vite and uvicorn --reload rely on filesystem events. On Docker Desktop those events must cross from the host into the Linux VM. This works well when the project lives inside the WSL 2 filesystem on Windows or with VirtioFS on macOS. If changes are not detected, switch the watcher to polling:

bash
CHOKIDAR_USEPOLLING=true npm run dev # nodemon, webpack, vite

Polling costs CPU, so treat it as a fallback. On Linux hosts, inotify events pass through natively; a very large project may need sysctl fs.inotify.max_user_watches=524288.

Compose Watch: Sync Instead of Mount

Compose can replace the bind mount with a file-sync mechanism. develop.watch describes what to do when paths change:

yaml
services: api: build: . develop: watch: - action: sync # copy changed files into the running container path: ./src target: /app/src ignore: - "**/*.test.js" - action: rebuild # rebuild the image and recreate the container path: package.json - action: sync+restart # copy, then restart the service path: ./config target: /app/config
bash
docker compose up --watch # or: docker compose watch (in a second terminal)

Because the container's filesystem is the image's own, there is no node_modules collision and no cross-VM I/O penalty; only changed files are copied. Watch mode is the recommended workflow for Docker Desktop users and pairs naturally with the dev server's own hot reload.

Permissions on Linux

On a Linux host, a bind mount shares real UIDs. A container running as root creates root-owned files in your project directory, and a container running as UID 1000 cannot write to files you own if your UID differs. The simplest remedy is to run the container as your own user (or build the image with a matching ARG UID):

bash
docker run --rm -v "$(pwd)":/app -w /app --user "$(id -u):$(id -g)" node:22-alpine npm run build

Docker Desktop avoids this issue by mapping file ownership automatically.

Common Mistakes

  • Bind-mounting the project without the anonymous node_modules volume, then hitting "module not found" or wrong-platform binary errors.
  • Keeping the project on the Windows C: drive while running Docker Desktop with WSL 2; move it into the WSL filesystem.
Quick Quiz
Question 1 of 3

What does the second entry in `volumes: ["./:/app", "/app/node_modules"]` achieve?

Key Takeaways

  • Use named volumes for data and bind mounts for source code during development; prefer --mount for explicit errors.
  • Shadow dependency directories with an anonymous volume so the image's own node_modules is used.
  • Keep projects inside the WSL 2 filesystem and use polling only as a fallback for file watching.
  • Compose develop.watch with sync and rebuild actions replaces bind mounts and avoids cross-VM I/O.
  • On Linux hosts, run the container with your own UID or build the image with a matching user to avoid root-owned files.

Next lesson: Docker Networking — connect containers to each other and to the outside world.

Bind Mounts and Live-Reload Development Workflows - Docker | CodeYourCraft | CodeYourCraft