Search…

Docker volumes and storage

In this series (8 parts)
  1. How containers work
  2. Docker fundamentals
  3. Writing production-quality Dockerfiles
  4. Docker networking
  5. Docker volumes and storage
  6. Docker Compose
  7. Container image security
  8. Docker in CI/CD pipelines

When a container is removed, its writable layer disappears with it. Every file created inside that container is gone. This is by design. Containers are meant to be disposable. But databases, uploads, and configuration files need to survive restarts. Docker provides three storage mechanisms to solve this: bind mounts, named volumes, and tmpfs mounts.

The three storage types

Each type maps differently to the host filesystem.

graph TB
  subgraph Host["Host Machine"]
      FS["Host Filesystem<br/>/home/user/app"]
      VOL["Docker-managed<br/>/var/lib/docker/volumes/"]
      RAM["Host Memory (RAM)"]
  end
  subgraph Container["Container"]
      BM["/app<br/>(bind mount)"]
      NV["/data<br/>(named volume)"]
      TM["/run/secrets<br/>(tmpfs)"]
  end
  FS --> BM
  VOL --> NV
  RAM --> TM

How Docker’s three storage types map to the host.

Bind mounts point directly to a path on the host. You control exactly where the data lives. The container sees whatever is at that host path, and changes flow both ways.

Named volumes are managed by Docker. Docker picks the storage location (usually under /var/lib/docker/volumes/). You reference them by name, not by path.

tmpfs mounts live entirely in memory. Nothing is written to disk. When the container stops, the data is gone.

Bind mounts

Bind mounts are the simplest option. You specify a host path and a container path.

docker run -d \
  --name devserver \
  -v /home/user/project:/app \
  -w /app \
  node:20 npm run dev

The container sees your project files at /app. Edit a file on the host and the container picks up the change instantly. This makes bind mounts ideal for local development.

You can also make them read-only:

docker run -d \
  -v /home/user/config/nginx.conf:/etc/nginx/nginx.conf:ro \
  nginx:latest

The :ro flag prevents the container from modifying the file. Useful for configuration you want to inject but protect.

Caveats. Bind mounts depend on the host directory structure. They break portability. If the host path does not exist, Docker creates it as an empty directory owned by root, which is rarely what you want.

Named volumes

Named volumes are Docker’s recommended storage mechanism for persistent data.

Creating and inspecting volumes

docker volume create pgdata

docker volume inspect pgdata

The inspect command returns the mount point, driver, and labels. By default, volumes use the local driver and live under /var/lib/docker/volumes/pgdata/_data.

Using a named volume

docker run -d \
  --name postgres \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:16

The database files persist in the pgdata volume. Remove the container, start a new one with the same volume, and the data is still there.

Volume lifecycle commands

# List all volumes
docker volume ls

# Remove a specific volume
docker volume rm pgdata

# Remove all unused volumes
docker volume prune

# Remove all unused volumes without confirmation
docker volume prune -f

The prune command only removes volumes not attached to any container. It will not touch volumes currently in use.

tmpfs mounts

tmpfs mounts store data in memory. They leave no trace on disk.

docker run -d \
  --name secure-app \
  --tmpfs /run/secrets:size=64m \
  myapp:latest

Use tmpfs for sensitive data like API keys that should never be written to disk. Also useful for scratch space that benefits from memory speed.

When to use each type

TypeBest forPersists?Portable?
Bind mountLocal development, config injectionYes (host FS)No
Named volumeDatabase storage, application dataYes (Docker-managed)Yes
tmpfsSecrets, scratch space, cachesNoN/A

Bind mounts work great during development because you get live reloading. Named volumes are the right choice for production data since Docker manages the storage lifecycle. tmpfs mounts fit anywhere you need speed or secrecy and can afford to lose the data on restart.

Sharing volumes between containers

Multiple containers can mount the same named volume. This is useful when one container writes data and another reads it.

# Create a shared volume
docker volume create shared-logs

# Writer container: an app that produces logs
docker run -d \
  --name writer \
  -v shared-logs:/var/log/app \
  myapp:latest

# Reader container: a log processor
docker run -d \
  --name reader \
  -v shared-logs:/var/log/app:ro \
  logprocessor:latest

The writer produces log files. The reader consumes them in read-only mode. Both containers see the same files in real time.

Be careful with concurrent writes from multiple containers. Named volumes do not provide file locking. If two containers write to the same file simultaneously, you will get corrupted data. Design your application so that only one container writes to a given file, or use a database instead.

Backup and restore patterns

Named volumes live on the Docker host, but you cannot easily access their contents directly. The standard approach is to run a temporary container that mounts the volume and creates a tar archive.

Backing up a volume

docker run --rm \
  -v pgdata:/source:ro \
  -v $(pwd):/backup \
  alpine \
  tar czf /backup/pgdata-backup.tar.gz -C /source .

This starts an Alpine container, mounts the pgdata volume read-only at /source, mounts the current directory at /backup, and creates a compressed archive. The --rm flag removes the container when it finishes.

Restoring a volume

# Create a fresh volume
docker volume create pgdata-restored

# Extract the backup into it
docker run --rm \
  -v pgdata-restored:/target \
  -v $(pwd):/backup:ro \
  alpine \
  tar xzf /backup/pgdata-backup.tar.gz -C /target

The restore container mounts the new volume at /target and extracts the archive into it. Start your database container using pgdata-restored as its volume.

Volume drivers

The default local driver stores data on the host filesystem. Docker also supports pluggable volume drivers for remote storage.

The local driver

Handles most use cases. Supports mount options for things like NFS:

docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw \
  --opt device=:/exports/data \
  nfs-data

This creates a volume backed by an NFS share. Containers using this volume read and write to the remote server transparently.

Third-party drivers

Drivers exist for cloud storage (AWS EBS, Azure Disk, GCE Persistent Disk), distributed filesystems (GlusterFS, Ceph), and more. Install them as Docker plugins:

docker plugin install rexray/ebs
docker volume create --driver rexray/ebs --opt size=50 ebs-vol

Third-party drivers are most useful in orchestrated environments or when you need volumes that can move between hosts.

Common mistakes

Forgetting to name volumes. Anonymous volumes are hard to track and easy to lose during cleanup. Always name your volumes.

# Bad: anonymous volume
docker run -v /data postgres:16

# Good: named volume
docker run -v pgdata:/data postgres:16

Using bind mounts in production. Bind mounts tie your container to a specific host path, making deployments fragile. Use named volumes instead.

Ignoring volume permissions. If the container runs as a non-root user, the volume directory must have the right ownership. Set permissions in your Dockerfile or an init script.

What comes next

With storage handled, the next step is writing Dockerfiles that produce small, secure, and cacheable images. We will cover multi-stage builds, layer ordering, and security scanning.

Start typing to search across all content
navigate Enter open Esc close