Containers are ephemeral by design — but your data shouldn't be. This lesson covers how Docker decouples storage from the container lifecycle so databases, uploads, and logs survive restarts and removals.

1. The Problem

Every container gets a thin writable layer on top of its image layers. When the container is removed, that layer is deleted — and all data written inside the container vanishes with it.

  • Databases — PostgreSQL writes to /var/lib/postgresql/data. Remove the container? Gone.
  • Uploads — User-uploaded files saved inside the container? Gone.
  • Logs — Application logs written to /var/log/app? Gone.

There's also a performance problem: the writable layer uses a copy-on-write (CoW) filesystem. Every first write to a file copies it up from the image layer — adding latency. For I/O-heavy workloads like databases, this overhead is unacceptable.

The Cardinal Rule

Never store valuable data in a container's writable layer. Treat containers as disposable; store state outside the container.

2. Three Storage Options

Docker provides three mechanisms to persist or share data beyond the container lifecycle:

TypeManaged ByLocationBest For
VolumeDocker/var/lib/docker/volumes/Databases, persistent app data
Bind MountYou (host path)Anywhere on hostDev hot-reload, config files
tmpfsKernelRAM onlySecrets, temp processing
Host Filesystem /var/lib/docker/volumes/ 📦 Volumes /home/user/project/ 📁 Bind Mounts RAM (never on disk) 🧠 tmpfs 🐳 Container Writable layer (ephemeral)

3. Volumes (Docker-Managed)

Volumes are the recommended way to persist data. Docker manages the storage directory — you reference volumes by name, not path.

# Create a named volume
docker volume create mydata

# Run a container with the volume mounted
docker run -d --name app1 -v mydata:/app/data alpine sh -c "echo hello > /app/data/test.txt"

# Another container can read the same data
docker run --rm -v mydata:/app/data alpine cat /app/data/test.txt
# Output: hello

Why volumes win:

  • Work identically on Linux, macOS, and Windows
  • Can be backed up, migrated, or pre-populated
  • Support volume drivers for remote/cloud storage
  • Safer — container can't accidentally traverse into host directories
Short-form vs --mount

-v mydata:/app/data is the short form. The explicit form is --mount type=volume,source=mydata,target=/app/data. Both do the same thing; --mount is more readable in scripts.

4. Bind Mounts

Bind mounts map an exact host path into the container. The host directory must already exist.

# Mount current directory into container
docker run -d -v $(pwd):/app -w /app node:20-alpine npm run dev

# Changes on host instantly appear in container (hot-reload!)
# Changes in container instantly appear on host

Use cases:

  • Development — edit code on host, app hot-reloads inside container
  • Config injection — mount a single config file into the container
  • Build output — container writes artifacts to a host directory
Security Risk

Bind mounts give the container direct access to host files. A container running as root with -v /:/host has full access to your entire filesystem. Always scope mounts to the minimum required path.

5. tmpfs Mounts

A tmpfs mount stores data in memory only. It is never written to the host filesystem, and disappears when the container stops.

# Mount tmpfs at /app/tmp (max 64MB)
docker run -d --tmpfs /app/tmp:size=64m myapp

# Or with --mount syntax
docker run -d --mount type=tmpfs,target=/app/tmp,tmpfs-size=67108864 myapp

Use cases:

  • Storing short-lived secrets (tokens, session keys) that must never hit disk
  • Scratch space for temporary processing (image manipulation, sorting)
  • Fast I/O for ephemeral data — RAM is orders of magnitude faster than disk

6. Read-Only Mounts

Append :ro to make any mount read-only inside the container:

# Container can read config but cannot modify it
docker run -d -v ./nginx.conf:/etc/nginx/nginx.conf:ro nginx

# Volume mounted read-only
docker run -d -v mydata:/app/data:ro myapp

A common pattern: mount config/code as read-only, then add a tmpfs for any directory the app needs to write to temporarily:

docker run -d \
  -v ./app:/app:ro \
  --tmpfs /app/tmp \
  --tmpfs /tmp \
  myapp

This gives you immutable infrastructure — the container cannot modify its own code or config, reducing the blast radius of a compromise.

7. Volume Lifecycle & Cleanup

