Bind Mounts vs Volumes
Containers are disposable, but many applications still need files that survive container removal. Docker gives you two main ways to attach persistent storage: bind mounts, which map a specific host path, and volumes, which Docker creates and manages for you.
The difference matters because storage choices affect portability, backups, permissions, development workflow, and whether data is accidentally tied to one machine path. Use bind mounts when you intentionally want a real host directory inside a container; use named volumes as the usual default for application data such as databases.
Overview: How Bind Mounts And Volumes Work
A container starts from an image made of read-only layers. Docker then adds a thin writable container layer on top. If a process writes to an ordinary path in the container, that change goes into the writable layer and disappears when the container is removed with docker rm.
Mounts replace that behavior for selected paths. Instead of writing into the container layer, Docker connects a path inside the container to storage outside the container layer. The mounted path can outlive the container, can be shared with another container, and can be backed up separately.
A bind mount maps an exact path from the host into the container. For example, $PWD/site on your machine can appear as /usr/share/nginx/html inside an Nginx container. Edits on the host appear in the container immediately, and writes from the container appear on the host. This is excellent for local development, configuration files, logs you want to inspect directly, and cases where the host path is part of the requirement.
A named volume is created and managed by Docker. You refer to it by name, such as postgres-data, and Docker stores the data under its storage area. On Linux Engine this is normally somewhere below /var/lib/docker/volumes. On Docker Desktop, that storage lives inside Docker Desktop’s Linux VM, not directly in your macOS or Windows project folder. You should treat the Docker CLI as the interface instead of editing the internal storage path by hand.
Volumes are more portable across hosts because the application only needs the volume name and container target path, not a hard-coded host directory. They are also less likely to inherit surprising host permissions or file watching behavior. For databases and other persistent service state, named volumes are usually the right default.
Both mount types can hide existing image files at the target path. If the image contains files in /app/data and you mount an empty host directory or volume there, the mounted storage is what the container sees. The original image files are not deleted, but they are obscured until you start a container without that mount.
Syntax
The modern and explicit form is --mount:
docker run --mount type=bind,source=/host/path,target=/container/path IMAGE
docker run --mount type=volume,source=volume-name,target=/container/path IMAGE
| Part | Meaning |
|---|---|
type=bind |
Mounts a specific host file or directory into the container. |
type=volume |
Mounts a Docker-managed named volume into the container. |
source |
For bind mounts, the host path. For volumes, the volume name. |
target |
The path where the mount appears inside the container. |
readonly |
Optional. Makes the mount read-only inside the container. |
-v or --volume |
Older shorthand: host-or-volume:container-path[:options]. It is common, but --mount is clearer for teaching and scripts. |
Compose uses similar ideas with YAML:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: changeme
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
Examples
Example 1: Bind mount a local web directory
Create a small local directory and serve it with Nginx. The host directory is mounted read-only so the web server can read the files but cannot change them:
mkdir -p "$PWD/site"
printf '<h1>Hello from a bind mount</h1>\n' > "$PWD/site/index.html"
docker run --name bind-nginx --mount type=bind,source="$PWD/site",target=/usr/share/nginx/html,readonly -p 8080:80 -d nginx:1.27-alpine
Output:
f2a7c2c0d9f8b2b1e2a44c5b98b5fd2e66cb5c2d4d85f8e9b674bbd5a2f3b111
Docker starts Nginx and maps your exact site directory over the image’s default web root. If you edit site/index.html on the host and refresh the browser at http://localhost:8080, Nginx sees the new file immediately. The port is published by -p 8080:80; mounting files does not publish ports by itself.
Example 2: Use a named volume for database data
A database should usually write to a Docker-managed named volume instead of a random project folder:
docker volume create postgres-data
docker run --name volume-postgres -e POSTGRES_PASSWORD=changeme --mount type=volume,source=postgres-data,target=/var/lib/postgresql/data -d postgres:16-alpine
Output:
postgres-data
9d89d6c5b3f1a03d5edc67c7abce42d8c53b6e04b1b3d4785a8e1a8b3dd0e456
The first command creates the volume. The second starts PostgreSQL and attaches that volume at the directory where PostgreSQL stores its database files. If the container is stopped and removed, the volume remains until you remove it with docker volume rm postgres-data.
Example 3: Recreate a container and keep the volume
Remove the database container, then start a replacement using the same named volume:
docker rm -f volume-postgres
docker run --name volume-postgres-2 -e POSTGRES_PASSWORD=changeme -v postgres-data:/var/lib/postgresql/data -d postgres:16-alpine
Output:
volume-postgres
0b5f4b0d2a3c5d0b39c145f4df98f6f8d6a6e311cf72a2f2a2f44d4c77f1c900
The container changed, but the volume name did not. Docker attaches the existing postgres-data volume, so the database files are still available. This example uses the shorter -v syntax because you will see it often: the part before the first colon is the volume name, and the part after it is the container path.
Example 4: The same idea in Compose
For multi-container projects, Compose keeps mount declarations next to the service definition:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: changeme
volumes:
- postgres-data:/var/lib/postgresql/data
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./site:/usr/share/nginx/html:ro
volumes:
postgres-data:
Run it:
docker compose up -d
Output:
[+] Running 3/3
✔ Volume "myapp_postgres-data" Created
✔ Container myapp-db-1 Started
✔ Container myapp-web-1 Started
Compose creates a project-scoped named volume for PostgreSQL and bind mounts the relative ./site directory into Nginx. The :ro suffix makes the web content read-only inside the container.
How It Works Step By Step
- The Docker CLI sends the requested mount configuration to the Docker daemon when you create the container.
- For a bind mount, the daemon resolves the host path. The path must exist for the explicit
--mount type=bindform. With the short-vform, Docker may create a missing host directory, which can hide mistakes. - For a named volume, Docker creates or finds the volume under Docker’s managed storage area.
- Docker prepares the container filesystem from the image’s read-only layers plus the thin writable container layer.
- Before the process starts, Docker attaches the mount at the target path. Anything from the image at that target is hidden behind the mount for this container.
- Reads and writes below the target path go to the bind mount or volume, not the container writable layer.
- When the container is removed, bind mount data remains at the host path and named volume data remains in Docker’s volume store.
Common Mistakes
Using a bind mount for production database state
This can work on one laptop, but it bakes a host-specific path into your deployment:
docker run --name bad-db -e POSTGRES_PASSWORD=changeme -v "$PWD/db-data":/var/lib/postgresql/data -d postgres:16-alpine
The container now depends on the current directory, host permissions, backup process, and filesystem behavior. A named volume is usually safer and clearer:
docker run --name good-db -e POSTGRES_PASSWORD=changeme -v postgres-data:/var/lib/postgresql/data -d postgres:16-alpine
Mounting over important image files
This command hides Nginx’s built-in web root with the current directory:
docker run --name hidden-web -v "$PWD":/usr/share/nginx/html -d nginx:1.27-alpine
If the current directory is empty or wrong, Nginx may serve nothing even though the image originally had default files. Fix the source path and mount only the directory you intend, preferably read-only for static content:
docker run --name clear-web -v "$PWD/site":/usr/share/nginx/html:ro -d nginx:1.27-alpine
Expecting container removal to delete volumes
docker rm removes the container, not named volumes. This is normally good because your data survives, but it surprises people during cleanup. Use docker volume ls to inspect volumes and remove a volume explicitly only when you are sure the data is no longer needed.
Best Practices
- Use named volumes as the default for persistent application state, especially databases.
- Use bind mounts for local development source code, configuration files, and host paths you intentionally want to share.
- Prefer
--mountin documentation and scripts because it names each field clearly. - Use read-only mounts with
readonlyor:rowhenever the container only needs to read files. - Do not mount over broad paths such as
/,/usr, or your whole project unless that is truly required. - Give volumes meaningful names such as
postgres-datainstead of relying only on anonymous volumes. - Back up important named volumes through Docker-aware backup commands or a temporary helper container, not by editing Docker’s internal storage directly.
- Remember Docker Desktop stores Linux container volumes inside its VM, so host filesystem paths and volume storage are not the same thing.
- Keep secrets out of images and ordinary bind-mounted project files; use mounted secret files or your orchestrator’s secret mechanism for real credentials.
Practice Exercises
- Create a local
sitedirectory, bind mount it into Nginx as read-only, and confirm that editingindex.htmlchanges what the browser receives. - Run PostgreSQL with a named volume, remove the container, and start a replacement container with the same volume. The expected result is that the database directory is reused.
- Write a Compose file with one service that uses a named volume and one service that uses a bind mount. Hint: database data and editable web files need different storage choices.
Summary
- Container writable layers are temporary; mounts keep selected data outside that layer.
- Bind mounts connect a specific host path to a container path.
- Named volumes are created, stored, and managed by Docker.
- Volumes are the recommended default for persistent service data.
- Bind mounts are ideal for local development and intentional host-file sharing.
- Mounts can hide files that were present in the image at the same target path.
- Removing a container does not remove named volumes unless you explicitly remove the volume.
