TL;DR — Quick Summary

Troubleshoot Docker volume issues including permission denied errors, data loss on container restart, bind mount problems, and orphaned volumes.

Understanding Docker Volumes

Docker containers are ephemeral by design. When a container is stopped and removed, all data written inside the container filesystem is lost. This is a feature, not a bug — it ensures containers are reproducible and stateless. But most real-world applications need persistent data: databases, uploaded files, configuration, logs.

Docker provides three mechanisms for data persistence:

  1. Named Volumes: Managed by Docker, stored in /var/lib/docker/volumes/. The recommended approach for most use cases.
  2. Bind Mounts: Map a specific host directory into the container. Useful for development but requires careful permission management.
  3. tmpfs Mounts: In-memory storage that is discarded when the container stops. Used for sensitive data that should never persist to disk.

This guide focuses on troubleshooting the most common volume-related problems: permission errors, data loss, orphaned volumes, and bind mount pitfalls.

Prerequisites

  • Docker Engine 20.10+ installed on Linux, macOS, or Windows with WSL2.
  • Shell access to the Docker host.
  • Basic familiarity with Docker CLI commands (docker run, docker inspect, docker volume).

Common Docker Volume Problems

1. Data Loss on Container Restart

Symptom: Your database or application data disappears every time you recreate the container.

Root Cause: No volume is mounted. Data is written to the container’s writable layer which is removed with the container.

Solution: Always mount a named volume for persistent data:

# Create a named volume
docker volume create myapp-data

# Run the container with the volume mounted
docker run -d \
  --name postgres-db \
  -v myapp-data:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:16

For Docker Compose, declare volumes explicitly:

services:
  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: secret

volumes:
  db-data:
    driver: local

Critical: If you use docker-compose down -v, the -v flag will delete all volumes defined in the compose file. Use docker-compose down (without -v) to keep your data.

2. Permission Denied Errors

Symptom: Container logs show Permission denied when trying to read or write files in a mounted volume.

Root Cause: The process inside the container runs as a specific UID (e.g., 999 for PostgreSQL, 1000 for Node.js) that does not match the ownership of the host directory.

Diagnostic steps:

# Check what user the container process runs as
docker exec mycontainer id

# Check ownership of the mounted path on the host
ls -lan /path/to/host/directory

# Check the mount point inside the container
docker exec mycontainer ls -lan /data

Solutions:

A) Use a named volume (Docker manages permissions automatically):

docker run -d -v myvolume:/data myimage

B) Match the UID with --user:

# Run as the current host user
docker run -d --user "$(id -u):$(id -g)" -v ./data:/data myimage

C) Fix ownership in the entrypoint:

# In your Dockerfile
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

# entrypoint.sh
#!/bin/sh
chown -R appuser:appgroup /data
exec su-exec appuser "$@"

D) Use POSIX ACLs on the host:

# Grant container UID access without changing ownership
setfacl -R -m u:999:rwx /path/to/host/directory
setfacl -R -d -m u:999:rwx /path/to/host/directory

3. Bind Mount Shows Empty Directory

Symptom: After mounting a host directory into a container, the container sees an empty directory even though the container image has files at that path.

Root Cause: Bind mounts overlay the container filesystem. The host directory replaces whatever was in the container image at that path.

Solution: Use named volumes instead. Docker volumes copy the image contents into the volume on first use:

# Named volume — image contents are preserved on first mount
docker run -d -v myvolume:/usr/share/nginx/html nginx

# Bind mount — host directory replaces image contents (empty if host dir is empty)
docker run -d -v ./empty-dir:/usr/share/nginx/html nginx  # → empty!

If you must use bind mounts, pre-populate the host directory:

# Extract image contents to host directory first
docker create --name temp nginx
docker cp temp:/usr/share/nginx/html/. ./my-html/
docker rm temp

# Now bind mount the populated directory
docker run -d -v ./my-html:/usr/share/nginx/html nginx

4. Orphaned Volumes Consuming Disk Space

Symptom: The Docker host runs out of disk space. /var/lib/docker/volumes is consuming gigabytes.

Diagnostic:

# List all volumes
docker volume ls

# Show volumes not referenced by any container
docker volume ls -f dangling=true

# Check total disk usage
docker system df

Solution:

# Remove all dangling (orphaned) volumes
docker volume prune

# Remove ALL unused volumes (including named ones not referenced)
docker volume prune -a

# Remove a specific volume
docker volume rm myoldvolume

Warning: docker volume prune -a will remove named volumes too if no container references them. Always verify with docker volume ls -f dangling=true first.

5. Volume Data Not Syncing on macOS/Windows

Symptom: File changes on the host are not reflected inside the container (or vice versa) when using bind mounts on macOS or Windows.

Root Cause: Docker Desktop runs containers inside a lightweight Linux VM. Bind mounts must pass through a filesystem sharing layer (gRPC-FUSE, VirtioFS, or osxfs) which introduces latency and synchronization delays.

Solutions:

  • Use VirtioFS (Docker Desktop 4.15+): In Docker Desktop settings, enable “VirtioFS” under the “General” tab. This is significantly faster than the legacy osxfs.
  • Use named volumes for large datasets: Named volumes live inside the VM and have native Linux I/O performance.
  • Use docker-compose watch (Compose 2.22+): Enables efficient file synchronization for development workflows.

Prevention and Best Practices

  • Always use named volumes for databases and stateful services. Never rely on bind mounts for production data persistence.
  • Never use docker-compose down -v in production. This deletes all volumes. Use down without -v.
  • Back up volumes regularly: Use docker run --rm -v myvolume:/data -v $(pwd):/backup busybox tar czf /backup/volume-backup.tar.gz -C /data .
  • Label your volumes: Use docker volume create --label env=production myvolume to organize and identify volumes.
  • Set read-only mounts where possible: Use :ro suffix (-v myvolume:/data:ro) for volumes that should not be written to, reducing the attack surface.
  • Monitor disk usage: Periodically run docker system df and set up alerts on /var/lib/docker disk usage.

Summary

  • Docker containers are ephemeral — always use volumes or bind mounts for persistent data.
  • Permission denied errors arise from UID mismatches between the container process and the mounted path.
  • Named volumes are preferred over bind mounts because Docker manages permissions and preserves image contents.
  • Clean up orphaned volumes with docker volume prune to recover disk space.
  • On macOS/Windows, use VirtioFS for better bind mount performance.