Featured image of post Docker for Beginners: Understanding Docker Storage and Volumes

Docker for Beginners: Understanding Docker Storage and Volumes

A hands-on guide to understanding how Docker handles storage. Learn how to persist data across container restarts using volumes and bind mounts, when to use each, and how to manage them effectively in development and production environments.

Introduction: From Connection to Persistence

You’ve come a long way in your Docker journey.

You’ve written your first Dockerfile.
You’ve built and deployed a real container. You even wired up your containers into a networked system.

Now you’re wondering: what happens to my app’s data?

What if a user submits a form, or a service logs information, or a database stores records—and then your container restarts?

Here’s the truth:
Docker containers are ephemeral by design—they don’t remember anything unless you explicitly ask them to. If your app stores something internally, and you remove the container, that data is gone.

This article is about solving that problem.

We’re about to explore one of the most important and misunderstood aspects of Docker: storage and volumes.

By the end of this guide, you’ll be able to:

  • Keep your application data even when containers are recreated
  • Reflect changes in your code instantly (without rebuilding images) while developing
  • Choose between volumes and bind mounts based on real-world needs

If the last article was about teaching your containers how to talk,
this one is about teaching them how to remember.

Let’s start with a simple question that every developer has asked at some point:

Act 1: The Problem – Where Did My Data Go?

Let’s begin with a quick experiment—no theory, just real output.

Step 1: Run a MySQL container

We’ll start a container that runs MySQL in interactive mode and ask it to remember something.

1
docker run -it --rm mysql:latest

Docker will pull the image if it’s not available and start a temporary container.

You’ll see a bunch of logs, eventually followed by something like:

1
MySQL init process done. Ready for start up.

You’re now inside a MySQL container, but here’s the catch:
It has no persistent storage unless you explicitly give it some.

Let’s run through a short database session:

Step 2: Add some data

Open the MySQL shell:

1
mysql -u root

Now run:

1
2
3
4
5
CREATE DATABASE test_db;
USE test_db;
CREATE TABLE users (id INT, name VARCHAR(50));
INSERT INTO users VALUES (1, 'Alice');
SELECT * FROM users;

You should see a result like:

1
2
3
4
5
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
+----+-------+

Great! You’ve added a record. Now exit:

1
EXIT;

Then shut down the container by pressing Ctrl+C.

Step 3: Start again—and look for the data

Let’s start the exact same container again:

1
docker run -it --rm mysql:latest

Wait for the startup logs, then log into MySQL again:

1
mysql -u root

Now try:

1
2
USE test_db;
SELECT * FROM users;

What do you see?

1
ERROR 1049 (42000): Unknown database 'test_db'

It’s gone.

So… what just happened?

Everything you did—the database, the table, the data—was written inside the container’s temporary writable layer.

That layer is deleted when the container stops, because we used --rm, which tells Docker to clean up everything after exit.

This is great for stateless apps like CLI tools or scratch containers.
But for anything that stores state—user uploads, logs, database records—this default behavior is catastrophic.

The Insight

Containers are ephemeral by design.
But your data doesn’t have to be.

In the next section, we’ll introduce Docker Volumes, the official way to give containers a memory beyond their own lifecycle.

Let’s dive in.

Act 2: The Solution — Enter Docker Volumes

So now you’ve seen the problem with your own eyes:
Containers forget everything the moment they stop.

That wasn’t a bug. It was by design.
But now it’s time to change the design.

Meet Docker Volumes

Docker volumes are like giving your container a long-term memory.

Imagine this: You’ve got a container that needs to store data.
Instead of keeping it all inside the container (which is risky),
you connect an external drive that Docker manages behind the scenes.

That’s a volume—a special folder on your host machine that:

  • Survives container shutdowns and rebuilds
  • Is managed entirely by Docker (you don’t need to worry where it’s stored)
  • Can be shared across multiple containers

Think of a volume like a storage locker:

  • Your container uses it when it’s running
  • But even if the container disappears, the locker (and the stuff inside) stays

Unlike bind mounts (which we’ll cover later), Docker volumes don’t depend on specific host file paths. They’re portable, easy to manage, and perfect for data that needs to stick around.

Visualising Docker Storage

As shown in the diagram below, Docker volumes are one of three storage paths available to containers:

