You spent two hours setting up your PostgreSQL container, seeded it with test data, ran your app against it — everything works perfectly. Then you restart the container. Gone. All of it. The database is empty again.
If you have been there, you already know the pain. If you have not, consider this your warning. Docker containers are ephemeral by design, and that is a feature — until you forget about it and lose your data.
This article is a practical guide to Docker volumes: what they are, how they work, when to use each type, and the patterns that will save you from the "where did my data go" panic at 2 AM.
Why Containers Lose Data
Every Docker container gets its own writable layer on top of its image. When you write files inside a running container, they go into this layer. The problem: when the container is removed (not just stopped, but removed), that writable layer is deleted with it.
This is intentional. Containers are supposed to be disposable. You should be able to spin up ten copies of your app container and throw them away without ceremony. But your database is not supposed to be disposable.
This is exactly the problem that volumes solve.
The Three Ways to Persist Data
Docker gives you three mechanisms for data persistence:
- Volumes — managed by Docker, stored in Docker's own storage area
- Bind mounts — map a host directory directly into the container
- tmpfs mounts — stored in memory, gone when the container stops (useful for sensitive data)
Let's look at each one.
Docker Volumes: The Recommended Approach
Volumes are managed entirely by Docker. You create them, Docker handles where they live on the host (usually /var/lib/docker/volumes/ on Linux). You do not need to worry about host paths.
Creating and using a volume
# Create a named volume
docker volume create myapp-data
# Run a container with that volume
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=secret \
-v myapp-data:/var/lib/postgresql/data \
postgres:16
Now your PostgreSQL data survives container restarts and even container removal. The volume persists until you explicitly delete it.
Inspect and manage volumes
# List all volumes
docker volume ls
# Inspect a volume (shows the actual host path)
docker volume inspect myapp-data
# Remove a volume (careful -- this deletes the data)
docker volume rm myapp-data
# Remove all unused volumes
docker volume prune
Why volumes beat bind mounts for production
- Volumes work the same on any OS — no path differences between Linux, macOS, and Windows
- Docker can manage backups and migrations of volumes
- Volumes can be shared between containers more safely
- Named volumes are easy to identify and reason about
Bind Mounts: Direct Host Access
Bind mounts map a specific directory from your host machine into the container. They are perfect for development.
docker run -d \
--name myapp \
-v /home/teguh/myapp:/app \
node:20 node /app/server.js
Anything you change in /home/teguh/myapp on your host is instantly reflected inside the container at /app. This is why bind mounts are the go-to for local development — you edit code in your editor, and the running container sees the changes immediately.
The development workflow
# docker-compose.yml for local development
services:
app:
build: .
ports:
- "3000:3000"
volumes:
# Bind mount for live code changes
- ./src:/app/src
# Named volume for node_modules (prevents host/container conflicts)
- node_modules:/app/node_modules
command: npm run dev
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
# Named volume for database persistence
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
node_modules:
Notice the node_modules trick. If you bind-mount your entire project directory, the container's node_modules (compiled for Linux) gets overwritten by your host's node_modules (compiled for macOS or Windows). Using a named volume for node_modules keeps them separate and avoids the infamous "wrong platform" binary errors.
Sharing Volumes Between Containers
Multiple containers can read from and write to the same volume. This is useful for sidecar patterns — like a log aggregator that reads files written by your application container.
# Container 1: writes logs
docker run -d \
--name app \
-v shared-logs:/var/log/app \
myapp:latest
# Container 2: reads and ships those logs
docker run -d \
--name log-shipper \
-v shared-logs:/var/log/app:ro \
fluent-bit:latest
The :ro flag makes the mount read-only in the second container — a good security practice when a container only needs to read, not write.
Volume Backup and Restore
This is the part people forget until disaster strikes. Always have a backup strategy.
Backup a volume
# Create a backup of the postgres_data volume
docker run --rm \
-v postgres_data:/source:ro \
-v $(pwd)/backups:/backup \
alpine \
tar czf /backup/postgres_$(date +%Y%m%d_%H%M%S).tar.gz -C /source .
This spins up a minimal Alpine container, mounts your volume as read-only, and tars up the contents into a backups/ folder on your host.
Restore from backup
# Create a fresh volume
docker volume create postgres_data_restored
# Extract your backup into it
docker run --rm \
-v postgres_data_restored:/target \
-v $(pwd)/backups:/backup \
alpine \
tar xzf /backup/postgres_20260303_180000.tar.gz -C /target
Common Mistakes and How to Avoid Them
Mistake 1: Anonymous volumes
# Bad: Docker generates a random ID as the volume name
docker run -v /var/lib/postgresql/data postgres:16
# Good: Always use named volumes
docker run -v postgres_data:/var/lib/postgresql/data postgres:16
Anonymous volumes accumulate silently. You will end up with dozens of docker volume ls entries that look like a3f2b8c1d4e... and have no idea what they contain.
Mistake 2: Forgetting volumes exist when rebuilding
When you run docker-compose down, volumes are preserved by default. This is usually what you want. But when you want a completely clean slate:
# Remove containers AND volumes
docker-compose down -v
# Or remove specific volumes
docker volume rm myproject_postgres_data
Mistake 3: Wrong permissions
If your container runs as a non-root user (it should), the volume mount might have permission issues.
FROM node:20-alpine
# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Create and own the data directory
RUN mkdir -p /app/data && chown appuser:appgroup /app/data
USER appuser
Putting It All Together
Here is a production-ready Docker Compose setup combining everything:
services:
app:
build:
context: .
target: production
ports:
- "3000:3000"
volumes:
- uploads:/app/uploads
depends_on:
- postgres
- redis
environment:
DATABASE_URL: postgresql://user:secret@postgres:5432/myapp
REDIS_URL: redis://redis:6379
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
command: redis-server --appendonly yes
volumes:
postgres_data:
redis_data:
uploads:
Notice: every stateful service (postgres, redis, file uploads) has a named volume. The init SQL script uses a bind mount with :ro because it is static config, not data.
The Mental Model
Think of it this way:
- Named volumes for any data that needs to survive: databases, user uploads, application state
- Bind mounts for development: source code, config files you want to edit live
- Read-only mounts for config: seed data, init scripts, certificates
- No mounts for anything truly ephemeral: temp files, build artifacts, logs you do not care about
Once this becomes habit, you stop losing data and start treating your containers with appropriate disposability — stateless app containers come and go freely, while your precious data sits safely in volumes.
Wrapping Up
Docker volumes are one of those things that seem complicated until they click, and then they seem obvious. The key insight is simple: separate your stateless compute (containers) from your stateful data (volumes).
Named volumes for production, bind mounts for development, always back up your volumes before doing anything destructive, and never use anonymous volumes if you can help it.
Now go check your running containers — you might have some orphaned anonymous volumes sitting around, taking up space and holding data you have already forgotten about.
docker volume ls
docker volume prune
You have been warned.
Top comments (0)