Volumes and Persistent Data

Chapter 4 — Volumes and Persistent Data

Back in Chapter 1 we noted that containers are ephemeral — anything written inside a container is lost the moment the container is removed. For a web server serving static files that's fine. For a database storing your data, or a development environment where you want your edits to survive, it's a problem. Volumes are Docker's answer.

This chapter covers how data loss actually happens, the three types of storage Docker provides, and the two you'll use every day: named volumes and bind mounts.

1. The Data Loss Problem

Every container has a thin writable layer on top of its read-only image layers. Any file created or modified inside the container goes into this writable layer. When you remove the container with docker rm, that writable layer is deleted with it — permanently.

Container running
🗄️ MySQL database files
📝 User records written
📝 Orders written
Image layers (read-only)

💥
docker rm
New container from same image
MySQL database files — GONE
User records — GONE
Orders — GONE
Image layers (read-only)

This happens even if you only run docker rm — not docker stop. Stopping a container leaves its writable layer intact; removing it deletes the layer. The solution is to store data outside the container's writable layer, somewhere that persists independently of any individual container.

2. Three Types of Docker Storage

Named Volumes
Managed by Docker — stored in Docker's own area on disk
Survive container removal
Easy to back up and migrate
Best for: databases, app data you don't need to edit directly
Use this for databases ✓
Bind Mounts
Points at a specific folder on your host machine
Changes on host are instantly visible in container
Changes in container are instantly visible on host
Best for: website files, source code, config files
Use this for dev work ✓
tmpfs Mounts
Stored in host memory only — never written to disk
Gone when container stops
Very fast
Best for: sensitive data (secrets, tokens) that should never touch disk
Advanced / specialist use

For the rest of this chapter we'll focus on named volumes and bind mounts — the two you'll use in almost every real scenario.

3. Named Volumes

A named volume is a storage area that Docker creates and manages. You give it a name, mount it into one or more containers at a specific path, and Docker handles where the data actually lives on disk. You don't need to know or care about the exact host path — Docker manages it for you.

Terminal
# Create a named volume explicitly (optional — docker run creates it automatically) $ docker volume create mysql-data mysql-data # Start MySQL, mounting the named volume at /var/lib/mysql (where MySQL stores data) $ docker run -d \ --name mydb \ -e MYSQL_ROOT_PASSWORD=secret \ -e MYSQL_DATABASE=myapp \ -v mysql-data:/var/lib/mysql \ --restart unless-stopped \ mysql:8.0 a91b3c2d4e5f... # Write some data into the database $ docker exec -it mydb mysql -u root -psecret myapp mysql> CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100)); mysql> INSERT INTO users (name) VALUES ('Philip'); mysql> exit # Now destroy the container completely $ docker rm -f mydb mydb # Create a brand new container from the same volume $ docker run -d \ --name mydb \ -e MYSQL_ROOT_PASSWORD=secret \ -v mysql-data:/var/lib/mysql \ mysql:8.0 # The data is still there $ docker exec -it mydb mysql -u root -psecret myapp -e "SELECT * FROM users;" +----+--------+ | id | name | +----+--------+ | 1 | Philip | +----+--------+
The volume outlives the container. You could remove and recreate the MySQL container ten times and the data would still be there, because it lives in the mysql-data volume — not inside the container itself.

Managing Named Volumes

CommandWhat it does
docker volume create NAME Create a named volume manually
docker volume ls List all volumes
docker volume inspect NAME Show volume details including where it lives on disk
docker volume rm NAME Delete a volume (must not be in use by any container)
docker volume prune Remove all volumes not used by any container — careful!
# See where Docker actually stores a named volume on disk docker volume inspect mysql-data [ { "Name": "mysql-data", "Driver": "local", "Mountpoint": "/var/lib/docker/volumes/mysql-data/_data", "Scope": "local" } ] # On Windows with Docker Desktop, volumes live inside the WSL2 VM # You can still access them via \\wsl$\docker-desktop-data in Explorer
docker volume prune removes data permanently. Unlike docker container prune which only removes stopped containers, docker volume prune deletes the actual data. Always double-check which volumes are safe to remove before running it.

4. Bind Mounts

A bind mount maps a specific folder on your host machine directly into the container. Unlike named volumes, you control exactly where the data lives — it's just a regular folder on your computer. This makes bind mounts ideal for development: edit files in your editor on the host, and the container sees the changes instantly.

Your host machine
/home/philip/mysite/
  index.html
  style.css
  about.html

← edit files here in VS Code
-v /home/philip/mysite:/usr/share/nginx/html
Inside the container
/usr/share/nginx/html/
  index.html
  style.css
  about.html

← nginx serves from here
Terminal — serving your website files with a bind mount
# On Linux/Mac — use the full path to your site folder $ docker run -d \ --name mysite \ -p 8080:80 \ -v /home/philip/mysite:/usr/share/nginx/html:ro \ --restart unless-stopped \ nginx:alpine # On Windows — use the Windows path (Docker Desktop converts it) $ docker run -d \ --name mysite \ -p 8080:80 \ -v C:\Users\Philip\mysite:/usr/share/nginx/html:ro \ --restart unless-stopped \ nginx:alpine # Or use $PWD (current directory) on Linux/Mac $ cd /home/philip/mysite && docker run -d \ --name mysite \ -p 8080:80 \ -v $(pwd):/usr/share/nginx/html:ro \ nginx:alpine

The :ro at the end means read-only — the container can read the files but cannot modify them. This is appropriate for serving a website. For a development environment where the container also writes files (logs, compiled output), leave off :ro.

