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.
|
|
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:
|
|
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:
|
|
Now run:
|
|
You should see a result like:
|
|
Great! You’ve added a record. Now 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:
|
|
Wait for the startup logs, then log into MySQL again:
|
|
Now try:
|
|
What do you see?
|
|
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:
- 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:
|
|
You now have a named volume managed by Docker.
Now list all volumes:
|
|
You should see something like:
|
|
Want to peek under the hood?
|
|
You’ll get a JSON output showing exactly where Docker is storing this volume on your host system. Something like:
|
|
That’s where your data actually lives—but Docker manages it for you.
To remove it (when you’re done experimenting):
|
|
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:
|
|
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
|
|
Now stop and remove the container:
|
|
Then spin up a new one—same volume, different container:
|
|
And check:
|
|
🎉 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:
|
|
Step 2: Run an Nginx container with a bind mount
|
|
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)
- Use the
-p 8080:80
maps your host’s port 8080 to the container’s port 80nginx:latest
starts the official Nginx image
Step 3: View in browser
Open your browser and go to:
|
|
You should see:
|
|
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:
|
|
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?
|
|
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
ordocker 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?
|
|
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 itselfrunc
, the low-level binary that actually spawns containersCRI-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.