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.
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 |
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 /appThe -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.
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:
volumes:
- ./:/app
- /app/node_modulesDocker 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.
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:
CHOKIDAR_USEPOLLING=true npm run dev # nodemon, webpack, vitePolling 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 can replace the bind mount with a file-sync mechanism. develop.watch describes what to do when paths change:
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/configdocker 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.
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):
docker run --rm -v "$(pwd)":/app -w /app --user "$(id -u):$(id -g)" node:22-alpine npm run buildDocker Desktop avoids this issue by mapping file ownership automatically.
node_modules volume, then hitting "module not found" or wrong-platform binary errors.C: drive while running Docker Desktop with WSL 2; move it into the WSL filesystem.What does the second entry in `volumes: ["./:/app", "/app/node_modules"]` achieve?
--mount for explicit errors.node_modules is used.develop.watch with sync and rebuild actions replaces bind mounts and avoids cross-VM I/O.Next lesson: Docker Networking — connect containers to each other and to the outside world.