Managing Docker Containers and Volumes
From the Make notes on docker an GitHub curriculum
TL;DR
You'll learn how containers move through their lifecycle (create, start, stop, remove), how to inspect and debug running containers, and how volumes keep your data alive after a container dies. You'll see real commands, a state diagram, and the exact pitfalls that bite beginners with data loss.
1. The Mental Model
A container is a process with its own filesystem view, not a tiny virtual machine. When the container stops, its writable layer still exists until you remove it — but anything written there dies the moment you delete it. Volumes are Docker-managed storage that lives outside the container's lifecycle, so data survives even when the container doesn't. Containers are disposable; your data shouldn't be.
2. The Core Material
2.1 The Container Lifecycle

Photo by Jan van der Wolf on Pexels
Every container moves through a small set of states. Knowing the exact command for each transition saves you from guessing.
docker create --name web nginx # creates, doesn't start
docker start web # created -> running
docker pause web # running -> paused (freezes processes)
docker unpause web # paused -> running
docker stop web # running -> stopped (SIGTERM, then SIGKILL after 10s)
docker kill web # running -> stopped (SIGKILL immediately)
docker restart web # stopped/running -> running again
docker rm web # stopped -> gone (filesystem deleted)
docker run is really docker create + docker start glued together, which is why most tutorials only teach run and skip the rest. But understanding the split matters once you're debugging a container that exited unexpectedly — you can inspect it before removing it.
docker run -d --name api -p 8080:80 nginx # -d = detached, -p = port mapping
docker ps # running containers
docker ps -a # all containers, including stopped
docker logs -f api # follow logs live
docker exec -it api /bin/bash # shell into a running container
docker inspect api # full JSON metadata: mounts, network, env
docker exec only works on a running container — it attaches a new process to an already-running namespace. If the container already exited, exec fails; you need docker start first, or docker logs to see why it died.
2.2 Why Containers Lose Data (and Volumes Fix It)

