Sharing Data Between Containers #
One of Docker’s strengths is running many containers in isolation. But there are situations where those containers must share data with each other — whether it’s a shared configuration file, static assets, output from one service that becomes input for another, or shared data accessed by several workers.
Sharing data between containers isn’t hard, but there are many ways to do it and each has trade-offs. The wrong choice can cause performance problems, data leaks between tenants, or fragile configuration.
This article covers all the available methods, their use cases, and the best practices that will help you choose the right approach for every situation.
Why Do Containers Need to Share Data? #
By default, containers are completely isolated from each other. Filesystems, networks, and process spaces are separated by Linux namespaces. But there are many real scenarios where full isolation is too rigid.
Example use cases:
- Web app + logger sidecar — the application container writes logs to a directory, and a sidecar container (e.g. Filebeat or Promtail) reads and ships the logs to centralized logging.
- Web app + nginx reverse proxy — nginx reads static files (images, CSS, JS) from a directory also written or copied by the web app.
- Multi-service database clusters — several database nodes share the same data directory (though this is usually better with shared network storage).
- Data processor + analyzer — a data pipeline where the first container writes results, then the second reads and analyzes them.
- Shared config — several services read the same configuration (e.g.
nginx.confor TLS certificate files) from one location.
flowchart LR
subgraph App["App Container"]
W[Web App]
end
subgraph Log["Logger Container"]
F[Filebeat]
end
subgraph "Data (Shared Volume logs/)"
L1[app.log]
end
W -->|writes| L1
L1 -->|reads| F
F -->|sends| Ext[(Elasticsearch)]In the diagram above, logs/ is a shared volume that the app can write and the logger can read. The two are isolated in every other way, but share the filesystem for specific data.
Methods for Sharing Data #
There are several methods for sharing data between containers in Docker, each with different characteristics.
Volumes #
Volumes are Docker’s recommended data sharing mechanism for almost every case. Volumes are managed by Docker, stored in /var/lib/docker/volumes/, and mounted to specific paths inside containers.
Characteristics:
- Isolated from the host filesystem — no dependence on the host’s directory structure.
- Portable — can be backed up, restored, and moved between hosts.
- SAFE FOR PRODUCTION — managed by Docker, supports drivers for distributed storage.
Creating and sharing volumes:
# Create a shared volume
docker volume create shared-data
# Run the first container that writes to the volume
docker run -d --name producer -v shared-data:/data alpine sh -c "echo hello > /data/message.txt"
# Run a second container that reads from the volume
docker run --rm --volumes-from producer alpine cat /data/message.txt
# Output: hello
--volumes-from is the old way to share volumes between containers. The modern way is to explicitly mount the same volume in both containers.
docker run -d --name producer -v shared-data:/data alpine sleep 3600
docker run -d --name consumer -v shared-data:/data alpine sleep 3600
Both containers see the same /data directory. Changes from one container are immediately visible in the other.
Bind Mounts #
Bind mounts mount a directory or file from the host directly into the container. Unlike volumes, bind mounts depend on the host structure.
Characteristics:
- Dependent on host paths — not portable between hosts with different directory layouts.
- High performance — native filesystem access, good for development.
- Not suitable for multi-host production — unless mounted from a shared filesystem (NFS, GlusterFS).
# Mount the same host directory into two containers
docker run -d --name producer -v /opt/shared:/data alpine sleep 3600
docker run -d --name consumer -v /opt/shared:/data alpine sleep 3600
The most common use: development workflows where host source code is mounted into the container. A host code editor edits files, the container app immediately sees the changes.
tmpfs Mounts #
tmpfs stores data in memory (RAM), not on disk. Data disappears when the container stops.
Characteristics:
- Very fast — data in memory.
- Ephemeral — lost on container restart.
- Not natively shared between containers — but can be used per-container with different data.
# tmpfs for one container, 100MB
docker run -d --tmpfs /tmp:size=100m alpine sleep 3600
Use case: temporary data that needs speed and doesn’t need persistence. Examples: caches, session data, temporary processing files.
Important note: tmpfs doesn’t truly “share” data between containers — each container has its own tmpfs. If you need to share data, volumes or bind mounts are the choice.
Containers as Volume Sources #
Docker also lets you mount filesystems from one container into another, without creating a named volume.
# First container as a "data container"
docker create --name data-container -v /data alpine true
# Second container mounts from the data container
docker run --volumes-from data-container alpine ls /data
This pattern was once popular, but is now less recommended because named volumes are more explicit and easier to manage. --volumes-from still works but is considered legacy.
Method Comparison #
Choosing the right method for the right situation is an important skill.
| Method | Performance | Portability | Production Safe | Best for |
|---|---|---|---|---|
| Named Volume | High | Very high | Yes | Almost every production case |
| Bind Mount | Very high (native) | Low (depends on host paths) | Be careful | Development, config needing direct host access |
| tmpfs | Very high (RAM) | Not portable | Yes (for ephemeral data) | Caches, sessions, temporary data |
| volumes-from | High | Medium | Legacy | Old code, gradual migration |
General recommendation: Use named volumes as the default. Bind mounts specifically for development where you need direct access to host source code. tmpfs for ephemeral data needing high performance. Avoid --volumes-from in new code.Advanced Usage Patterns #
The Sidecar Pattern #
A sidecar is a pattern where one container “attaches” to a main container to perform a specific task. One of the most common sidecar use cases is shared logging.
# docker-compose.yml
version: "3.8"
services:
app:
image: myapp:latest
volumes:
- logs:/app/logs
log-shipper:
image: filebeat:8
user: root
volumes:
- logs:/var/log/app:ro
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
depends_on:
- app
volumes:
logs:
Here, app writes logs to /app/logs, and log-shipper mounts the same directory read-only (ro). Filebeat reads the logs and ships them to Elasticsearch or another logging system.
Advantages of this pattern:
- The application doesn’t need to know about the logging infrastructure.
- The log shipper can be restarted or replaced without disturbing the application.
- Clean separation of concerns.
The Init Container Pattern #
Init containers run before the main container and prepare the data it needs.
# Kubernetes pod spec
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
initContainers:
- name: config-loader
image: config-loader:latest
volumeMounts:
- name: config
mountPath: /config
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: config
mountPath: /app/config
volumes:
- name: config
emptyDir: {}
The init container fills the config volume, then the main container mounts the same volume to read the configuration. This pattern is common in Kubernetes and can be adopted in Docker Compose for the same workflow.
Shared Caches #
Several containers benefit from a shared cache (e.g. build caches or download caches).
# docker-compose.yml for a build pipeline
services:
builder:
image: golang:1.22
volumes:
- go-cache:/root/.cache/go-build
- go-mod:/go/pkg/mod
command: go build ./...
tester:
image: golang:1.22
volumes:
- go-cache:/root/.cache/go-build
- go-mod:/go/pkg/mod
command: go test ./...
volumes:
go-cache:
go-mod:
The build cache go-cache and module cache go-mod are shared between the builder and tester containers. This saves download time and recompilation.
Best Practices #
Use Named Volumes for Almost Every Case #
Named volumes are the safe default. They’re portable, Docker-managed, and support drivers for advanced use cases (NFS, cloud storage, etc.).
# docker-compose.yml
services:
app:
volumes:
- app-data:/var/lib/app
volumes:
app-data:
Use Read-Only Mounts for Consumers #
If one container only needs to read data written by another, mount it as read-only. This reduces the risk of a consumer container accidentally modifying data.
# Producer (write)
docker run -d --name producer -v shared:/data writer-image
# Consumer (read-only)
docker run -d --name consumer -v shared:/data:ro reader-image
:ro is the read-only flag. The mount becomes unwritable, so the container can only read.
Avoid Sharing Rapidly Changing Filesystems #
If many containers read and write to the same volume at high throughput, consider shared network storage (NFS, GlusterFS, cloud storage) or a database, rather than a direct filesystem.
Shared filesystems aren’t optimal for workloads needing high consistency and locking. A database with proper transaction handling is far better suited for data that changes frequently and is accessed by many processes.
Give Volumes Clear Names #
# BAD: generic names
volumes:
- data:/data
- logs:/logs
# GOOD: names describing the contents
volumes:
- user-uploads:/var/lib/app/uploads
- app-logs:/var/log/app
- nginx-cache:/var/cache/nginx
Clean Up Unused Volumes #
Volumes not mounted to any container still use disk space. Docker doesn’t remove them automatically.
# List unused volumes
docker volume ls -f dangling=true
# Remove unused volumes
docker volume prune
docker volume prune deletes ALL unused volumes. Make sure no important data lives in the volumes to be removed. For production, it’s better to remove volumes one by one by name.Back Up Volumes Regularly #
Volumes contain persistent data. They need backing up like a database or any other disk. Several ways:
- Mount the volume into a backup container that runs
tarand stores it to S3 or other storage. - Use a tool like
docker-backuporresticwith Docker volume support. - For Kubernetes, use Velero or another backup tool.
# Back up a volume to a .tar.gz file
docker run --rm \
-v my-volume:/source:ro \
-v $(pwd)/backup:/backup \
alpine tar -czf /backup/my-volume-$(date +%F).tar.gz -C /source .
Anti-Patterns to Avoid #
Modifying Another Container’s Internal Structure #
# ANTI-PATTERN: container A modifies binaries in container B
services:
app:
volumes:
- /usr/bin/app
updater:
volumes:
- /usr/bin/app
command: cp /new-binary /usr/bin/app
This is very fragile. Container A might restart with a different image and the new binary disappears. For updates, rebuild the image, don’t patch from another container.
Relying on --volumes-from in New Code
#
The --volumes-from pattern is legacy. For new code, use named volumes with explicit mounting.
Sharing Volumes Containing Database State #
Databases usually have their own concurrency control. Sharing database files between containers on the same filesystem without mature orchestration will cause data corruption.
Use replication or shared network storage with proper locking for multi-instance databases. Don’t share database files between containers directly.
Ignoring Permissions #
# ANTI-PATTERN: container A runs as root and writes files; container B runs as non-root and can't read them
docker run -d --name writer -v shared:/data alpine sh -c "echo data > /data/file"
docker run --rm -v shared:/data:ro alpine cat /data/file
# Output: cat: can't open '/data/file': Permission denied
Make sure the user IDs in both containers are consistent, or set the volume permissions correctly at initialization.
# SOLUTION: set permissions when creating the volume
docker run -d --name writer \
-v shared:/data \
alpine sh -c "chown 1000:1000 /data && echo data > /data/file"
Real-World Example Cases #
Static Site Generator + Web Server #
A common pattern for static site generators (Hugo, Jekyll, Gatsby): one container builds, another serves.
# docker-compose.yml
services:
builder:
image: hugomods/hugo:base
volumes:
- ./site:/src
- public:/src/public
command: hugo --minify
server:
image: nginx:alpine
depends_on:
- builder
volumes:
- public:/usr/share/nginx/html:ro
ports:
- "8080:80"
volumes:
public:
The builder produces static files into the public volume, then nginx mounts the same volume (read-only) to serve them. When the build reruns, the files in the public volume update, but nginx doesn’t need a restart — it reads files from disk on every request.
Multi-Stage Builds with Artifact Sharing #
# Dockerfile
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o server
FROM alpine:3.19
COPY --from=builder /app/server /server
CMD ["/server"]
Multi-stage builds are “container sharing” at the image level — a build stage produces artifacts, then the production stage only copies the needed binary. This shrinks the final image size and ensures the production image only contains what’s needed at runtime.
Shared Configuration Directories #
For shared configuration (certificate files, nginx.conf, app.yml), one volume can hold all the files and be mounted into several services.
# Set up the config volume once
docker volume create app-config
docker run --rm -v app-config:/config -v ./configs:/source alpine cp -r /source/. /config/
# All services mount the same config
docker run -d --name app -v app-config:/etc/app:ro myapp
docker run -d --name proxy -v app-config:/etc/nginx:ro nginx
This pattern ensures all services see the same configuration. Updating config = updating the volume = restarting the services that need it (optional, if the service supports hot-reload).
Performance Considerations #
The data sharing method you choose affects performance. For certain workloads, the difference is significant.
Local filesystem vs network filesystem. Volumes hosted on a local filesystem are far faster than network filesystems (NFS, EFS, etc.). For databases or I/O-intensive workloads, local SSD or NVMe is the best choice.
Mount propagation. Docker has several mount propagation modes: rprivate (default), shared, slave, rshared, rslave. For most cases, the default is enough. For advanced cases like using a bind mount that must be visible in sub-mounts, you might need shared or rshared.
Volume drivers. Docker supports drivers for various backends: local (default), nfs, aws (EFS), gcs (Google Cloud Storage), azurefile. Choose the driver according to your performance and availability needs.
# docker-compose.yml with an NFS driver
volumes:
shared-data:
driver: local
driver_opts:
type: nfs
o: addr=10.0.0.5,rw
device: ":/path/to/dir"
Benchmark tip: For I/O-sensitive workloads, benchmark volumes before committing to a production setup. Create a container doing heavy reads/writes, measure throughput, and compare across drivers or storage options.
Summary #
- Containers are isolated by default, but there are many situations where they need to share data — sidecar logging, shared config, data pipelines, cache sharing, etc.
- Named volumes are the safe default — portable, Docker-managed, support drivers for distributed storage. Use them for almost every case.
- Bind mounts fit development (host source code) or config needing direct host access. Watch out for portability.
- tmpfs for ephemeral data needing high performance (caches, sessions, temporary files). Doesn’t truly “share” between containers.
- The sidecar pattern is the most common way to share logs: the app writes, the log shipper reads via the same volume.
- Read-only mounts for consumer containers that only need to read, for security.
- Back up volumes regularly — just like a database or any disk containing persistent data.
- Avoid sharing database files between containers without orchestration. Use replication or shared network storage.
- Watch permissions — make sure user IDs are consistent between containers sharing a volume.
- Don’t use
--volumes-fromin new code. Use named volumes with explicit mounting.