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.yml config 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

FeatureVolumesBind Mounts
Managed byDockerYou
Location/var/lib/docker/volumes/Anywhere on host
Direct host accessRequires docker volume inspect to find pathYou choose the path
PermissionsDocker handles themYou manage them
Performance on LinuxNativeNative
Performance on macOS/WindowsBetter (uses VM-native storage)Slower (requires file sync)
Backup easeNeed to find mountpoint or use docker cpDirect — just back up the directory
Compose portabilityHigher (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

  1. Name your volumes — anonymous volumes are hard to track and easy to accidentally prune
  2. Use relative paths in Compose./config:/config is more portable than /home/user/apps/myapp/config:/config
  3. Document your mount points — add comments in your docker-compose.yml
  4. Back up before docker volume prune — always
  5. Use :ro when containers only need to read — limits blast radius if a container is compromised
  6. 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.