Volumes persist after container removal. This is by design — but it means orphaned volumes accumulate silently.

# List all volumes
docker volume ls

# Inspect a volume
docker volume inspect mydata

# Remove a specific volume
docker volume rm mydata

# Remove ALL unused volumes (not attached to any container)
docker volume prune

# Nuclear option: prune everything
docker system prune --volumes
Create Attach Detach Orphan Prune docker volume create docker run -v container stops/rm no container using it docker volume prune

Named vs Anonymous volumes:

  • Named-v mydata:/data — easy to identify and reuse
  • Anonymous-v /data (no name) — gets a random hash ID, hard to track, easy to orphan
Tip

Always use named volumes. Anonymous volumes are almost impossible to identify later and become disk-wasting orphans.

8. Volume Drivers & Remote Storage

By default, volumes use the local driver (files on the host). Volume drivers extend this to remote and cloud storage:

  • NFS — shared network filesystem for multi-host access
  • AWS EBS — block storage in AWS (single-attach)
  • Azure Files — SMB-based shared storage
  • GlusterFS / Ceph — distributed storage clusters
# Create a volume using NFS driver
docker volume create --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw \
  --opt device=:/shared \
  nfs-data

In Kubernetes, this concept is abstracted further with PersistentVolumes (PV) and PersistentVolumeClaims (PVC). You declare "I need 10GB of fast storage" and the cluster provisions it — no driver configuration in your container spec.

Interactive Quizzes

Hands-On Tasks

Task 1: Share Data Between Containers via a Volume
  1. Create a named volume: docker volume create shared-data
  2. Write data from container A:
    docker run --rm -v shared-data:/data alpine sh -c "echo 'Written by container A' > /data/message.txt"
  3. Read from container B:
    docker run --rm -v shared-data:/data alpine cat /data/message.txt
  4. Verify output: Written by container A
  5. Clean up: docker volume rm shared-data

What you proved: Volumes exist independently of any single container and can share data between them.

Task 2: PostgreSQL Data Survives Container Destruction
  1. Start PostgreSQL with a named volume:
    docker run -d --name pg1 \
      -e POSTGRES_PASSWORD=secret \
      -v pgdata:/var/lib/postgresql/data \
      postgres:16-alpine
  2. Create a table and insert data:
    docker exec pg1 psql -U postgres -c "CREATE TABLE test(id serial, msg text); INSERT INTO test(msg) VALUES ('I survive!');"
  3. Destroy the container: docker rm -f pg1
  4. Start a new container with the same volume:
    docker run -d --name pg2 \
      -e POSTGRES_PASSWORD=secret \
      -v pgdata:/var/lib/postgresql/data \
      postgres:16-alpine
  5. Query the data:
    docker exec pg2 psql -U postgres -c "SELECT * FROM test;"
  6. Verify I survive! is still there.
  7. Clean up: docker rm -f pg2 && docker volume rm pgdata

What you proved: Database data in a named volume persists across container destruction and recreation.

🌐 Not Just Docker

Podman volumes work identically — podman volume create, same -v name:/path syntax, same lifecycle. Kubernetes abstracts further with PersistentVolumeClaims (PVC): you declare storage needs declaratively, and the cluster provisions and binds the appropriate backend (EBS, NFS, local SSD) automatically.

🏭 Industry Practice: Databases in Containers

Running databases in containers is now mainstream — PostgreSQL, MySQL, MongoDB all have official images. The pattern: named volume for data directory + container is disposable. In Kubernetes, StatefulSets manage this: each pod replica gets its own PersistentVolumeClaim, volumes follow pods across rescheduling, and storage classes handle provisioning. This is how companies run thousands of database instances on container platforms.

Key Takeaways

  • Container writable layers are ephemeral — never store important data there
  • Volumes are Docker-managed, portable, and the default choice for persistent data
  • Bind mounts map host paths directly — great for development, risky for production
  • tmpfs lives in RAM only — use for secrets and scratch data that must not persist
  • :ro flag makes mounts read-only — defense in depth for config and code
  • Volumes survive container removal — clean up with docker volume prune
  • Always use named volumes — anonymous volumes become untrackable orphans
  • Volume drivers extend storage to NFS, cloud, and distributed systems