Docker Storage Diagram

  • The writable layer is where data goes by default inside the container—and it vanishes when the container dies.
  • Bind mounts connect directly to a specific path on your host (we’ll get to those later).
  • Volumes, in contrast, are managed by Docker and stored in a safe, consistent location. They’re the gold standard for persisting data.

Volumes live on the host’s physical storage, but you don’t manage them directly—they’re abstracted by Docker.

Creating and Inspecting Volumes

Let’s start by creating our first Docker volume:

1
docker volume create my_data

You now have a named volume managed by Docker.

Now list all volumes:

1
docker volume ls

You should see something like:

1
2
DRIVER              VOLUME NAME
local               my_data

Want to peek under the hood?

1
docker volume inspect my_data

You’ll get a JSON output showing exactly where Docker is storing this volume on your host system. Something like:

1
"Mountpoint": "/var/lib/docker/volumes/my_data/_data"

That’s where your data actually lives—but Docker manages it for you.

To remove it (when you’re done experimenting):

1
docker volume rm my_data

These commands give you full control over volumes as first-class citizens in your Docker environment.

Using a Volume with a Container

Let’s modify our earlier experiment—this time with a volume:

1
2
3
4
docker run -d --name mysql_persistent \
  -e MYSQL_ROOT_PASSWORD=secret \
  -v my_data:/var/lib/mysql \
  mysql:latest

What this does:

  • -d: Run in detached mode
  • --name: Give our container a name
  • -e: Set the root password for MySQL
  • -v my_data:/var/lib/mysql tells Docker to mount your volume into the MySQL container’s data directory
  • All data written inside /var/lib/mysql (where MySQL stores everything) is now stored in the volume, not the container

You’ve now externalized MySQL’s data. Even if the container goes away, the data will not.

Now run the same database commands from before:

Proving It: Stop, Remove, Recreate

1
2
3
4
5
6
7
8
docker exec -it mysql_persistent mysql -uroot -p
# Then:
CREATE DATABASE remember_me;
USE remember_me;
CREATE TABLE notes (id INT, text VARCHAR(50));
INSERT INTO notes VALUES (1, 'I persist!');
SELECT * FROM notes;
EXIT;

Now stop and remove the container:

1
2
docker stop mysql_persistent
docker rm mysql_persistent

Then spin up a new one—same volume, different container:

1
2
3
4
docker run -d --name mysql_new \
  -e MYSQL_ROOT_PASSWORD=secret \
  -v my_data:/var/lib/mysql \
  mysql:latest

And check:

1
2
3
4
docker exec -it mysql_new mysql -uroot -p
# Then:
USE remember_me;
SELECT * FROM notes;

🎉 Your data is still there.

Reflection: Why Volumes Are Docker’s Preferred Storage Option

So why does Docker manage volumes as a special kind of storage?

Because it gives you isolation, control, and flexibility:

  • Volumes aren’t tied to fragile host paths
  • They’re portable across environments (great for teams or CI/CD)
  • They’re safer in production (less chance of exposing sensitive host files)
  • Docker can optimize them behind the scenes for performance

And perhaps most importantly:
They decouple your data from your container logic.

Your containers come and go.
Your data sticks around.

You just built a container that remembers things—even after it’s gone. That’s a big leap.

Ready to see the alternative approach?
Next up: bind mounts—more flexible, more dangerous.

Act 3: The Alternative – Bind Mounts

Volumes are Docker’s preferred way to manage persistent data—but they’re not the only option.

Sometimes, especially during development, you want something more direct.
You want your container to read and write from a specific folder on your host machine—maybe your source code directory, a log folder, or a shared file.

That’s where bind mounts come in.

What Are Bind Mounts?

A bind mount is like a shortcut between a folder on your host machine and a folder inside your container.

Unlike volumes, which are managed and abstracted by Docker, bind mounts are tightly coupled to the exact file path on your host. Whatever happens in that folder—whether it’s edited by Docker or something else—is visible inside the container in real time.

Think of it like putting a window in your container wall. Whatever happens outside, you see it inside.

Why Use Bind Mounts?

Bind mounts come with risks, but they also unlock powerful workflows—especially in development.

