Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Volumes in Compose

Define and manage persistent data volumes in Docker Compose for reliable stateful services.

Focus: define volumes in compose for persistence

Sponsored

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

  1. Declare the volume at the top level of your docker-compose.yml under the volumes: key.
  2. Mount it into a service by adding a volumes: key to that service definition.
  3. 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: local unless you are using a cloud or third-party volume driver. Most teams stick with the default local driver 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 declaring volumes: 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 -v deletes 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

  1. Use the external: true flag in the volume declaration to pre-create a volume manually or reuse one created outside Compose.
  2. For multi-node deployments (Swarm or Kubernetes), use volume drivers like rclone or nfs to make volumes accessible across hosts.
  3. Combine named volumes with tmpfs mounts for ephemeral high-speed storage (e.g., /tmp or 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.