Docker Compose, by default, throws away all your data when you docker-compose down or when a container crashes. That’s usually fine for ephemeral development environments, but for anything that needs to survive a restart, you need a way to make your data persistent. Named volumes are Docker’s built-in solution for this.

Let’s say you’re running a PostgreSQL database in a Docker Compose setup. Without persistence, every time you bring down and then up your services, your entire database gets wiped clean.

Here’s a docker-compose.yml for a simple PostgreSQL setup:

version: '3.8'
services:
  db:
    image: postgres:14
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
    ports:
      - "5432:5432"
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:

When you run docker-compose up -d, Docker creates a named volume called db_data. It then mounts this volume to /var/lib/postgresql/data inside the PostgreSQL container. This is where PostgreSQL stores its actual database files.

The magic happens because this db_data volume exists outside of the container’s lifecycle. When the container stops or is removed, the db_data volume remains. The next time you start the container, Docker reattaches the same db_data volume to /var/lib/postgresql/data, and your PostgreSQL instance boots up with all its previous data intact.

Let’s see it in action.

First, start the service:

docker-compose up -d

Now, connect to your PostgreSQL instance (using psql or any client) and create a table and insert some data. For example:

CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100));
INSERT INTO users (name) VALUES ('Alice');
INSERT INTO users (name) VALUES ('Bob');

Verify the data:

SELECT * FROM users;

You should see Alice and Bob.

Now, let’s simulate a restart. Stop and remove the containers:

docker-compose down

And then bring them back up:

docker-compose up -d

Connect to the database again and run SELECT * FROM users;. Alice and Bob are still there. The data persisted because it was stored in the db_data named volume.

To inspect the named volume itself, you can use docker volume ls to see all volumes and docker volume inspect <volume_name> to get details. For db_data, you’d see something like this:

docker volume inspect db_data
[
    {
        "CreatedAt": "2023-10-27T10:00:00Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "your_project_name",
            "com.docker.compose.version": "2.23.3"
        },
        "Mountpoint": "/var/lib/docker/volumes/db_data/_data",
        "Name": "db_data",
        "Options": {}
    }
]

The Mountpoint is the directory on your Docker host where the volume’s data is actually stored. You can even browse this directory (though it’s generally discouraged to modify files directly here unless you know exactly what you’re doing).

The key advantage of named volumes over bind mounts (where you map a host directory directly to a container directory) is that Docker manages the volume’s lifecycle and location. This makes it more portable and less prone to configuration errors, especially when dealing with different operating systems or complex file path setups. Docker handles the underlying storage details, allowing you to focus on your application.

Most people think of volumes in docker-compose.yml as just a way to make data survive container restarts. What’s less obvious is that Docker uses these named volumes as a central registry for persistent storage, and they can be explicitly managed, backed up, and even migrated independently of your Compose project. You can create a named volume before you define it in your docker-compose.yml, or you can even share a single named volume across multiple unrelated Compose projects if you’re careful.

The next step is understanding how to manage these volumes more actively, like backing them up or migrating them to a different Docker host.

Want structured learning?

Take the full Docker course →