Use bind mounts when:

  • You want live code editing during development (hot reloads)
  • You want a container to write logs or results to a specific folder on the host
  • You’re working with large data sets already on your host

Hands-On: Run a Web Server with a Bind Mount

Let’s walk through an example where we mount a local HTML file into an Nginx container.

Step 1: Create a test HTML file

On your host machine, create a file:

1
2
mkdir my-web-content
echo "<h1>Hello from bind mount!</h1>" > my-web-content/index.html

Step 2: Run an Nginx container with a bind mount

1
2
3
4
docker run -d --name webserver \
  -v $(pwd)/my-web-content:/usr/share/nginx/html \
  -p 8080:80 \
  nginx:latest

Here’s what’s happening:

  • -v $(pwd)/my-web-content:/usr/share/nginx/html tells Docker:
    • Use the my-web-content folder on the host
    • Mount it inside the container at /usr/share/nginx/html (where Nginx serves files)
  • -p 8080:80 maps your host’s port 8080 to the container’s port 80
  • nginx:latest starts the official Nginx image

Step 3: View in browser

Open your browser and go to:

1
http://localhost:8080

You should see:

1
Hello from bind mount!

Now try editing index.html on your host and refresh the browser—you’ll see the change immediately.

Reflection: Why Bind Mounts Are Powerful (and Risky)

Bind mounts are powerful because they remove friction during development. You don’t have to rebuild your image every time you change code.

But they come with trade-offs:

Feature Docker Volumes Bind Mounts
Managed by Docker ✅ Yes ❌ No (host-dependent)
Tied to host path ❌ No ✅ Yes
Portable across hosts ✅ Yes ❌ No
Access outside Docker ❌ No ✅ Yes (host can see/modify)
Best for Production, backups Development, quick prototyping

Bind mounts = more control, more danger.
Accidentally delete a file on the host? It’s gone in the container too.

In production environments, bind mounts are rarely recommended. The tight coupling to host paths makes them fragile and hard to port.

But in dev workflows? They’re a game-changer.

In the next act, we’ll learn what it means for data to persist, when to use volumes, when bind mounts make sense, and how to think like a container-native engineer when managing storage.

Act 4: Reflect and Apply

You’ve now seen both Docker volumes and bind mounts in action. You’ve written data, deleted containers, recreated them, and confirmed that the data persisted. But what did it all mean? Let’s step back and make sense of it all.

When to Use Docker Volumes

Volumes are Docker’s recommended way to persist data—and for good reason:

  • Safe: They’re isolated from the host file system and managed entirely by Docker.
  • Portable: You can back them up, migrate them, and move them between environments without worrying about paths.
  • Ideal for production: Most production-grade container orchestration systems (like Kubernetes) expect volumes, not bind mounts.

Use volumes when:

  • Your containerized service needs durable, reliable, and secure data storage.
  • You want to keep your app portable between dev, staging, and prod.
  • You’re managing databases, queues, logs, or other persistent services.

When to Use Bind Mounts

Bind mounts give you power and flexibility—but with great power comes… potential mess.

  • Fast iteration: Great for development when you want instant feedback from code edits.
  • Direct host access: When you need containers to read/write files on the host system.
  • Experimentation: Ideal for debugging or light data processing tasks.

Use bind mounts when:

  • You’re actively developing an app and want changes on your machine to instantly reflect in the container.
  • You want to inspect container output directly on the host.
  • You’re doing one-off work that doesn’t need full Docker volume management.

Portability and Isolation: The Tradeoff

Volumes = more isolation, better for portability and security.
Bind mounts = more visibility, better for dev speed, but riskier in prod.

If you’re ever unsure, start with volumes. They’re safer, easier to reason about, and less likely to lead to subtle bugs.

Visual Recap: Volumes vs. Bind Mounts

Feature Docker Volumes Bind Mounts
Managed by Docker ✅ Yes ❌ No (host-managed)
Portable ✅ Easily across machines/environments ❌ Host-dependent paths
Dev-friendly ⚠ Not ideal for live code editing ✅ Excellent for live code updates
Prod-safe ✅ Recommended ⚠ Risky unless strictly managed
Visible to Host FS ❌ Hidden from direct host access ✅ Fully visible on host

Final Thought