Photo by Simon on Pexels
Every container gets a writable layer on top of its read-only image layers (this is Docker's union filesystem, overlay2 on Linux). Any file you create or modify inside the container goes into that writable layer. The problem: docker rm deletes that layer permanently.
flowchart LR
A["Image layers (read-only)"] --> B["Writable container layer"]
B --> C{"docker rm"}
C -->|"no volume mounted"| D["Data gone forever"]
C -->|"volume mounted"| E["Data persists in volume/host dir"]
Docker gives you three storage options:
| Type | Where it lives | Use case |
|---|---|---|
| Writable layer (default) | Inside container, managed by storage driver | Throwaway scratch data |
| Bind mount | A specific path on the host | Local dev, sharing source code into a container |
| Named volume | Docker-managed area (/var/lib/docker/volumes/ on Linux) |
Production persistent data (databases, uploads) |
# Named volume — Docker manages the path, portable across hosts
docker volume create db_data
docker run -d --name postgres \
-v db_data:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret postgres:16
# Bind mount — you control the exact host path
docker run -d --name app \
-v "$(pwd)/src:/usr/src/app" node:20 node server.js
docker volume ls # list volumes
docker volume inspect db_data # see mountpoint, driver
docker volume rm db_data # delete — only if unused
docker volume prune # delete ALL unused volumes
The -v syntax is host_source:container_target. If host_source contains a slash or starts with ./ or /, Docker treats it as a bind mount. If it's a plain name like db_data, Docker treats it as a named volume and creates it automatically if it doesn't exist.
You can also mount volumes read-only so a container can read config but never corrupt it:
docker run -v config_vol:/etc/app/config:ro myimage
2.3 Cleaning Up Without Shooting Yourself

Photo by RDNE Stock project on Pexels
Docker never deletes anything automatically, so unused containers, images, and volumes pile up fast. The commands below are safe in roughly increasing order of aggressiveness.
docker container prune # remove all STOPPED containers
docker image prune # remove dangling (untagged) images
docker image prune -a # remove ALL images not used by a running container
docker volume prune # remove volumes not attached to any container
docker system prune -a --volumes # the nuclear option: containers + images + volumes + networks
Run docker system df first to see how much space each category is using — it tells you whether pruning is even worth doing before you nuke anything.
stateDiagram-v2
[*] --> Created: docker create
Created --> Running: docker start
Running --> Paused: docker pause
Paused --> Running: docker unpause
Running --> Stopped: docker stop / kill
Stopped --> Running: docker start
Stopped --> [*]: docker rm
Created --> [*]: docker rm
3. Worked Example
Say you're running a Postgres database for local development and want it to survive container restarts and rebuilds.
# Step 1: create a named volume for persistent data
docker volume create pg_data
# Step 2: run Postgres, mounting the volume at Postgres's data directory
docker run -d --name mydb \
-e POSTGRES_PASSWORD=devpass \
-v pg_data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
# Step 3: create a table and insert data
docker exec -it mydb psql -U postgres -c \
"CREATE TABLE users(id serial, name text); INSERT INTO users(name) VALUES ('alice');"
# Step 4: prove it's running
docker exec -it mydb psql -U postgres -c "SELECT * FROM users;"
# id | name
# ----+-------
# 1 | alice
# Step 5: destroy the container entirely
docker stop mydb && docker rm mydb
# Step 6: spin up a FRESH container, same volume
docker run -d --name mydb2 \
-e POSTGRES_PASSWORD=devpass \
-v pg_data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
docker exec -it mydb2 psql -U postgres -c "SELECT * FROM users;"
# id | name
# ----+-------
# 1 | alice <-- data survived container deletion
The container mydb is completely gone — a different name, different ID, different writable layer. But alice is still there, because she lived in pg_data, not in the container. That's the whole point of volumes.
4. Production Pitfalls & Best Practices
4.1 Real-World Best Practices

Photo by Yoga Vidya Mandiram on Pexels
- Name your volumes explicitly (
-v db_data:/data) instead of letting Docker generate anonymous hash names — anonymous volumes are nearly impossible to identify later withdocker volume ls. - Use bind mounts for source code in dev, named volumes for data in prod. Bind mounts give instant file sync for hot-reload; named volumes give Docker-managed, backup-friendly storage.
- Always check
docker ps -abefore prune commands — a stopped container you forgot about might hold logs you still need. - Back up volumes with a throwaway container, don't touch the host filesystem directly:
bash docker run --rm -v pg_data:/data -v "$(pwd)":/backup alpine \ tar czf /backup/pg_backup.tar.gz -C /data . - Set
restart: unless-stopped(or--restart unless-stopped) on long-running services so they survive host reboots without needing cron jobs. - Use
docker inspect --formatto script around state, e.g.docker inspect -f '{{.State.Running}}' mydbin CI health checks instead of parsingdocker pstext output. - Tag volumes by project with Compose (
project_db_data) sodocker volume lsstays readable across multiple projects on one machine.
4.2 Common Bugs & Anti-Patterns
Bug 1 — Writing data inside the container with no volume, then wondering where it went:
# BAD
docker run -d --name db postgres:16
# ...weeks later...
docker rm -f db # all data silently destroyed
# FIXED
docker run -d --name db -v pg_data:/var/lib/postgresql/data postgres:16
Bug 2 — Confusing bind mount source paths (relative paths resolve from the wrong directory):
# BAD — depends on where you happened to run the command from
docker run -v ./data:/app/data myimage
# FIXED — always use an absolute path, or $(pwd) explicitly
docker run -v "$(pwd)/data":/app/data myimage
Bug 3 — Running docker system prune --volumes on a shared host:
# BAD — wipes every unused volume, including a teammate's "temporarily stopped" service data
docker system prune -a --volumes
# FIXED — prune scoped and inspect first
docker volume ls -f dangling=true
docker volume prune # review the list it prints before confirming
Bug 4 — Expecting docker stop to be instant, then force-killing too eagerly:
# BAD — sends SIGKILL immediately, DB may not flush writes
docker kill mydb
# FIXED — give the app time to shut down cleanly
docker stop -t 30 mydb # waits 30s for graceful shutdown before SIGKILL
4.3 When to Use What (Decision Matrix
| Situation | Right approach |
|---|---|
| Local dev, want live code reload | Bind mount source folder into container |
| Database / persistent app state | Named volume |
| Sharing config files read-only | Named or bind volume with :ro flag |
| Temporary scratch space, disposable | Default writable layer, no volume |
| Backing up production data | docker run --rm + tar through a volume mount |
| Multi-service app needing shared data | Named volume referenced by multiple containers in Compose |
5. Now Try It
Right here, in your terminal: create a named volume called notes_vol, run an alpine container that mounts it at /data and writes a file (echo "hello" > /data/note.txt), then remove the container completely and start a new alpine container mounting the same volume to cat /data/note.txt. Then run docker volume inspect notes_vol and note the Mountpoint field.
Success looks like: the second, brand-new container prints hello even though the first container no longer exists — proving you understand that data lives in the volume, not the container.
Frequently asked about Managing Docker Containers and Volumes
Study this next
Get the full Make notes on docker an GitHub curriculum
Clone the complete plan to your dashboard for unlimited AI-generated notes, practice quizzes, and a personalised revision schedule.
Save this course free