Docker by Example: Named Volumes
In the last tutorial, we used bind mounts to connect our code directory to a container for live development. That works great for code, but databases need something different. You don't want to edit database files directly on your computer, but you absolutely need that data to persist when containers stop and start.
That's where named volumes come in.
What are named volumes?
Named volumes are Docker-managed storage areas that persist independently of containers. Unlike bind mounts where you point to a specific folder on your computer, named volumes are stored in a special location managed by Docker. You give them a name, and Docker takes care of the rest.
They're perfect for databases because:
- Data persists even when you delete the container
- You don't store database files in your project directory where they can be accidentally edited or deleted
- Docker manages the storage location and permissions
- They work consistently across different operating systems
Running PostgreSQL in Docker
Let's run a PostgreSQL database in Docker and see how data persistence works.
First, start a basic Postgres container using the postgres:16 base image:
docker run --name my-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 postgres:16This pulls the official PostgreSQL image from Docker Hub and runs it. The -e flag sets an environment variable (the database password), and -p publishes the port so we can connect to it from your computer.
- Stop your local PostgreSQL service
- Use a different external port:
-p 5433:5432(this maps your computer's port 5433 to the container's port 5432)
If you use a different port, remember to connect to that port (e.g., localhost:5433) in all the following steps.
Connecting with pgAdmin
If you have pgAdmin installed, you can use it to connect to your Docker database.
- Open pgAdmin
- Right-click "Servers" and select "Register" → "Server"
- In the "General" tab, give it a name like "Docker Postgres"
- In the "Connection" tab, enter:
- Host:
localhost(or127.0.0.1) - Port:
5432(or whatever port you used in the docker run command) - Username:
postgres - Password:
mysecretpassword
- Host:
- Click "Save"
You should now be connected! You can use pgAdmin's query tool to run SQL commands.
Connecting with psql
If you prefer the command line or don't have pgAdmin, you can use psql.
If you have psql installed on your computer:
psql -h localhost -U postgresIf you don't have psql installed, you can run it inside the Docker container:
docker exec -it my-postgres psql -U postgresEnter the password mysecretpassword when prompted.
Testing data persistence
Let's create some data and see what happens when we stop and restart the container.
Using pgAdmin's query tool or psql, create a table and add some data:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
INSERT INTO users (name, email) VALUES
('Alice', '[email protected]'),
('Bob', '[email protected]'),
('Charlie', '[email protected]');
SELECT * FROM users;You should see your three users. Beautiful!
Now let's stop the container:
docker stop my-postgresAnd start it again:
docker start my-postgresConnect again (via pgAdmin or psql) and query your data:
SELECT * FROM users;The data is still there! That's because even though containers are immutable, Postgres stores its data in a specific location (/var/lib/postgresql/data), and Docker automatically creates an anonymous volume for it when you don't specify one.
The problem with anonymous volumes
Here's where things get tricky. Let's remove the container and create a new one:
docker stop my-postgres
docker rm my-postgres
docker run --name my-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 postgres:16Connect to the database (via pgAdmin or psql) and check:
SELECT * FROM users;You get an error! The table doesn't exist anymore!
What happened? When you removed the container, that anonymous volume got left behind (orphaned), and the new container created a fresh new anonymous volume. Your data still exists somewhere in Docker's storage, but you can't access it because you don't know the volume's randomly generated name.
This is where named volumes save the day.
Using named volumes for true persistence
Let's do it right this time. Stop and remove the container:
docker stop my-postgres
docker rm my-postgresNow create a new container with a named volume:
docker run --name my-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -v pgdata:/var/lib/postgresql/data postgres:16The -v pgdata:/var/lib/postgresql/data flag creates a named volume called pgdata and mounts it to the Postgres data directory in the container.
Connect and create your data again:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
INSERT INTO users (name, email) VALUES
('Alice', '[email protected]'),
('Bob', '[email protected]'),
('Charlie', '[email protected]');
SELECT * FROM users;Now let's really test it. Stop and remove the container:
docker stop my-postgres
docker rm my-postgresThe container is gone. But now create a brand new container with the same named volume:
docker run --name my-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -v pgdata:/var/lib/postgresql/data postgres:16Connect and check your data:
SELECT * FROM users;Your data is still there! The named volume persists independently of the container. You can remove and recreate containers all day long, and as long as you mount the same named volume, your data persists.
This is incredibly powerful. You can:
- Upgrade the Postgres version by pulling a new image and creating a new container with the same volume
- Recover from a corrupted container by creating a new one
- Share the same data volume across multiple container instances (with appropriate database clustering)
Managing volumes
You can see all your volumes with:
docker volume lsYou should see pgdata in the list, along with any anonymous volumes left behind from earlier experiments.
To see detailed information about a volume:
docker volume inspect pgdataTo remove a specific volume:
docker volume rm pgdataTo remove all unused volumes (including those orphaned anonymous volumes):
docker volume pruneBe careful with prune - it will delete any volumes not currently attached to a container, which could include data you want to keep!
Why immutability still matters
Even with named volumes, remember that the container and image are still immutable:
- The database software (PostgreSQL itself) doesn't change unless you pull a new image and create a new container
- The database configuration baked into the image doesn't change
- Only the data in your named volume can change
This is actually a powerful combination:
- Your database software version is predictable and version-controlled (immutable images)
- Your data persists and grows over time (named volumes)
- You can upgrade PostgreSQL by pulling a new image and creating a new container, while keeping the same data volume
The immutable container runs the database software, while the named volume stores the mutable data. Each does what it does best, and together they give you a robust, maintainable database setup.
When to use named volumes vs bind mounts
Now that you've seen both, here's when to use each:
Named volumes - Use for:
- Database data
- Application-generated files you need to persist
- Any data that needs to persist but you don't need to edit directly
Bind mounts - Use for:
- Source code during development
- Configuration files you edit frequently
- Any files where you need direct access from your computer
The rule of thumb: If you're editing it, use a bind mount. If you're just storing it, use a named volume.
Next up
