

Sarthak Varshney is a Docker Captain, 5x C# Corner MVP, and 2x Alibaba Cloud MVP, with over six years of hands-on experience in the IT industry, specializing in cloud computing, DevOps, and modern application infrastructure. He is an Author and Associate Consultant, known for working extensively with cloud platforms and container-based technologies in real-world environments.
Picture this. You're learning Docker. You've got your first container running — maybe a little Node.js app, maybe a Python Flask server. You make a change to your code. You refresh the browser.
Nothing changes.
Of course it doesn't. Your code on the laptop and your code inside the container are two completely separate things. The container baked your files in at build time, like a photograph. Editing the original scene doesn't change the photo.
So what do you do? Rebuild the image. Wait. Re-run the container. Check the browser. Found another typo? Rebuild again. Wait again.
After the fifth time, you start questioning your life choices.
This is exactly the problem bind mounts solve. And once you get it, it'll feel like someone finally handed you the TV remote after you've been walking to the TV to change the channel for a week.
Let's keep this honest and simple.
A bind mount is when you tell Docker: "Hey, instead of using your own internal filesystem for this path, just use this folder that's sitting on my actual laptop."
That's it. You're mounting — or connecting — a folder from your host machine directly into the container. Whatever is in that folder on your machine is instantly visible inside the container. And vice versa — if the container writes something to that path, it shows up on your machine too.
Think of it like a shared Google Drive folder. Two people (you and the container) are looking at the exact same folder in real time. One person edits a file, the other sees it immediately. No uploading, no syncing, no delay.
The folder lives on your machine. The container just gets a window into it.
You'll hear about Docker Volumes a lot too, so let's quickly sort out the difference before things get confusing.
| Bind Mount | Docker Volume | |
|---|---|---|
| Where is the data? | On your host machine, at a path you choose | Managed by Docker, stored somewhere in Docker's own storage area |
| Who controls the path? | You do | Docker does |
| Best for? | Development — editing code live | Production — persistent databases, etc. |
| Can you browse the files? | Yes, just open the folder | Not directly (needs extra steps) |
For development, bind mounts are your best friend. For production databases or persistent app data, volumes are the better call. Today, we're firmly in the development camp.
Here's the flag you'll use:
docker run -v /path/on/your/machine:/path/inside/container image-name
Or using the newer, cleaner --mount syntax:
docker run --mount type=bind,source=/path/on/your/machine,target=/path/inside/container image-name
Both do the same thing. The -v style is shorter and you'll see it everywhere. The --mount style is more explicit and easier to read when things get complex.
Let's break down the -v flag specifically:
-v /path/on/your/machine:/path/inside/container
Left side = reality. Right side = container's view of reality.
Alright, enough theory. Let's build something real. We'll make a simple HTML web app that hot-reloads as you edit files — no rebuilds, no restarts.
A tiny web server (we'll use Nginx since it's dead simple) serving a static HTML page. You edit the HTML on your laptop. The browser updates automatically (or with a refresh — we'll add live reload too). The container never restarts.
Open your terminal and run:
mkdir docker-live-demo
cd docker-live-demo
mkdir app
Inside the app folder, create index.html:
touch app/index.html
Open it in your editor (VS Code, Vim, Notepad++ — whatever you use) and paste this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Docker Bind Mount Demo</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: #1a1a2e;
color: #eee;
}
h1 { font-size: 3rem; color: #e94560; }
p { font-size: 1.2rem; color: #aaa; }
</style>
</head>
<body>
<div>
<h1>🐳 Hello, Docker!</h1>
<p>Edit this file. Refresh the browser. Magic.</p>
</div>
</body>
</html>
Now here's the money command:
docker run -d \
--name live-demo \
-p 8080:80 \
-v $(pwd)/app:/usr/share/nginx/html \
nginx:alpine
Let's read this like a sentence:
docker run -d → Run this container in the background (detached mode)--name live-demo → Give it a friendly name so we're not juggling container IDs-p 8080:80 → Forward port 8080 on your laptop to port 80 inside the container (where Nginx listens)-v $(pwd)/app:/usr/share/nginx/html → THIS IS THE MAGIC. Mount our app folder directly into Nginx's web rootnginx:alpine → Use the lightweight Alpine-based Nginx imageThe $(pwd) is a shell trick that gives you the current directory's full path automatically. So if you're in /Users/sarthak/docker-live-demo, it becomes /Users/sarthak/docker-live-demo/app:/usr/share/nginx/html.
Go to http://localhost:8080
You should see your dark-themed page with the Docker whale emoji and "Hello, Docker!"
Go back to app/index.html on your machine. Change the heading text:
<h1>🚀 Bind Mounts Are Awesome!</h1>
Save the file. Now go back to the browser and hit refresh.
Boom. Updated. No rebuilds. No docker stop. No docker start. You just edited a file on your laptop and the container served it instantly.
That's what bind mounts do.
Refreshing manually is fine, but since we're here, let's go one step further and add actual live reload using a small tool called browser-sync.
We'll add a second container that watches our files and auto-refreshes the browser.
docker-compose.ymltouch docker-compose.yml
Paste this:
version: "3.8"
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./app:/usr/share/nginx/html
livereload:
image: node:18-alpine
working_dir: /app
volumes:
- ./app:/app
ports:
- "3000:3000"
- "3001:3001"
command: sh -c "npm install -g browser-sync && browser-sync start --server --files '**/*' --port 3000 --ui-port 3001"
docker compose up -d
Go to http://localhost:3000
Browser-sync serves and watches the same ./app folder. Now edit your HTML, save — and watch the browser refresh itself automatically. No F5 needed.
You've just set up a two-container live reload dev environment using bind mounts. In a Docker Compose file. From scratch.
Not bad for a beginner, right?
Sometimes things don't work and you want to check what's actually mounted. Here's how:
docker inspect live-demo
Scroll through the JSON output and look for the Mounts section. You'll see something like:
"Mounts": [
{
"Type": "bind",
"Source": "/Users/sarthak/docker-live-demo/app",
"Destination": "/usr/share/nginx/html",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
]
That "RW": true means the mount is read-write — the container can both read from and write to your folder. If you want to make it read-only (for security), add :ro to your -v flag:
-v $(pwd)/app:/usr/share/nginx/html:ro
Now the container can serve files but can't modify them. Good practice when your container doesn't need to write back.
$(pwd)# WRONG — Docker doesn't understand relative paths like this
-v ./app:/usr/share/nginx/html
# RIGHT — Give it the full absolute path
-v $(pwd)/app:/usr/share/nginx/html
On Linux and Mac, $(pwd) in the terminal gives you the absolute path automatically. On Windows with CMD, use %cd% instead. On PowerShell, use ${PWD}.
If you try to bind mount a path that doesn't exist on your host, Docker creates it — but it creates it as a directory, not a file. So if you were trying to mount a config file:
# You forgot to create nginx.conf first
-v $(pwd)/nginx.conf:/etc/nginx/nginx.conf
Docker creates a nginx.conf folder on your machine instead of a file. Then Nginx inside the container sees a directory where it expects a config file and crashes. Always make sure the source path actually exists before mounting.
This is a double-edged sword. Bind mounts are live, which means the moment you save a broken file, the container serves the broken version. There's no buffer. If you're halfway through editing and accidentally save, the error shows up immediately. That's the trade-off for the convenience.
On Linux specifically, the user inside the container (often root or a specific app user) might not have permission to read your host files. You might see 403 Forbidden or permission denied errors.
Quick fix — check who owns the files:
ls -la app/
And make sure the container user can read them. Usually chmod 644 on files and chmod 755 on directories sorts it out.
If you're on Windows and mounting shell scripts or config files, the Windows-style line endings (CRLF) can cause Linux containers to misread them and fail mysteriously. Use a .editorconfig or configure your editor to save files with Unix-style line endings (LF) when working on Docker projects.
By default, bind mounts are read-write. That means your container can create, edit, and delete files in your mounted folder. For a dev server that's fine — you want that freedom.
But sometimes you're mounting something sensitive, like a config file or secrets. In those cases, make it read-only:
-v $(pwd)/config:/etc/myapp/config:ro
Rule of thumb:
Here's a small challenge to cement what you've learned. Don't skip this — actually doing it is worth more than reading ten articles.
my-portfoliohtml subfolder with at least three files: index.html, about.html, and style.css./html to Nginx's web rootstyle.css — change the background color — refresh and see it update livecontact.html while the container is running. Navigate to /contact.html in the browser. Does it show up without restarting the container?Bonus challenge: Add a :ro read-only flag to the mount. Then try creating a file from inside the container using docker exec. What happens?
docker exec -it your-container-name sh
# Inside the container:
touch /usr/share/nginx/html/test.txt
Did it work? What error did you get? Why? Understanding that error will teach you more about bind mounts than any paragraph I can write.
# Basic bind mount
docker run -v /host/path:/container/path image
# Bind mount (read-only)
docker run -v /host/path:/container/path:ro image
# Using --mount syntax (cleaner for complex setups)
docker run --mount type=bind,source=/host/path,target=/container/path image
# Check what's mounted in a running container
docker inspect container-name
# Run the live reload Nginx example
docker run -d \
--name live-demo \
-p 8080:80 \
-v $(pwd)/app:/usr/share/nginx/html \
nginx:alpine
# Stop and remove the container
docker stop live-demo && docker rm live-demo
Bind mounts are one of those Docker features that, once you understand them, you use every single day during development. They bridge the gap between your comfortable local editing setup and the isolated, reproducible environment that containers give you.
You get the best of both worlds — your favourite editor, your machine's file system, your keyboard shortcuts — and the container handles the runtime, the dependencies, the OS environment. No more "but it works on my machine" when you can actually run it on something that looks nothing like your machine.
The workflow we built today — edit code locally, serve from container, see changes immediately — is the foundation of pretty much every Docker-based dev environment you'll encounter in real projects. Node apps, Python APIs, static sites, documentation servers, you name it.
Next time you're setting up a project with Docker, before you reach for that docker build command after every small edit, ask yourself: can I just mount a folder here instead? Nine times out of ten, you can.
Happy shipping. 🐳