You’ve just practiced one of the most essential skills in container-based development: separating your data from your runtime.

This separation is what allows you to:

  • Scale apps safely
  • Upgrade containers without data loss
  • Share infrastructure while isolating data
  • Migrate workloads across environments

In the next act, we’ll pull back and examine some best practices and common issues when working with Docker storage—so you don’t get tripped up in production.

Act 5: Best Practices and Troubleshooting

You’ve now walked the walk—tested both volumes and bind mounts, created and deleted containers, and watched your data survive. But in real-world development and production, things can still go sideways. Let’s wrap up with some practical guidance to keep your Docker storage setup reliable, clean, and low-stress.

Best Practices to Follow

These aren’t just rules—they’re guardrails to keep you sane as your projects grow:

1. Prefer volumes for long-term persistence

Volumes are isolated, managed by Docker, and portable. They’re the right default for:

  • Production workloads
  • Any container that stores durable state
  • Sharing data safely between containers

Start with volumes. Reach for bind mounts only when you need direct host integration.

2. Use bind mounts for fast local development

In dev environments, bind mounts let you:

  • Instantly reflect code changes
  • Share logs or config files
  • Explore container internals

Just remember they aren’t portable—and shouldn’t sneak into production builds.

3. Name your volumes and mount points clearly

Avoid cryptic names like vol123 or /data. Use expressive names like:

1
2
docker volume create app_logs
docker run -v app_logs:/var/log/app my-service

It makes your setup more understandable and debuggable for others (or your future self).

4. Use read-only mounts when possible

Want to mount a config file or directory into a container but avoid accidental changes?

1
docker run -v /my/config:/app/config:ro my-image

Adding :ro makes it read-only inside the container—super useful for safety.

5. Keep volumes away from build-time logic

Volumes are runtime-only. Don’t rely on them during docker build. Instead:

  • Copy static files via COPY in your Dockerfile
  • Mount volumes at runtime for dynamic data

Common Storage Issues (and How to Solve Them)

Even with best practices, it’s easy to hit bumps. Here are some common Docker storage issues—and how to fix them fast.

❌ Problem: My container can’t write to the volume

Cause: Permissions mismatch between the container user and host directory

Fix:

  • Use docker exec to inspect permissions
  • Adjust file ownership (chown) or run the container as the matching UID/GID
  • For bind mounts, try using a permissive test directory first

❌ Problem: My data is gone after rebuild

Cause: You didn’t mount a volume or bind mount—data was written to the container’s internal writable layer, which is deleted with the container.

Fix:

  • Double-check your docker run command
  • Add -v mydata:/app/data or a bind mount
  • Confirm with docker volume ls or docker inspect

❌ Problem: I’m running out of disk space

Cause: Stopped containers, old volumes, images, and build cache can pile up.

Fix:

  • See what’s using space:
    1
    
    docker system df
    
  • Clean up safely:
    1
    
    docker system prune -f
    

    ⚠️ Be careful—this deletes stopped containers, unused networks, and dangling images

❌ Problem: I can’t find where Docker stored my volume

Fix:

  • Use docker volume inspect your_volume_name to see the mount point on your host

Pro Tip: Docker Inspect Is Your Friend

Want to see how your volume is mounted into a container?

1
docker inspect my_container

Look under the "Mounts" section. You’ll see paths, modes, and mount types—perfect for debugging weird behaviors.

With these best practices and tips in your toolkit, you’re no longer just storing data—you’re managing it cleanly, safely, and portably across any environment.

In our final act, we’ll zoom out once again and prepare you for what powers all of this behind the scenes: container runtimes like Docker’s own containerd. Ready for a peek under the hood?

Let’s go.

Coming Up Next: What Powers a Container?

So far, you’ve learned how to run containers, wire them together with networks, and give them a memory through volumes and mounts.

But one big question still remains:

What is actually running your container behind the scenes?

In our next article, we’ll look under Docker’s hood and explore its runtime — the component responsible for starting, stopping, and isolating containers.

You’ll meet:

  • containerd, the modern runtime that powers Docker itself
  • runc, the low-level binary that actually spawns containers
  • CRI-O and how Kubernetes chooses runtimes

These details matter when you start moving from Docker CLI to container orchestration. Get ready — we’re going lower-level.