Volumes in Compose
Define and manage persistent data volumes in Docker Compose for reliable stateful services.
Focus: define volumes in compose for persistence
When you run a container that writes data — a database record, a file upload, a log entry — that data vanishes the moment the container stops, is replaced, or gets destroyed. That’s by design for stateless microservices, but real applications need state. Without a volume, restarting a container wipes your work. Volumes in Docker Compose solve this by keeping your data alive across container lifecycles, giving you persistence without losing the portability of containers.
The problem this lesson solves
Every Docker container starts with a fresh filesystem. When you run docker compose down, any files created inside the container are gone. That is great for stateless web servers, but terrible for databases, file storage, or any service that must remember what it did between restarts. Without volumes, you would need to re-import data, re-upload files, or accept data loss every time you update your stack. The problem is simple: containers are ephemeral, but your data is not.
Core concept / mental model
Think of a Docker volume as a persistent storage locker that lives outside the container but is accessible from inside it. The container sees the volume as a folder in its filesystem. When the container dies or is recreated, the volume — and everything in it — remains untouched. In Compose, you define these volumes declaratively in the YAML file, so the entire team uses the same storage setup.
There are two volume types in Compose: - Named volumes: Managed by Docker — you give them a name, and Docker handles the location on the host. - Bind mounts: Map a specific directory on the host machine into the container (less portable).
For most persistence needs in a team environment, named volumes are the default choice because they are portable, easy to back up, and don't depend on host paths.
How it works step by step
- Declare the volume at the top level of your
docker-compose.ymlunder thevolumes:key. - Mount it into a service by adding a
volumes:key to that service definition. - Use it — the container writes to the mount path; the data persists.
Volume declaration syntax
version: "3.8"
services:
db:
image: postgres:15
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Pro tip: You don't need to specify
driver: localunless you are using a cloud or third-party volume driver. Most teams stick with the defaultlocaldriver for development.
Hands-on walkthrough
Let's build a mini project with a database that survives restarts.
Step 1: Create a project directory
mkdir compose-volumes && cd compose-volumes
Step 2: Write docker-compose.yml
version: "3.8"
services:
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
adminer:
image: adminer
ports:
- "8080:8080"
volumes:
pgdata:
Step 3: Start the stack and create data
docker compose up -d
Now connect to http://localhost:8080 with system postgres, user postgres, password secret, database myapp. Create a test table — for example:
CREATE TABLE ping (id SERIAL PRIMARY KEY, ts TIMESTAMP DEFAULT NOW());
INSERT INTO ping (ts) VALUES (NOW());
SELECT * FROM ping;
Step 4: Destroy the containers and recreate
docker compose down
docker compose up -d
Connect to Adminer again — the table and data are still there. That is persistence through volumes.
Expected outcome: The data survives the full destroy/recreate cycle.
Verify volume existence
docker volume ls | grep compose-volumes
Output shows a named volume like compose-volumes_pgdata.
Compare options / when to choose what
| Feature | Named Volume | Bind Mount |
|---|---|---|
| Location managed by Docker | ✅ Yes | ❌ No — specify host path |
| Portable across machines | ✅ Yes | ❌ Path dependent |
| Back up easily | ✅ Yes (docker run --rm -v volume_name:/data ...) |
✅ Yes (copy directory) |
| Performance | ✅ Good | ✅ Good (native) |
| Use case | Persistent database, config, uploads | Sharing source code for hot-reload |
Pro tip: For development, use named volumes for database data and bind mounts for application code that needs real-time reloading (like
./app:/app).
Troubleshooting & edge cases
Problem: Permission denied inside container
Volumes owned by root inside the container may conflict with the host user. For PostgreSQL, the image already uses the postgres user. If you use a custom image, set the user in the service or adjust ownership.
services:
myservice:
user: "1000:1000"
Problem: Volume not mounting / data disappears
- Check the path: The container's destination path must match the application's expectation. Postgres expects
/var/lib/postgresql/data, not/data. - Typos in volume name: A missing volume declaration at the top level will cause an error but Compose will still try to create an anonymous volume. Always verify with
docker volume ls. - Existing volume with stale data: If you change the image or schema, old data may cause startup failures. Remove the volume manually:
docker compose down -v(use with care!).
Problem: Anonymous volumes instead of named
If you omit the named volume at the top level but still write - /var/lib/postgresql/data (without name:), Compose creates an anonymous volume with a random hash. That volume won't be reused after down unless you manually inspect docker volume ls. Always use named volumes for persistence.
# Wrong — creates anonymous volume
volumes:
- /var/lib/postgresql/data
# Correct — uses named volume
volumes:
- pgdata:/var/lib/postgresql/data
What you learned & what's next
You now know how to define named volumes in Compose to persist data across container restarts. You can differentiate between named volumes and bind mounts, choose the right approach for your scenario, and troubleshoot common pitfalls like permissions or missing volume declarations. This skill is essential for running stateful services like databases, message queues, and file stores in a containerized environment.
Next lesson: Volumes with multiple services — learn how to share the same volume across two containers (e.g., a writer and a reader service), and how to use volume-only containers for setup scripts.
Challenge for today: Take any database in your existing Compose file and move its data directory to a named volume. Verify data persistence by stopping and restarting the stack.
Practice recap
Create a new Compose file with two services: postgres with a named volume pgdata and adminer. Start the stack, add a row in Adminer, then run docker compose down && docker compose up -d. Confirm the row persists. Next, add a second database service pg2 with its own named volume and test that they remain isolated.
Common mistakes
- Declaring
volumes:at the service level without also declaringvolumes:at the top level — this creates an anonymous volume (random hash), which is not automatically reused. - Using a bind mount for database data (e.g.,
./data:/var/lib/postgresql/data) on different OS platforms — host paths break portability. - Forgetting that
docker compose down -vdeletes volumes — irreversible data loss when run accidentally. - Mounting a volume to a container path that already contains important files but the volume is empty — the empty volume shadows the container's files.
Variations
- Use the
external: trueflag in the volume declaration to pre-create a volume manually or reuse one created outside Compose. - For multi-node deployments (Swarm or Kubernetes), use volume drivers like
rcloneornfsto make volumes accessible across hosts. - Combine named volumes with
tmpfsmounts for ephemeral high-speed storage (e.g.,/tmpor cache) that never persists.
Real-world use cases
- Persist PostgreSQL data so the customer database survives deploys and restarts without re-importing records.
- Store uploaded user images in a named volume shared between a web server and a background worker container.
- Keep Redis AOF or RDB snapshot files across container restarts to avoid losing cached session data.
Key takeaways
- Named volumes survive container restarts and recreations — they are the standard for persistent data in Compose.
- Always declare volumes at the top level of docker-compose.yml for full control and reusability.
- Bind mounts are for development code reloading, not for data that must persist across teams or machines.
- Use docker compose down without -v to keep volumes; use -v only when you intentionally want to delete all data.
- Volume paths inside the container must match the application's expected data directory exactly.
- Check volume existence with docker volume ls and inspect contents with docker run --rm -v volume_name:/data alpine ls /data.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.