One of the most common frustrations when starting with Docker is losing all your data every time a container is removed or recreated. This happens because Docker containers are ephemeral by design — their writable layer is created with the container and destroyed along with it. Docker Volumes solve this problem by providing a storage mechanism that exists entirely outside the container lifecycle. Whether you're running a MySQL database, storing user uploads, or persisting application configuration, Volumes ensure your data survives container restarts, updates, and replacements. This guide covers everything you need to know about Docker Volumes, from basic concepts to production-ready backup strategies, all tested on Ubuntu VPS environments.

Understanding Docker's Layered Filesystem

Before diving into Volumes, it helps to understand how Docker manages storage internally. Every Docker image is built from a stack of read-only layers using a Union Filesystem (UnionFS). When a container is started from an image, Docker adds a thin writable layer on top of all the image layers. Any file you create, modify, or delete inside a running container goes into this writable layer — the underlying image layers are never changed.

The critical issue is that this writable layer is tightly coupled to the specific container instance. When you run docker rm or docker-compose down, the writable layer is permanently deleted along with the container. This means any data written inside the container — such as database files, uploaded assets, or generated logs — is gone forever. Docker provides three mechanisms for persisting data beyond the container lifecycle: Volumes, Bind Mounts, and tmpfs Mounts.

Docker Volumes Explained

A Docker Volume is a dedicated storage directory managed by the Docker Engine, completely separate from the container lifecycle. Volume data is stored on the host at /var/lib/docker/volumes/<volume-name>/_data, but Docker owns and manages this path — not the user directly. This distinction provides several important advantages over other storage approaches.

Because Volumes are managed by Docker, they are the recommended storage mechanism for production workloads. You can delete and recreate containers freely without touching the Volume data. Multiple containers can mount the same Volume simultaneously. Volumes can be backed up, restored, and migrated with standard Docker tooling, and they support Volume Drivers that extend storage to remote backends like NFS, AWS EBS, or Azure Disk.

Volume vs Bind Mount vs tmpfs: Comparison Table

Choosing the right storage type depends on your use case. Here is a quick comparison to guide your decision:

Type Storage Location Persistence Best For
Named Volume /var/lib/docker/volumes/ Permanent (manual delete) Databases, user uploads, production data
Bind Mount Any path on host Permanent (host-dependent) Development, live code reloading, config files
tmpfs Mount RAM only (no disk write) Temporary (lost on stop) Session caches, temp data, secrets in memory
Anonymous Volume /var/lib/docker/volumes/ (random ID) Permanent but hard to track Not recommended for production

Named Volumes are the default recommendation for production use. Bind Mounts work well in development environments where you want to mount your local source code into a container and have changes reflected immediately without rebuilding the image.

Creating and Managing Volumes — Essential Commands

Here are the core Docker CLI commands every developer working with Volumes should know:

# Create a new Named Volume
docker volume create myapp_data

# List all Volumes on this host
docker volume ls

# Inspect a Volume (shows mount path, driver, labels)
docker volume inspect myapp_data

# Remove a specific Volume (container must not be using it)
docker volume rm myapp_data

# Remove all unused Volumes (prune)
docker volume prune

# Remove Volumes together with containers on compose down
docker-compose down -v

Sample output from docker volume inspect:

[
    {
        "CreatedAt": "2026-06-09T10:00:00+07:00",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/myapp_data/_data",
        "Name": "myapp_data",
        "Options": {},
        "Scope": "local"
    }
]

Mounting Volumes with docker run and Docker Compose

There are two primary ways to attach a Volume to a container: using the -v or --mount flags with docker run, or declaring them in a docker-compose.yml file.

Option 1 — docker run with the -v flag

# Syntax: -v <volume-name>:<path-inside-container>
docker run -d \
  --name mysql_db \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=myapp \
  -v mysql_data:/var/lib/mysql \
  mysql:8.0

# Bind Mount — maps a specific host directory into the container
docker run -d \
  --name nginx_web \
  -v /home/user/website:/usr/share/nginx/html:ro \
  -p 80:80 \
  nginx:alpine

Option 2 — docker-compose.yml (recommended for production)

version: '3.8'

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  app:
    image: myapp:latest
    depends_on:
      - db
    volumes:
      - app_uploads:/app/uploads
      - app_logs:/app/logs
    ports:
      - "8000:8000"

# Declare Named Volumes at the top level
volumes:
  postgres_data:
  app_uploads:
  app_logs:

Declaring Volumes at the top level of docker-compose.yml instructs Docker to create them automatically if they do not exist yet. Running docker-compose down without the -v flag will stop and remove containers while leaving all Volumes intact. This is the intended behavior for production deployments — containers are ephemeral, data is not.

Backing Up and Restoring Docker Volumes

No storage solution is complete without a tested backup strategy. The standard Docker approach is to spin up a temporary container that mounts the Volume and writes a compressed archive to the host.

Creating a Volume Backup

# Backup the postgres_data Volume to the current directory
docker run --rm \
  -v postgres_data:/source:ro \
  -v $(pwd):/backup \
  alpine \
  tar czf /backup/postgres_data_$(date +%Y%m%d).tar.gz -C /source .

# Verify the backup file
ls -lh postgres_data_*.tar.gz

Restoring a Volume from Backup

# Create a new Volume for the restore target
docker volume create postgres_data_restore

