Containers are ephemeral. When a container stops, all data written inside it disappears. That is fine for stateless apps, but databases and file storage need to persist data.
Docker solves this with volumes. And Docker solves container communication with networks.
In this tutorial, you will learn both.
The Ephemeral Container Problem
When you run a PostgreSQL container and insert some rows, that data lives in the container’s writable layer. Stop the container, remove it, and start a new one — the data is gone.
docker run -d --name mydb postgres:17
# ... insert data ...
docker rm -f mydb
docker run -d --name mydb postgres:17
# Data is gone!
To persist data across container restarts and replacements, you need volumes.
Three Storage Options
Docker has three ways to mount storage into a container:
| Type | Description | Use Case |
|---|---|---|
| Named volume | Managed by Docker, stored in Docker’s data directory | Production databases, persistent data |
| Bind mount | Maps a host directory into the container | Development (sync code, config files) |
| tmpfs | In-memory, not written to disk | Temporary files, secrets in memory |
Named Volumes
A named volume is created and managed by Docker. Docker stores the data in its own directory (usually /var/lib/docker/volumes/ on Linux). You do not need to know where.
Create a volume:
docker volume create mydata
Use a volume when running a container:
docker run -d \
--name mydb \
-v mydata:/var/lib/postgresql/data \
postgres:17
The -v mydata:/var/lib/postgresql/data format is: volumeName:containerPath.
Now when you stop and remove the container, the data stays in the mydata volume. Create a new container with the same volume and the data is there.
Volume Commands
# List all volumes
docker volume ls
# Inspect a volume (see where data is stored)
docker volume inspect mydata
# Remove a volume
docker volume rm mydata
# Remove all unused volumes
docker volume prune
Volumes in Docker Compose
In Compose, declare volumes at the bottom of the file:
services:
db:
image: postgres:17
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
When you run docker compose down, volumes are not deleted. Data persists.
To also delete volumes:
docker compose down -v
Bind Mounts
A bind mount maps a directory on your host machine into the container. Changes on the host are immediately visible inside the container, and vice versa.
docker run -d \
--name myapp \
-v /home/alex/myproject:/app \
myapp:1.0
Or using the newer --mount syntax (more explicit):
docker run -d \
--name myapp \
--mount type=bind,source=/home/alex/myproject,target=/app \
myapp:1.0
Bind Mounts in Compose
services:
api:
build: .
volumes:
- ./src:/app/src # bind mount: host path : container path
- ./config:/app/config:ro # :ro = read-only
The ./src is relative to the docker-compose.yml file. Docker resolves it to an absolute path.
When to Use Bind Mounts
Use bind mounts in development to sync code changes into running containers. Combined with Compose Watch Mode (covered in Docker Tutorial #8), this gives you hot reload.
Do not use bind mounts in production. Use named volumes instead. Bind mounts create a dependency on the exact path on the host machine, which is not portable.
Sharing Volumes Between Containers
Multiple containers can use the same named volume:
services:
app:
image: myapp:1.0
volumes:
- shared_data:/data
backup:
image: backup-tool:1.0
volumes:
- shared_data:/data:ro # read-only for the backup container
volumes:
shared_data:
tmpfs Mounts
A tmpfs mount stores data in memory only. It is never written to disk:
docker run -d \
--tmpfs /tmp \
myapp:1.0
In Compose:
services:
app:
image: myapp:1.0
tmpfs:
- /tmp
- /run
Use tmpfs for temporary files, session data, or secrets you never want written to disk.
Docker Networking
Docker networking controls how containers communicate with each other and with the outside world.
Network Drivers
Docker supports several network drivers:
| Driver | Description | Use Case |
|---|---|---|
| bridge | Default. Containers on the same bridge network can communicate | Single-host development and production |
| host | Container shares the host network stack | High-performance, no network isolation |
| none | No networking | Maximum isolation |
| overlay | Multi-host networking for Docker Swarm | Distributed apps (Swarm only) |
For most use cases, you use the bridge driver.
Default Bridge Network vs User-Defined Bridge Networks
Docker has a default bridge network called bridge. All containers without a specified network are connected to it.
The problem: containers on the default bridge network cannot resolve each other by name. They can only communicate by IP address, which changes every time.
docker run -d --name web nginx
docker run -d --name api myapp:1.0
# Inside api container: cannot reach "web" by name
# You would need to use the web container's IP address
User-defined bridge networks solve this. Containers on a user-defined network can reach each other by service name:
# Create a user-defined network
docker network create mynetwork
# Connect containers to it
docker run -d --name web --network mynetwork nginx
docker run -d --name api --network mynetwork myapp:1.0
# Inside api container: "web" resolves to the web container's IP
curl http://web:80
Networks in Docker Compose
Docker Compose automatically creates a user-defined bridge network for your project. All services in the same Compose file are on the same network and can reach each other by service name.
services:
api:
build: .
# api can reach db as "db:5432"
db:
image: postgres:17
# api can connect with: postgresql://user:pass@db:5432/myapp
You can also define custom networks:
services:
api:
networks:
- frontend
- backend
db:
networks:
- backend # only reachable from api, not from web
web:
image: nginx:1.27
networks:
- frontend # only reachable from api
networks:
frontend:
backend:
This separates your network into a frontend (public-facing) and backend (database) segment.
Publishing Ports
By default, container ports are not accessible from the host. You must publish them with -p:
docker run -d -p 8080:80 nginx
# -p hostPort:containerPort
In Compose:
services:
web:
image: nginx:1.27
ports:
- "8080:80"
To listen only on localhost (not expose to external network):
docker run -d -p 127.0.0.1:8080:80 nginx
Network Commands
# List all networks
docker network ls
# Inspect a network (see connected containers, subnet, etc.)
docker network inspect bridge
# Create a network
docker network create mynetwork
# Connect a running container to a network
docker network connect mynetwork mycontainer
# Disconnect a container from a network
docker network disconnect mynetwork mycontainer
# Remove a network
docker network rm mynetwork
# Remove all unused networks
docker network prune
Docker Engine v29: nftables Support
Docker Engine v29 adds opt-in support for nftables as the firewall backend. By default, Docker still uses iptables.
To enable nftables, set in /etc/docker/daemon.json:
{
"firewall-backend": "nftables"
}
nftables is the modern Linux firewall subsystem, replacing iptables. If your Linux system uses nftables by default (Ubuntu 22.04+, Fedora), enabling this avoids conflicts between Docker’s iptables rules and your system’s nftables rules.
Note: The nftables backend is still experimental. It does not support Docker Swarm mode and has not yet migrated all overlay network rules. Test before enabling in production.
For most developers, the default (iptables) is fine.
Common Mistakes
Using bind mounts in production
Bind mounts depend on the exact path existing on the host machine. This is not portable. A new server won’t have the same path. Use named volumes in production instead.
Relying on the default bridge network for container communication
The default bridge network does not support DNS-based name resolution between containers. Always use user-defined networks (or Docker Compose, which creates one automatically).
Forgetting to remove orphan volumes
When you frequently create and destroy containers, unused volumes accumulate. Run docker volume prune periodically to clean up. Or use docker compose down -v during development to also remove volumes.
What’s Next?
Next: Docker Tutorial #7: Multi-stage Docker Builds for Production