Live editing workflow: With a bind mount in place, open index.html in your editor on the host, save a change, and refresh your browser — you'll see the change immediately without restarting the container. The container is always reading the actual files on your disk.

5. Named Volume vs Bind Mount — Which to Use

Situation Use Why
Database data (MySQL, PostgreSQL, Redis) Named volume You don't need to browse the data files directly; Docker manages them safely
Serving website files you're editing Bind mount You edit files on the host in your editor and see changes live
Source code for a dev environment Bind mount Your editor lives on the host; the container runs/builds the code
Config files (nginx.conf, php.ini) Bind mount Easy to edit on host, version-control alongside your project
Shared data between multiple containers Named volume Mount the same volume into multiple containers — they share the data
Production app data Named volume More portable — not tied to a specific host path

6. Practical Example — nginx with Custom Config and Site Files

This is a preview of Chapter 7's Apache scenario, but with nginx to show the pattern now. We'll mount both the site files and a custom config file from the host:

# Folder structure on your host: # mysite/ # html/ ← your website files # index.html # nginx.conf ← custom nginx configuration # First, extract the default nginx config to use as a starting point docker run --rm nginx:alpine cat /etc/nginx/conf.d/default.conf > mysite/nginx.conf # Run nginx with both mounts docker run -d \ --name mysite \ -p 8080:80 \ -v $(pwd)/mysite/html:/usr/share/nginx/html:ro \ -v $(pwd)/mysite/nginx.conf:/etc/nginx/conf.d/default.conf:ro \ --restart unless-stopped \ nginx:alpine # Edit nginx.conf on your host, then reload nginx without restarting the container docker exec mysite nginx -t # test the config first docker exec mysite nginx -s reload # apply it
Mounting a single file: You can bind-mount a single file rather than a whole directory. This is handy for config files — you keep the file in your project folder, bind-mount it into exactly the right place in the container, and edit it like any other file on your machine.

7. Backing Up and Restoring Volumes

Named volumes are managed by Docker, so they're not immediately accessible as a folder. The standard backup technique is to spin up a temporary container, mount the volume, and archive its contents:

# ── BACKUP ────────────────────────────────────────────────────────── # # Mount the volume + a backup destination directory, tar the contents docker run --rm \ -v mysql-data:/source:ro \ -v $(pwd)/backups:/backup \ alpine \ tar czf /backup/mysql-data-$(date +%Y%m%d).tar.gz -C /source . # Result: backups/mysql-data-20240616.tar.gz on your host # ── RESTORE ───────────────────────────────────────────────────────── # # Create a fresh volume and extract the backup into it docker volume create mysql-data-restored docker run --rm \ -v mysql-data-restored:/target \ -v $(pwd)/backups:/backup:ro \ alpine \ tar xzf /backup/mysql-data-20240616.tar.gz -C /target
For MySQL specifically it's better to use mysqldump (a logical backup) rather than copying the raw data files. Raw file copying can produce a corrupt backup if MySQL is writing at the same time. Use:

docker exec mydb mysqldump -u root -psecret myapp > backup.sql

8. Common Volume Patterns

# ── Named volume — database ──────────────────────────────────────── # docker run -d \ -v db-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=secret \ mysql:8.0 # ── Bind mount — serve website files (read-only) ─────────────────── # docker run -d \ -p 80:80 \ -v /home/philip/mysite:/usr/share/nginx/html:ro \ nginx:alpine # ── Bind mount — development (read-write, edits visible both ways) ── # docker run -it \ -v $(pwd):/app \ -w /app \ python:3.12-slim \ bash # ── Mount a single config file ────────────────────────────────────── # docker run -d \ -v $(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf:ro \ nginx:alpine # ── Two containers sharing the same named volume ─────────────────── # docker run -d --name writer -v shared-data:/data myapp-writer docker run -d --name reader -v shared-data:/data:ro myapp-reader
Exercises
  1. Prove containers are ephemeral. Run docker run -it --name test ubuntu bash, create a file inside with echo "hello" > /myfile.txt, then exit and run docker rm test. Start a new ubuntu container and confirm /myfile.txt doesn't exist. This is the problem volumes solve.
  2. Persist data with a named volume. Create a named volume called test-vol and run docker run -it --rm -v test-vol:/data ubuntu bash. Inside, write echo "persisted!" > /data/hello.txt and exit. Run a second container with the same volume mount and confirm the file is still there with cat /data/hello.txt.
  3. Serve your website with a bind mount. Create a folder called mysite on your host with a simple index.html. Run docker run -d -p 8080:80 -v /full/path/to/mysite:/usr/share/nginx/html:ro nginx:alpine. Visit http://localhost:8080 to confirm it serves your file. Now edit index.html on your host and refresh — you should see the change immediately without restarting the container.
  4. Inspect a volume. Run docker volume ls to see your volumes, then docker volume inspect test-vol to find its Mountpoint on disk. On Linux you can navigate directly to that path and see your files. On Windows with Docker Desktop, note the WSL2 path shown.
  5. Back up a named volume. Using the backup command from Section 7, create a .tar.gz backup of your test-vol into a backups/ folder on your host. Then create a new volume called test-vol-restore, restore the backup into it, and verify the file is there by running a container with the restored volume mounted.
Next: Chapter 5 — Networking Basics
Volumes solved persistent data. Chapter 5 solves connectivity. You'll learn how Docker containers communicate — with your browser, with your host, and with each other. Covered: port mapping in depth, Docker's default bridge network, creating custom networks so containers can find each other by name, and the key difference between localhost on your host vs inside a container.