Skip to content

Docker Volumes Example


Concepts and commands are great, but let’s actually put them to work using only what you already know — containers, images, and now volumes. No new tooling required.

Real World Example of Using Volumes

The Setup

We want a Postgres database that survives container restarts, redeployments, and even a full removal of the container — while anything talking to it stays completely disposable and rebuildable. We’ll wire two containers together using a Docker network, and give the database a named volume so its data lives independently of the container itself.

Note

The goal is to give hands on experience to docker volume persistent nature. So, you don’t require any database skill to complete this exercise. If you know it’s good for you. If you don’t know — it’s completely fine as it is not mandatory.

    flowchart TB
    subgraph Net["app-net (Docker network)"]
        Web["client container<br/>(ephemeral, rebuilt freely)"]
        DB["db container<br/>(ephemeral, but data isn't)"]
    end
    Vol[("pgdata volume<br/>(Docker-managed, persistent)")]
    Web -->|"talks to db:5432"| DB
    DB -->|"reads/writes"| Vol
  

Step 1: Create a Network So the Containers Can Talk

docker network create app-net

This gives both containers a shared network where they can reach each other by container name — no IP addresses to hardcode.

Step 2: Create a Named Volume for the Database

docker volume create pgdata

Step 3: Run the Database Container

docker container run -d \
  --name db \
  --network app-net \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_PASSWORD=apppass \
  -e POSTGRES_DB=appdb \
  postgres:16

The database’s actual data directory is mounted to the pgdata volume. Nothing about the container itself — its writable layer, its lifecycle — matters to whether that data survives.

Step 4: Run a Simple Client Container

To prove the network is actually doing something — letting a completely separate container reach the database by name — let’s spin up a tiny, fully pre-baked Python script that connects to it and reads back whatever’s inside.

Create a folder with exactly one file in it:

mkdir db-client && cd db-client

Save the following as client.py in that folder, copied exactly as-is:

import psycopg2

conn = psycopg2.connect(
    host="db",
    dbname="appdb",
    user="appuser",
    password="apppass",
)
cur = conn.cursor()
cur.execute("SELECT * FROM notes;")

rows = cur.fetchall()
if rows:
    for row in rows:
        print(row)
else:
    print("No rows yet — the notes table might be empty or not created.")

cur.close()
conn.close()

Notice the host="db" — that’s not a placeholder, it’s the actual container name from Step 3. Because both containers share app-net, Docker’s built-in DNS resolves db straight to the database container’s address automatically, no IP addresses involved.

Run it as a throwaway container, bind-mounting just this one script in:

docker container run --rm \
  --name db-client \
  --network app-net \
  -v "$(pwd)":/app \
  -w /app \
  python:3.12-slim \
  sh -c "pip install psycopg2-binary && python client.py"

Right now it’ll print “No rows yet,” since we haven’t inserted anything. That’s expected — we’re about to fix that next.

Proving Persistence Actually Works

# Insert some data directly into the database
docker container exec -it db \
  psql -U appuser -d appdb \
  -c "CREATE TABLE notes (id serial, body text); INSERT INTO notes (body) VALUES ('hello from lesson');"

# Run the client container again — it should now print that row
docker container run --rm \
  --name db-client \
  --network app-net \
  -v "$(pwd)":/app \
  -w /app \
  python:3.12-slim \
  sh -c "pip install psycopg2-binary && python client.py"

# Remove the db container entirely (not the volume)
docker container rm -f db

# Recreate the db container, attaching the SAME volume
docker container run -d \
  --name db \
  --network app-net \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_PASSWORD=apppass \
  -e POSTGRES_DB=appdb \
  postgres:16

# Run the client one more time to check the data is still there
docker container run --rm \
  --name db-client \
  --network app-net \
  -v "$(pwd)":/app \
  -w /app \
  python:3.12-slim \
  sh -c "pip install psycopg2-binary && python client.py"

That last run still prints hello from lesson — even though the original db container was completely removed and a brand-new one created in its place. The data was never tied to that specific container; it lived in pgdata the whole time, and any container that mounts pgdata picks up right where the last one left off.

Warning

Removing a container with docker container rm never touches a named volume attached to it — that’s expected and safe, as we covered in the lifecycle section. The only way to lose pgdata here is to explicitly run docker volume rm pgdata. Never run that command out of habit; only when you genuinely intend to delete the data for good.

Adding a Backup Step to the Same Setup

Following the lifecycle practices from earlier, let’s bolt on a simple backup routine for this exact setup, using the same throwaway-container pattern:

docker container run --rm \
  -v pgdata:/data \
  -v "$(pwd)/backups":/backup \
  busybox \
  tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz -C /data .

Wire this into a daily cron job or a scheduled task, and you’ve got a database that’s disposable at the container level and durable at the data level — which is really the whole point of everything covered in this section.

What This Example Demonstrates

  • The client container stays fully ephemeral — it’s created fresh, does its job, and removes itself (--rm) every single time, nothing to worry about.
  • The db container is also ephemeral, but its actual data lives in a named volume, decoupled from the container’s lifecycle entirely.
  • Removing and recreating the db container — as long as it’s reattached to pgdata — picks up exactly where things left off.
  • The Docker network let a completely separate container reach the database by name, with zero hardcoded IP addresses involved.
  • A simple backup routine, built on the same throwaway-container pattern from the lifecycle section, adds a safety net beyond just “the volume still exists on this one host.”

This pattern — ephemeral containers, a persistent named volume, a shared network for communication, and a scheduled backup — is genuinely how a lot of real Docker setups handle stateful services, whether it’s Postgres, MySQL, Redis with persistence enabled, or anything else that needs to remember something between restarts.

Last updated on