The core idea
A bind mount maps a specific folder (or file) on your machine to a path in the container. It isn't a copy: both sides see the same files, and a change on one side shows up on the other immediately. That makes it ideal for development and for handing a container config files. It's a poor fit for production data, which belongs in volumes.
Live editing
$ mkdir site && echo '<h1>v1</h1>' > site/index.html$ docker run -d --name live -p 8086:80 -v "$PWD/site":/usr/share/nginx/html:ro nginx:alpine$ curl localhost:8086<h1>v1</h1>$ echo '<h1>v2, edited on the host</h1>' > site/index.html$ curl localhost:8086<h1>v2, edited on the host</h1>
No rebuild, no restart. :ro stops the container from writing back:
$ docker exec live sh -c 'echo x > /usr/share/nginx/html/new.html'sh: can't create /usr/share/nginx/html/new.html: Read-only file system
The source must be an absolute path ("$PWD/site", not site). A bare name like site would be read as a volume
name instead.
Trap 1: the mount hides what was there
A bind mount replaces the whole directory. Whatever the image had at that path becomes invisible:
$ docker run --rm -v "$PWD/site":/usr/share/nginx/html nginx:alpine ls /usr/share/nginx/htmlindex.html
nginx's own 50x.html is gone; only your folder's contents show. The classic case is mounting a Node project over
/app for development: the image's node_modules disappears behind your folder, which has none (or has packages built
for macOS):
$ docker run --rm notes-dev ls /appdist node_modules package-lock.json package.json src tsconfig.json$ docker run --rm -v "$PWD":/app notes-dev ls /apppackage-lock.json package.json src tsconfig.json$ docker run --rm -v "$PWD":/app -v /app/node_modules notes-dev ls /appnode_modules package-lock.json package.json src tsconfig.json
(Output joined onto one line per command.) The fix in the last line adds an anonymous volume at /app/node_modules.
The more specific mount wins, so the image's node_modules shows through again (it's an empty volume, so it gets
pre-filled; see 04.02 Volumes). Docker also creates an empty node_modules folder in your project as the mount
point.
Trap 2: missing paths
The two syntaxes disagree about a host path that doesn't exist:
$ docker run --rm -v "$PWD/does-not-exist":/data alpine true$ ls -d does-not-existdoes-not-exist$ docker run --rm --mount type=bind,src="$PWD/does-not-exist",dst=/data alpine truedocker: Error response from daemon: invalid mount config for type "bind": bind source path does not exist: ...
-v quietly creates an empty directory, which is how a typo becomes "my config file is empty". --mount refuses.
In scripts, prefer --mount.
Trap 3: who owns the files
A container running as root that writes into a bind mount creates files owned by UID 0. On a Linux host, those
files are then owned by root on your machine too, and you need sudo to delete them. Run the container as your own
user to avoid it:
docker run --rm -u "$(id -u):$(id -g)" -v "$PWD":/work -w /work node:22-slim npm run buildDocker Desktop on macOS and Windows hides this: its file sharing maps ownership to your user, so the same file shows as yours on the Mac. Code that works on your laptop can still leave root-owned files on a Linux CI runner.
You start a dev container with -v ./src:/app/src, but there's a typo and the folder is actually ./scr. The app starts and can't find any source files. Why?
Serve a folder with nginx as above and edit it live. Then reproduce trap 1 with the notes API's build stage (docker build --target build -t notes-dev .) and fix it with the extra -v /app/node_modules. Finally try the --mount version with a typo and read the error.
STUCK? ▸
Changes don't show up: some editors replace files instead of writing to them, and a bind-mounted single file keeps pointing at the old one. Mount the folder, not the file.
Recap
Interview questions
When would you use a bind mount instead of a volume?
Your dev container mounts the project into /app and now the app can't find its dependencies. What happened?