Use Bind Mounts for Dev
Learn to use bind mounts for development with Docker: mount host directories into containers for real-time code syncing, no rebuilds.
Focus: use bind mounts for development
You've built a Docker image, started a container, and maybe even written a Compose file. But every time you change one line of JavaScript, Python, or HTML, you rebuild the image and restart the container. In minutes, that friction kills your flow. Bind mounts solve that by letting your container see and edit files directly from your host — changes appear instantly, no rebuilds required.
The problem this lesson solves
Every Docker beginner hits the same wall: you edit your source code, save the file, and the running container still shows the old version. So you run docker build again, wait for layers to cache, then docker run or restart Compose. That loop eats seconds, then minutes, then hours over a project. During development, you don't need isolation — you need iteration speed.
Bind mounts fix this by breaking the wall between your host filesystem and the container filesystem. Instead of copying your code into the image (like COPY does in a Dockerfile), you tell Docker: "use this folder on my laptop as the container's folder." Any edit you make in your editor hits the container immediately.
Core concept / mental model
Think of a bind mount as a live portal between a directory on your host machine and a directory inside your container. What you change on one side instantly appears on the other. There is no copy step, no build step — just a direct mapping managed by the Docker engine.
Pro tip: Docker has two types of mounts — bind mounts and volumes. Volumes are Docker-managed and great for persistent data (databases). Bind mounts are host-path-managed and perfect for live code editing.
Key definitions
- Host path: the absolute or relative directory on your laptop/desktop (e.g.,
/Users/you/projector./app). - Container path: the directory inside the container where those files appear (e.g.,
/usr/src/app). - Mount flag:
--mount type=bind,source=<host-path>,target=<container-path>or the shorthand-v <host-path>:<container-path>.
Why use bind mounts for development?
| Without bind mount | With bind mount |
|---|---|
| Edit code → rebuild image → restart container → see change | Edit code → see change instantly |
| Image contains stale snapshot of code | Container always sees latest files |
| Slow iteration on UI, CSS, or config | Real-time feedback loop |
| Hard to test small tweaks | Tweak, save, refresh — done |
How it works step by step
- Start a container with a bind mount. When you run
docker run, add the--mountor-vflag pointing to your project directory. - Docker binds the host directory into the container at the container path you specify. This is a kernel-level operation — no copying, no syncing.
- Your container process reads and writes as if those files were always there. The same inode and file handles are shared.
- You edit files on your host. Your IDE’s save triggers a filesystem event that both host and container see.
- The container’s running application (web server, script, watcher) detects the changes and reloads — or you manually restart the process inside the container.
Important: If the container writes to a bind-mounted directory, those changes appear on your host too. That’s great for log files or generated output, but be careful — you don’t want a container accidentally deleting your source.
Running with -v (shorthand)
docker run -p 3000:3000 -v $(pwd):/app node:18-alpine node /app/server.js
Running with --mount (verbose, recommended for clarity)
docker run -p 3000:3000 --mount type=bind,source="$(pwd)",target=/app node:18-alpine node /app/server.js
Both commands do the same thing: mount the current directory ($(pwd)) into /app inside the container. Changes to server.js are live.
Hands-on walkthrough
Let's build a tiny Node.js app to see the use bind mounts for development flow in action.
1. Create a simple app
Create app.js on your host:
const http = require('http');
const server = http.createServer((req, res) => {
res.end('Hello from Docker!\n');
});
server.listen(3000, () => console.log('Running on port 3000'));
2. Run with a bind mount
docker run -d -p 3000:3000 --name dev-test -v $(pwd):/app node:18-alpine node /app/app.js
Test it: curl http://localhost:3000 → shows Hello from Docker!.
3. Edit the file live
Change 'Hello from Docker!' to 'Hello from bind mount!' in app.js on your host. Save the file.
4. See the change — no rebuild
curl http://localhost:3000
→ Hello from bind mount!
The running Node.js server picked up the new file because the bind mount reflects the host file immediately.
Cleanup
docker stop dev-test
docker rm dev-test
Compare options / when to choose what
| Mount type | Description | Best for |
|---|---|---|
| Bind mount | Maps a host path directly | Development — live code editing, config files |
| Volume | Docker-managed storage | Databases, persistent data you don't want on host |
| tmpfs mount | In-memory, temporary | Secrets that must not hit disk |
When to use bind mounts for development
- Framework apps (Node, React, Flask, Django) — edit, save, refresh.
- Config hot-reloading — mount
nginx.conf, restart nginx in the container. - Log collection — mount a host directory for container logs.
When not to use bind mounts
- Production — never mount your development code into a production container. Use
COPYin a Dockerfile for reproducible builds. - Windows/macOS performance — bind-mounted directories on non-Linux hosts can be slower due to filesystem translation. Use volumes for high-I/O scenarios.
Troubleshooting & edge cases
1. The container says "file not found"
Cause: The mount path inside the container doesn't exist, or you used an absolute host path that doesn't match.
Fix: Docker creates the destination directory if it doesn't exist, but only the last component. Ensure source is correct: -v /Users/you/project:/app works; /app is created inside.
Pro tip: Always use
$(pwd)or an absolute path for the source. Relative paths like-v ./app:/appwork from the shell’s current directory — but be explicit in scripts.
2. Changes not showing inside the container
Cause: The application pre-caches files or uses an older node process.
Fix: Use a file-watcher inside the container (like nodemon) or restart the process manually:
docker exec dev-test kill -HUP 1 # SIGHUP to PID 1, if your app supports it
3. Permission denied errors
Cause: The container process runs as a non-root user but the mounted files are owned by your host user (UID 1000).
Fix: Remap UIDs in the Dockerfile or run the container with --user matching your host UID:
docker run -u $(id -u):$(id -g) -v $(pwd):/app node:18-alpine node /app/app.js
4. Empty directory on container side
Cause: A volume or named mount shadows the bind mount path.
Fix: Check docker inspect <container> — look at Mounts list. Remove conflicting volumes.
What you learned & what's next
You now understand use bind mounts for development — how to map a host directory into a container for real-time file syncing without rebuilds. You can apply it to any framework: Node, Python, Go, React. The core idea is simple: bind mounts remove the build step from the edit-run cycle.
Key learning objectives covered: - Explain the core idea behind bind mounts for development. - Complete a practical exercise with a live-edited Node.js app.
Next lesson: Use named volumes for persistent data — you'll learn how volumes differ from bind mounts and why databases belong in Docker volumes, not bind mounts.
Practice recap
Try this: Create a simple Python Flask app with a single route. Run it in a container with a bind mount and the Flask reloader enabled. Edit the route's response — you should see the change on the next HTTP request without restarting the container. Then repeat with Docker Compose using the volumes key instead of -v.
Common mistakes
- Mounting a subdirectory (e.g.,
./src) but the container expects the root of the project — results in missing files likepackage.jsonorrequirements.txt. - Using
COPYin Dockerfile and also running with a bind mount — theCOPYfiles are overwritten by the mount, leading to confusion about which files are used. - Forgetting to stop the container before editing — the container is live but the app may cache old content. Use a file watcher or restart process.
- Running on Windows with Git Bash or PowerShell and using
$(pwd)which includes backslashes or spaces — quote the path or use%cd%in CMD.
Variations
- Use
docker-compose.ymlvolumes key:volumes: - ./app:/app— same bind mount behavior, declarative syntax. - Use read-only bind mounts:
--mount type=bind,source=$(pwd),target=/app,readonly— prevents accidental writes from the container to host. - Mount multiple source directories:
-v ./src:/app/src -v ./config:/app/config— useful for monorepos or split configuration.
Real-world use cases
- Frontend dev (React/Vue): mount project directory, run
npm run devinside container — HMR works because files are live. - Django/Flask app: mount project root, run development server with
--reload— Python picks up changes immediately. - API development with config: mount
nginx.confand static assets, edit configs, reload nginx viadocker exec— no rebuild per tweak.
Key takeaways
- Bind mounts map a host directory into a container — changes are immediate, no rebuilds needed.
- Use
--mount type=bind,source=...,target=...for clarity;-vshorthand works but can be ambiguous. - Bind mounts are for development only — use
COPYin Dockerfile for reproducible production images. - File watchers inside the container (nodemon, webpack-dev-server) still work because the kernel sees the host files.
- Permission issues are common — match container UID with host UID via
--userflag. - Always clean up:
docker rmafter stopping dev containers to prevent orphans.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.