When I started working with Docker seriously, the first architectural problem I ran into wasn’t networking — it was data. I had two containers that needed to see the same files, and I had no idea why my changes in one container weren’t showing up in the other. It turns out that container storage is isolated by design, and sharing data between containers is something you have to set up deliberately. In this article, I’ll walk you through exactly how that works, from the fundamentals of container storage up to production-grade sharing patterns.
Why Containers Don’t Share Data by Default
Every Docker container gets its own writable filesystem layer on top of its image. This layer is created using a union filesystem (usually overlay2 on modern Linux hosts), and it’s tied to the container’s lifecycle. When the container is removed, that writable layer — and anything written to it — disappears with it.
This isolation is intentional. Containers are meant to be ephemeral, disposable, and independent. But real applications often need shared state: a web server and a log-shipping sidecar reading the same log files, a build container and a deploy container sharing artifacts, or multiple app replicas reading the same configuration.
Docker gives you three main mechanisms to break out of this isolation deliberately:
- Volumes — managed by Docker itself, stored under
/var/lib/docker/volumes/ - Bind mounts — a direct mapping of a host path into a container
- tmpfs mounts — in-memory storage, never written to disk
For sharing data between containers specifically, volumes are almost always the right tool, so I’ll spend most of this article there, then cover bind mounts and the legacy --volumes-from flag for completeness.
Docker Storage Fundamentals
Before diving into commands, it helps to understand what’s actually happening under the hood.
When you docker run an image, Docker mounts the image’s read-only layers and stacks a thin writable layer on top using overlay2. Any file write goes into that writable layer via copy-on-write — if a file exists in a lower read-only layer and you modify it, the whole file is copied up first, then modified.
Volumes bypass this union filesystem entirely. A volume is a directory that lives outside the container’s layered filesystem, managed directly by the Docker daemon, and bind-mounted into the container at a path you specify. Because it’s a real bind mount at the kernel level, I/O performance on volumes is close to native, and — critically — the same volume can be mounted into multiple containers simultaneously.
You can see this for yourself:
docker info | grep "Storage Driver"
Expected output on a typical Linux host:
Storage Driver: overlay2
Creating and Inspecting Named Volumes
Let’s create a named volume and inspect it.
docker volume create shared-data
Expected output:
shared-data
Inspect it:
docker volume inspect shared-data
Expected output:
[
{
"CreatedAt": "2026-07-29T10:12:03Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/shared-data/_data",
"Name": "shared-data",
"Options": {},
"Scope": "local"
}
]
That Mountpoint is the real directory on your host where the data physically lives. You generally shouldn’t touch it directly — always go through Docker so permissions and drivers are handled consistently.
Sharing a Volume Between Two Containers
Here’s the core pattern. I’ll spin up a “writer” container and a “reader” container, both mounting the same named volume.
docker run -d --name writer \
-v shared-data:/data \
busybox sh -c "while true; do date >> /data/log.txt; sleep 5; done"
docker run -it --name reader \
-v shared-data:/data \
busybox sh -c "sleep 20 && cat /data/log.txt"
Expected output from the reader container, after it wakes up:
Wed Jul 29 10:15:03 UTC 2026
Wed Jul 29 10:15:08 UTC 2026
Wed Jul 29 10:15:13 UTC 2026
Wed Jul 29 10:15:18 UTC 2026
Both containers reference the same volume name, shared-data, and Docker mounts the exact same underlying directory into each. There’s no copying, no syncing daemon, no network filesystem involved — it’s the same inode structure on disk, visible from two mount namespaces.
Clean up:
docker rm -f writer reader
docker volume rm shared-data
Using --mount Instead of -v
The -v flag is compact but has quirky parsing rules (it behaves differently depending on whether the source looks like a path or a volume name). The --mount flag is more explicit and is what I’d recommend for anything beyond quick testing:
docker run -d --name writer \
--mount source=shared-data,target=/data \
busybox sh -c "while true; do date >> /data/log.txt; sleep 5; done"
Bind Mounts for Container-to-Container Sharing
Bind mounts map a specific host directory into a container. Two containers can share data through a bind mount by both pointing at the same host path:
mkdir -p ~/shared-folder
docker run -d --name writer \
-v ~/shared-folder:/data \
busybox sh -c "echo hello from writer > /data/message.txt && sleep 3600"
docker run --rm \
-v ~/shared-folder:/data \
busybox cat /data/message.txt
Expected output:
hello from writer
Bind mounts are useful during development because you can edit files directly on the host with your normal editor and see changes reflected instantly in the container. The tradeoff is that bind mounts couple your setup to the host’s directory structure, which makes them less portable than named volumes — this matters when you move from a laptop to a CI runner to a production host.
The Legacy --volumes-from Flag
Before named volumes existed, Docker had --volumes-from, which lets a container inherit all volume mounts from another container:
docker run -d --name data-container -v /data busybox true
docker run -d --name app --volumes-from data-container busybox sh -c "sleep 3600"
This still works today, but it’s rarely the right choice anymore. Named volumes are more explicit, easier to reason about, and don’t depend on keeping a “data-only” container alive just to hold a mount reference. I’m including it here mainly so you recognize it if you see it in older Dockerfiles or scripts.
Read-Only Sharing
Often, only one container should be allowed to write, while others should only read. You can enforce this at the mount level:
docker run -d --name writer -v shared-data:/data busybox \
sh -c "date > /data/status.txt && sleep 3600"
docker run --rm -v shared-data:/data:ro busybox \
sh -c "echo test > /data/status.txt"
Expected output:
sh: can't create /data/status.txt: Read-only file system
This is a simple but effective way to prevent accidental writes from a container that should only be consuming data — for example, a metrics scraper reading application logs.
tmpfs Mounts: In-Memory Sharing Within a Pod-Like Setup
If the data being shared is sensitive or purely transient (like a socket file or a short-lived cache), tmpfs avoids touching disk entirely:
docker run -d --name cache-writer \
--tmpfs /cache:rw,size=64m \
busybox sh -c "echo cached > /cache/data && sleep 3600"
Note that tmpfs mounts are not shareable between separate containers the way named volumes are — each container gets its own tmpfs instance. tmpfs sharing really only makes sense within a single Kubernetes Pod where containers share the same tmpfs volume definition (see the Kubernetes section below).
Real-World Example: Nginx Serving Files Written by an App Container
A pattern I use often: an application container generates static assets (build output, reports, generated images), and an Nginx container serves them.
docker volume create web-assets
docker run -d --name asset-builder \
-v web-assets:/build \
node:20-alpine sh -c "echo '<h1>Built by app container</h1>' > /build/index.html && sleep 3600"
docker run -d --name web \
-v web-assets:ro,volume-opt=/build \
-v web-assets:/usr/share/nginx/html:ro \
-p 8080:80 \
nginx:alpine
Now visiting http://localhost:8080 serves the file the app container wrote — no copying, no shared network storage, just the same volume mounted twice.
Docker Compose Example
In practice, most people wire this up through Compose rather than raw docker run:
version: "3.9"
services:
builder:
image: node:20-alpine
command: sh -c "echo '<h1>Hello from builder</h1>' > /build/index.html && sleep 3600"
volumes:
- web-assets:/build
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- web-assets:/usr/share/nginx/html:ro
depends_on:
- builder
volumes:
web-assets:
docker compose up -d
curl http://localhost:8080
Expected output:
<h1>Hello from builder</h1>
Kubernetes Equivalent
If you’re running on Kubernetes instead of plain Docker, the sharing pattern maps onto emptyDir (for containers within the same Pod) or a shared PersistentVolumeClaim (for cross-Pod sharing).
Same-Pod sharing with emptyDir:
apiVersion: v1
kind: Pod
metadata:
name: shared-data-pod
spec:
containers:
- name: writer
image: busybox
command: ["sh", "-c", "date >> /data/log.txt; sleep 3600"]
volumeMounts:
- name: shared-volume
mountPath: /data
- name: reader
image: busybox
command: ["sh", "-c", "sleep 10 && cat /data/log.txt"]
volumeMounts:
- name: shared-volume
mountPath: /data
volumes:
- name: shared-volume
emptyDir: {}
Cross-Pod sharing needs a PersistentVolumeClaim backed by a storage class that supports ReadWriteMany access mode (like NFS or a cloud file store — hostPath and most block storage classes only support ReadWriteOnce).
Internal Working: What Actually Happens at the Kernel Level
When Docker mounts a volume into a container, it’s issuing a bind mount system call under the hood, associating the volume’s directory on the host with a path inside the container’s mount namespace. Because Linux mount namespaces are just views into the same underlying filesystem, two containers with the same volume bind-mounted are, at the kernel level, looking at identical inodes. There is no synchronization lag, no eventual consistency — writes from one container are immediately visible to the other, exactly like two processes on the same host writing to the same directory (because that’s essentially what’s happening).
This is worth knowing because it explains both the power and the danger of shared volumes: concurrent writers can race, corrupt files, or step on each other exactly as they would outside containers. Docker doesn’t add any locking or coordination — that’s still your application’s responsibility.
Security Considerations
A few things I always check before using shared volumes in anything beyond local development:
- File ownership and UID/GID mismatches. If your writer container runs as UID 1000 and your reader runs as UID 999, you can end up with permission errors even though both containers can technically see the volume. Standardize UIDs across images that share a volume, or set explicit permissions after first write.
- Avoid mounting the Docker socket as a “shared data” shortcut. I’ve seen people bind-mount
/var/run/docker.sockto let one container control others. That’s a container-escape risk, not a data-sharing pattern — don’t conflate the two. - Use
:rowherever a container doesn’t need to write. Least privilege applies to filesystems too. - Be careful with bind mounts of sensitive host paths (like
/etcor/). A misconfigured bind mount can expose host files to a container that shouldn’t see them.
Best Practices
- Prefer named volumes over bind mounts for anything that isn’t local development — they’re portable and Docker-managed.
- Use
--mountover-vin scripts and Compose files for clarity, even though-vis fine for quick manual testing. - Mark shared volumes read-only wherever only one side needs write access.
- Don’t use shared volumes as a substitute for a proper message queue or database when multiple containers need to coordinate on the same data — file-based sharing has no locking guarantees.
- Label your volumes (
docker volume create --label) so you can find and clean them up later; orphaned volumes are one of the most common causes of disk bloat on long-running Docker hosts.
Troubleshooting
“Permission denied” when a container writes to a shared volume Check the UID the process runs as inside the container versus the ownership of the volume’s contents:
docker run --rm -v shared-data:/data busybox ls -la /data
Changes in one container aren’t visible in another Confirm both containers are actually referencing the same volume name (not two differently-named volumes, which is a very common typo-driven bug), using:
docker inspect writer --format '{{ json .Mounts }}'
docker inspect reader --format '{{ json .Mounts }}'
Volume seems to have disappeared after docker compose down By default, docker compose down does not remove named volumes — but docker compose down -v does. Double-check which flag was used before assuming data loss.
Monitoring Volume Usage
docker system df -v
This shows disk usage broken down by images, containers, and volumes, which is the fastest way to catch a volume silently growing out of control.
Summary
Container storage is isolated by default, and that’s a good thing for most workloads — but when you genuinely need two or more containers to see the same data, Docker gives you clean, well-tested primitives to do it. Named volumes are the right default for cross-container sharing because they’re portable and Docker-managed; bind mounts are great for development where you want host-editor convenience; tmpfs is for transient, memory-only data. Once you understand that a shared volume is really just a bind mount at the kernel level, the behavior — and the pitfalls around concurrent writes and permissions — becomes much easier to reason about.
References
- Docker Docs — Manage data in Docker
- Docker Docs — Volumes
- Docker Docs — Bind mounts
- Docker Docs — tmpfs mounts
- Kubernetes Docs — Volumes
- Kubernetes Docs — Persistent Volumes
- CNCF — Cloud Native Storage Landscape
