Docker Volumes & Storage

intermediate docker volumes storage persistence

Here’s the thing about containers — they’re ephemeral. When a container is removed, all data inside it is gone. That writable layer we talked about in the images doc? It lives and dies with the container.

So if we’re running a database in a container and the container crashes or gets replaced during a deploy, all our data vanishes. That’s where volumes come in.

Three types of storage

Docker gives us three ways to persist data:

Volumes
Managed by Docker
Stored in /var/lib/docker/volumes/
Best for production data
Bind Mounts
Maps a host path
Any path on host machine
Best for development
tmpfs
In-memory only
Never written to disk
Best for sensitive temp data

Volumes are completely managed by Docker. We don’t need to care about the exact path on the host. Docker handles it. They also work on both Linux and macOS/Windows.

# Create a named volume
docker volume create my-data

# Run a container with a volume mounted
docker run -d --name db \
  -v my-data:/var/lib/postgresql/data \
  postgres:16-alpine

# The data survives container removal
docker rm -f db
# my-data still exists — start a new container with it
docker run -d --name db-new \
  -v my-data:/var/lib/postgresql/data \
  postgres:16-alpine

# List all volumes
docker volume ls

# Inspect a volume
docker volume inspect my-data

Bind mounts — great for development

Bind mounts map a specific directory on our host machine into the container. This is perfect for development because we can edit code on our machine and see changes instantly inside the container.

# Mount current directory into /app in the container
docker run -d --name dev-server \
  -v $(pwd):/app \
  -p 3000:3000 \
  node:20-alpine npm run dev

# Changes to files on our machine show up immediately in the container

The only difference from volumes is that we specify a full path on the host instead of a volume name. If the path starts with / or ./, Docker treats it as a bind mount. If it’s just a name, it’s a volume.

tmpfs — in-memory storage

tmpfs mounts store data in memory. Nothing is written to disk, and the data disappears when the container stops. Good for sensitive data like secrets or session tokens that we don’t want lingering on disk.

docker run -d --name secure-app \
  --tmpfs /app/tmp:rw,size=64m \
  my-app

Volumes in docker-compose

This is where we’ll use volumes the most. In a compose file, we define volumes at the top level and reference them in services.

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pg-data:/var/lib/postgresql/data   # named volume

  app:
    build: .
    volumes:
      - ./src:/app/src                      # bind mount for dev
      - node_modules:/app/node_modules      # named volume for deps

volumes:
  pg-data:         # Docker manages this
  node_modules:    # Keeps node_modules inside volume, not on host

Common patterns

Database persistence — always use a named volume for database data directories. PostgreSQL uses /var/lib/postgresql/data, MySQL uses /var/lib/mysql, MongoDB uses /data/db.

Preserving node_modules — a common trick in Node.js projects. We bind-mount our source code but keep node_modules in a named volume so it doesn’t conflict with the host’s node_modules.

Backup a volume — volumes don’t have a built-in backup command, but we can use a temporary container to tar the data.

# Backup a volume to a tar file
docker run --rm \
  -v pg-data:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/pg-data-backup.tar.gz -C /data .