# Extract the archive into the new Volume
docker run --rm \
  -v postgres_data_restore:/target \
  -v $(pwd):/backup \
  alpine \
  sh -c "cd /target && tar xzf /backup/postgres_data_20260609.tar.gz"

Database-Specific Backup (Recommended for PostgreSQL and MySQL)

# PostgreSQL — pg_dump produces a consistent logical backup
docker exec postgres_container pg_dump -U myuser mydb \
  > backup_$(date +%Y%m%d).sql

# MySQL — mysqldump
docker exec mysql_container mysqldump \
  -u root -psecret myapp \
  > mysql_backup_$(date +%Y%m%d).sql

# Restore PostgreSQL
cat backup_20260609.sql | docker exec -i postgres_container \
  psql -U myuser mydb

Production Tip: Never back up a running database by taring its raw data files. The result may be an inconsistent snapshot that cannot be restored cleanly. Always use the database's own dump tools — pg_dump for PostgreSQL, mysqldump for MySQL, and mongodump for MongoDB. These tools are designed to produce consistent backups while the database is still accepting connections. Set up a nightly cron job and test your restore procedure at least once a month.

Sharing Volumes Between Multiple Containers

One of the most powerful features of Docker Volumes is the ability to mount the same Volume into multiple containers simultaneously. This is useful in microservice architectures where services need to read from or write to a shared file store.

version: '3.8'

services:
  # Application server writes uploaded files
  app:
    image: myapp:latest
    volumes:
      - shared_uploads:/app/uploads

  # Nginx reads uploads to serve as static files
  nginx:
    image: nginx:alpine
    volumes:
      - shared_uploads:/usr/share/nginx/html/uploads:ro
    ports:
      - "80:80"

  # Background worker processes uploaded files
  worker:
    image: myworker:latest
    volumes:
      - shared_uploads:/worker/input:ro

volumes:
  shared_uploads:

In this example, all three containers share the shared_uploads Volume. Nginx and the worker both mount it read-only (:ro) to prevent accidental writes. Applying :ro to any container that only needs read access is a security best practice that reduces the blast radius of a potential compromise or misconfiguration.

Handling Volume Permissions on Linux

Permission issues are among the most common problems when using Volumes on Linux. The UID/GID of the process running inside a container may not match the ownership of the Volume directory on the host, resulting in "Permission denied" errors.

# Check ownership of a Volume on the host
ls -la /var/lib/docker/volumes/myapp_data/_data

# Fix permissions in the Dockerfile
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
RUN chown -R appuser:appgroup /app
USER appuser

# Or specify the user at runtime
docker run -d \
  --user 1000:1000 \
  -v myapp_data:/app/data \
  myapp:latest

# In docker-compose.yml
services:
  app:
    image: myapp:latest
    user: "1000:1000"
    volumes:
      - myapp_data:/app/data

Official Docker images for databases like MySQL, PostgreSQL, and MongoDB handle permission setup automatically in their entrypoint scripts. If you are building your own images, always define a non-root user in your Dockerfile and ensure the Volume mount point is owned by that user before the USER directive.

Production Best Practices for Docker Volumes

Running Docker Volumes on a production VPS requires discipline around naming, backup, and monitoring to avoid unpleasant surprises.

# Automated nightly backup cron job (add via crontab -e)
# Runs at 02:00 every night, keeps 30 days of backups
0 2 * * * docker run --rm \
  -v myapp_postgres_data:/source:ro \
  -v /backup/docker:/backup \
  alpine \
  tar czf /backup/postgres_$(date +\%Y\%m\%d).tar.gz \
  -C /source . && \
  find /backup/docker -name "postgres_*.tar.gz" -mtime +30 -delete

Frequently Asked Questions

What is the difference between a Docker Volume and a Bind Mount?

Docker Volumes are managed entirely by the Docker Engine and stored under /var/lib/docker/volumes/, making them portable and easy to migrate. Bind Mounts tie a container path directly to a specific path on the host filesystem, which is great for development workflows where you want live code reloading. For production workloads, Named Volumes are recommended because Docker handles their lifecycle, permissions, and they are host-path agnostic.

Will data in a Docker Volume be lost when a container is deleted?

No. Volumes have a lifecycle completely independent from containers. Running docker rm -f or docker-compose down will remove the container but leave the Named Volume intact. Data is only deleted when you explicitly run docker volume rm or docker volume prune. This makes it safe to recreate or update containers without risking data loss, though regular backups are still strongly recommended.

How do I back up data from a Docker Volume?

The standard approach is to spin up a temporary Alpine container that mounts the volume and creates a tar archive: docker run --rm -v myvolume:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz -C /data . This produces a backup.tar.gz in your current directory. For PostgreSQL or MySQL, use pg_dump or mysqldump respectively instead of raw file backups to ensure data consistency.

Should I use a Volume or tmpfs for temporary data in a container?

If the data does not need to survive container restarts and you want maximum speed, use a tmpfs mount since it stores data in RAM without any disk I/O. If losing data on restart is unacceptable, use a Named Volume. tmpfs is ideal for session caches and regenerable temp files, while Volumes are the right choice for databases, user uploads, and configuration files that must persist across restarts.

High-Performance KVM VPS by AsiaGB

Full root access VPS ready for Docker, MySQL, Python, Node.js and more. Starting from 500 THB/month with 99% uptime.

View VPS Plans

View all affordable VPS Thailand plans →