Skip to content
Menu

Virtualization4 min read

Volumes & Storage

Solving the persistence problem — bind mounts, named volumes, and tmpfs

The Persistence Problem

Why Container Storage Is Not Enough

  • Container's read-write layer (overlay2 upperdir) is ephemeral — destroyed when the container is removed
  • Overlay2 copy-on-write adds overhead for write-heavy workloads: first write to a file copies the entire file from lowerdir to upperdir
  • Two problems:
    • Data loss — database files, uploads, state all vanish with docker rm
    • Performance — CoW overhead is unacceptable for databases, logging, I/O-intensive apps
  • Solution: mount host storage directly into the container, bypassing the overlay filesystem entirely

Three Mount Types

Bind Mounts

  • Map a host directory/file directly into the container
  • Kernel bind mount — same underlying filesystem, direct access
  • Changes are bidirectional and immediate
  • Container sees host files; host sees container writes
console
# -v syntax
docker run -v /host/path:/container/path

# --mount syntax (preferred)
docker run --mount \
  type=bind,\
  source=/host/path,\
  target=/container/path

# Read-only bind mount
docker run --mount \
  type=bind,\
  source=/host/path,\
  target=/container/path,\
  readonly

Good for: development (live code reload), sharing config files

Risk: container can modify host files (unless readonly)

Named Volumes

  • Managed by Docker, stored in /var/lib/docker/volumes/<name>/_data
  • Docker manages lifecycle: create, list, inspect, remove
  • Volume population: if the image has data at the mount point, Docker copies it into the volume on first use
  • Volume drivers: local (default), NFS, AWS EBS, Azure Files
console
# -v syntax
docker run -v mydata:/container/path

# --mount syntax (preferred)
docker run --mount \
  type=volume,\
  source=mydata,\
  target=/container/path

# Volume management
docker volume create mydata
docker volume ls
docker volume inspect mydata
docker volume rm mydata

Good for: databases, persistent application data

tmpfs Mounts

  • In-memory filesystem, never written to disk
  • Data lost when container stops
  • Faster than disk-backed storage
  • Not shared between containers
console
# --tmpfs syntax
docker run --tmpfs /container/path

# --mount syntax (preferred)
docker run --mount \
  type=tmpfs,\
  target=/container/path

# With size limit
docker run --mount \
  type=tmpfs,\
  target=/run/secrets,\
  tmpfs-size=64m

Good for: secrets, temporary scratch space, sensitive data that shouldn't persist on disk

Note

Key difference: Bind mounts depend on the host's directory structure. Named volumes are fully managed by Docker and portable. Always prefer --mount syntax over -v — it's more explicit and catches errors (e.g., -v silently creates a directory if the source doesn't exist; --mount fails with an error for bind mounts).

How Mounts Bypass Overlay

Volumes and bind mounts are overlaid on top of the overlay2 merged view at specific mount points. Writes to those paths go directly to the backing storage, completely bypassing the copy-on-write mechanism.

Container Filesystem View
Overlay2 Merged View (lowerdir + upperdir)

/usr, /bin, /lib, ...

Read from image layers (lowerdir)
Write via CoW to upperdir

/var/lib/mysql

Named volume
Direct I/O — bypasses overlay

/app/src

Bind mount
Direct I/O — bypasses overlay

  1. Write to /usr/bin/foo

    Goes through overlay2 CoW

  2. Write to /var/lib/mysql/data

    Direct to volume — no CoW overhead

  3. Write to /app/src/main.py

    Direct to host directory — visible immediately

Storage Drivers vs Volumes

Aspect Storage Driver (overlay2) Volumes
Purpose Container filesystem (image layers + RW layer) Persistent data
Performance Copy-on-write overhead on first write Direct I/O — native filesystem performance
Lifecycle Dies with the container (docker rm) Independent of container lifecycle
Sharing Not shareable between containers Can be shared (mounted into multiple containers)
Backup Complex — data is in overlay layer format Simple — regular directory on the host filesystem
Portability Tied to image build Independent, can be backed by NFS/cloud drivers

Volume Population — Bind Mounts vs Named Volumes

Named Volume (First Use)

If the image has files at /usr/share/nginx/html:

bash
docker run -v mysite:/usr/share/nginx/html nginx

Docker copies image files into the volume on first mount (volume is empty). Subsequent mounts use the volume's existing data.

Image data is preserved.

Bind Mount

If the image has files at /usr/share/nginx/html:

bash
docker run -v /my/html:/usr/share/nginx/html nginx

The host directory completely obscures the image contents. If /my/html is empty, the container sees an empty directory.

Image data is hidden.

Best Practices

Databases

Always use named volumes. Never store database data in the container layer.

bash
docker run -d \
  --name postgres \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

Config Files

Use bind mounts with :ro (read-only) to inject configuration.

bash
docker run -d \
  --name nginx \
  --mount type=bind,\
source=./nginx.conf,\
target=/etc/nginx/nginx.conf,\
readonly \
  nginx

Secrets / Sensitive Data

Use tmpfs mounts so secrets never touch disk. In Swarm mode, use Docker secrets.

bash
docker run -d \
  --mount type=tmpfs,\
target=/run/secrets,\
tmpfs-size=1m \
  myapp

Development

Use bind mounts for live code reload — edit on host, see changes immediately in container.

bash
docker run -d \
  -v $(pwd)/src:/app/src \
  -p 3000:3000 \
  node-dev

Warning

Warning: Don't mount /var/run/docker.sock into a container unless absolutely necessary. This gives the container full control over Docker and effectively root access to the host. See Docker Architecture — Socket File Security.

Volume Commands — Quick Reference

console
console

    $ docker volume create mydata

    mydata


    $ docker volume ls

    DRIVER    VOLUME NAME
local     mydata
local     pgdata


    $ docker volume inspect mydata

    [{
    "CreatedAt": "2026-03-31T10:00:00Z",
    "Driver": "local",
    "Mountpoint": "/var/lib/docker/volumes/mydata/_data",
    "Name": "mydata",
    "Scope": "local"
}]


    $ docker volume rm mydata

    mydata


    $ docker volume prune

    WARNING! This will remove anonymous local volumes not used by at least one container.
Total reclaimed space: 1.2GB
  

Tip

Tip: Use docker volume prune periodically to clean up orphaned anonymous volumes. Named volumes are not removed by prune — only anonymous (unnamed) volumes that are no longer referenced by any container.

Solidnines — solidnines.com