Complete Guide to Docker Volumes and Bind Mounts
Every self-hosted container you run — Jellyfin, Paperless-ngx, Nextcloud, your databases — needs to store data somewhere. Kill the container without persistent storage and everything’s gone.
Docker gives you two main options: volumes and bind mounts. Understanding when to use each is one of the most important skills for any self-hoster.
The Problem: Containers Are Ephemeral
By default, any data written inside a container lives in its writable layer. When the container is removed, that data vanishes. This is by design — containers are meant to be disposable.
For self-hosted services, you need data to survive container restarts, updates, and recreations. That’s where volumes and bind mounts come in.
Docker Volumes
A Docker volume is a storage unit managed entirely by Docker. It lives in Docker’s storage directory (typically /var/lib/docker/volumes/) and Docker handles the filesystem details.
Creating and Using Volumes
Create a named volume:
docker volume create my_data
Use it in a container:
docker run -d \
--name postgres \
-v my_data:/var/lib/postgresql/data \
postgres:16
In Docker Compose:
services:
postgres:
image: postgres:16
volumes:
- pg_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: changeme
volumes:
pg_data:
Managing Volumes
List all volumes:
docker volume ls
Inspect a volume:
docker volume inspect my_data
This shows you the mount point, creation date, and driver. The output looks like:
[
{
"CreatedAt": "2026-03-15T04:00:00-04:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/my_data/_data",
"Name": "my_data",
"Options": {},
"Scope": "local"
}
]
Remove unused volumes:
# Remove a specific volume
docker volume rm my_data
# Remove ALL unused volumes (careful!)
docker volume prune
Warning: docker volume prune removes every volume not attached to a running container. Always double-check before running this.
When to Use Volumes
- Databases (PostgreSQL, MariaDB, Redis) — volumes handle file locking and permissions better
- When you don’t need direct file access from the host
- Cross-platform setups — volumes work identically on Linux, macOS, and Windows
- When Docker should manage the storage lifecycle
Bind Mounts
A bind mount maps a specific host directory into the container. You control where the data lives on your filesystem.
Using Bind Mounts
docker run -d \
--name nginx \
-v /home/user/website:/usr/share/nginx/html:ro \
nginx:latest
In Docker Compose:
services:
nginx:
image: nginx:latest
volumes:
- ./website:/usr/share/nginx/html:ro
jellyfin:
image: jellyfin/jellyfin:latest
volumes:
- ./jellyfin/config:/config
- /mnt/media:/media:ro
The :ro Flag
Appending :ro makes the mount read-only inside the container. Use this for:
- Media libraries (Jellyfin, Plex reading your files)
- Static website content
- Configuration files you don’t want containers to modify
When to Use Bind Mounts
- Config files you want to edit directly (e.g.,
nginx.conf,docker-compose.ymlconfig directories) - Media libraries stored on specific drives or NAS mounts
- Development — live-reloading code changes
- Backups — when you want files in a predictable location for rsync/restic
Volumes vs Bind Mounts: The Comparison
| Feature | Volumes | Bind Mounts |
|---|---|---|
| Managed by | Docker | You |
| Location | /var/lib/docker/volumes/ | Anywhere on host |
| Direct host access | Requires docker volume inspect to find path | You choose the path |
| Permissions | Docker handles them | You manage them |
| Performance on Linux | Native | Native |
| Performance on macOS/Windows | Better (uses VM-native storage) | Slower (requires file sync) |
| Backup ease | Need to find mountpoint or use docker cp | Direct — just back up the directory |
| Compose portability | Higher (no host path assumptions) | Lower (paths must exist) |
Common Patterns for Self-Hosters
Pattern 1: Config in Bind Mounts, Data in Volumes
This is the most common and recommended approach:
services:
nextcloud:
image: nextcloud:latest
volumes:
- ./nextcloud/config:/var/www/html/config # bind mount — easy to edit/backup
- nextcloud_data:/var/www/html/data # volume — Docker manages it
volumes:
nextcloud_data:
Pattern 2: Everything in a Project Directory
Popular with self-hosters who want simple backups:
services:
paperless:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
volumes:
- ./paperless/data:/usr/src/paperless/data
- ./paperless/media:/usr/src/paperless/media
- ./paperless/export:/usr/src/paperless/export
- ./paperless/consume:/usr/src/paperless/consume
Backup the entire project directory and you’re done. Simple.
Pattern 3: Shared Media Mount
Multiple containers reading from the same media library:
services:
jellyfin:
image: jellyfin/jellyfin:latest
volumes:
- /mnt/media:/media:ro
- ./jellyfin/config:/config
radarr:
image: linuxserver/radarr:latest
volumes:
- /mnt/media/movies:/movies
- ./radarr/config:/config
sonarr:
image: linuxserver/sonarr:latest
volumes:
- /mnt/media/tv:/tv
- ./sonarr/config:/config
Permission Issues (And How to Fix Them)
The #1 frustration with bind mounts is permission errors. A container running as UID 1000 can’t write to a directory owned by root, and vice versa.
Quick Fixes
Check what user the container runs as:
docker exec <container> id
Set ownership to match:
# If container runs as UID 1000
sudo chown -R 1000:1000 ./my-app-data
Use PUID/PGID (LinuxServer.io images):
services:
sonarr:
image: linuxserver/sonarr:latest
environment:
- PUID=1000
- PGID=1000
volumes:
- ./sonarr:/config
Many popular self-hosting images from LinuxServer.io support PUID and PGID environment variables, which tell the container to run its process as your user. This is the cleanest solution.
Backing Up Volumes
Volumes aren’t as straightforward to back up as bind mounts. Here are your options:
Option 1: Docker cp
docker cp my_container:/var/lib/postgresql/data ./backup/
Option 2: Temporary Backup Container
docker run --rm \
-v my_data:/source:ro \
-v $(pwd)/backup:/backup \
alpine tar czf /backup/my_data.tar.gz -C /source .
Option 3: Find the Mountpoint
docker volume inspect my_data --format '{{ .Mountpoint }}'
# Then back up that directory directly (requires root)
For most self-hosters, using bind mounts in predictable locations and backing up those directories with rsync or restic is the simplest approach.
Troubleshooting
“Permission denied” on bind mount
# Check ownership
ls -la ./my-data/
# Fix it
sudo chown -R $(id -u):$(id -g) ./my-data/
Volume not showing data after container restart
Make sure you’re using the same volume name. Anonymous volumes (no name) create a new volume each time:
# Bad — anonymous volume, lost on recreate
docker run -v /data myimage
# Good — named volume, persists
docker run -v my_data:/data myimage
“Directory not empty” errors
Some containers expect empty directories on first run. If you’re bind-mounting into a path that has default files in the image, the bind mount hides them. Solution: let the container start once without the mount, copy the defaults out, then add the mount.
Dangling volumes eating disk space
# See how much space volumes use
docker system df
# Clean up unused volumes
docker volume prune
Best Practices
- Name your volumes — anonymous volumes are hard to track and easy to accidentally prune
- Use relative paths in Compose —
./config:/configis more portable than/home/user/apps/myapp/config:/config - Document your mount points — add comments in your
docker-compose.yml - Back up before
docker volume prune— always - Use
:rowhen containers only need to read — limits blast radius if a container is compromised - Test your backups — spin up a fresh container with your backup data and verify it works
Conclusion
For most self-hosting setups, the answer is straightforward:
- Bind mounts for config files, media libraries, and anything you want to easily access and back up
- Volumes for databases and internal application data where Docker can manage permissions
Pick a consistent pattern, document it, and make sure your backup strategy covers both. Your future self — frantically restoring after a drive failure at 2 AM — will thank you.
Running into storage issues with your self-hosted setup? Check out our Docker Security Best Practices and Automating Backups guides for the complete picture.