In the previous tutorial, we used Docker Compose to run an app with a database. We briefly mentioned volumes and networking. Now let’s understand them properly.
These two topics answer two important questions:
- Volumes: How do I keep data when a container is removed?
- Networking: How do containers talk to each other?
Why Containers Lose Data
Containers are designed to be temporary. You can create them, destroy them, and replace them. This is a feature, not a bug — it makes containers predictable and easy to deploy.
But it means that anything you write inside a container is lost when the container is removed.
Let’s prove it:
# Start an Ubuntu container
docker run -it --name test ubuntu bash
# Inside the container, create a file
echo "important data" > /myfile.txt
cat /myfile.txt
# Output: important data
# Exit the container
exit
# Remove the container
docker rm test
# Start a new container
docker run -it --name test ubuntu bash
# The file is gone
cat /myfile.txt
# Output: cat: /myfile.txt: No such file or directory
The file was stored inside the container’s writable layer. When we removed the container, the layer was deleted, and the file disappeared.
For databases, logs, uploaded files, and configuration, you need volumes.
What Are Volumes?
A volume is a directory on the host machine that Docker manages. When you mount a volume into a container, the data is stored on the host — not inside the container. If the container is removed, the data stays.
Without Volume With Volume
┌─────────────┐ ┌─────────────┐
│ Container │ │ Container │
│ │ │ /app/data ──┼──┐
│ data is │ │ │ │
│ inside │ └─────────────┘ │
│ container │ │
└─────────────┘ ┌─────────────────▼┐
↓ │ Volume (on host) │
container removed │ Data survives! │
↓ └──────────────────┘
data is gone
There are two types of mounts: named volumes and bind mounts.
Named Volumes
Named volumes are managed entirely by Docker. You create them, give them a name, and Docker stores the data in a location it manages (usually /var/lib/docker/volumes/ on Linux).
Creating a Named Volume
docker volume create mydata
Using a Named Volume
docker run -v mydata:/app/data ubuntu bash
This mounts the volume mydata at /app/data inside the container. Anything written to /app/data is stored in the volume.
Let’s test it:
# Start container and write data
docker run -it --name writer -v mydata:/app/data ubuntu bash -c "echo 'Hello from volume' > /app/data/test.txt"
# Remove the container
docker rm writer
# Start a new container with the same volume
docker run -it --name reader -v mydata:/app/data ubuntu bash -c "cat /app/data/test.txt"
# Output: Hello from volume
The data survived because it is in the volume, not the container.
Managing Volumes
# List all volumes
docker volume ls
# Inspect a volume (shows where data is stored)
docker volume inspect mydata
# Remove a volume (data is deleted!)
docker volume rm mydata
# Remove all unused volumes
docker volume prune
Named Volumes in Docker Compose
This is the most common way to use named volumes:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: alex
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
The volumes: section at the bottom declares the named volume. The volumes: under the service mounts it. PostgreSQL stores its data at /var/lib/postgresql/data, so this volume keeps your database safe.
Important: Running docker compose down does not delete volumes. Running docker compose down -v does. Be careful with the -v flag.
Bind Mounts
Bind mounts link a specific directory on your computer to a directory inside the container. Unlike named volumes, you choose exactly where the data lives.
docker run -v /Users/alex/code/my-app/src:/app/src node:20-alpine
This mounts /Users/alex/code/my-app/src from your computer at /app/src in the container.
Bind Mounts in Docker Compose
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- ./src:/app/src
The ./src:/app/src syntax maps the src folder on your computer to /app/src in the container. When you edit files in ./src, the changes appear inside the container immediately.
This is perfect for development — you can edit code on your computer and see changes inside the container without rebuilding the image.
Named Volumes vs Bind Mounts
| Feature | Named Volume | Bind Mount |
|---|---|---|
| Managed by | Docker | You |
| Location on host | Docker decides | You choose |
| Portable | Yes | No (paths differ per OS) |
| Best for | Databases, persistent data | Development, sharing source code |
| Syntax in Compose | myvolume:/path | ./local/path:/container/path |
Rule of thumb: Use named volumes for data that containers generate (databases, caches). Use bind mounts for data you create (source code, config files).
Docker Networking
Now let’s talk about how containers communicate with each other and with the outside world.
The Default Bridge Network
When you start a container, Docker connects it to a default bridge network. Containers on the same bridge network can reach each other by IP address.
# Start two containers
docker run -d --name container-a nginx
docker run -d --name container-b nginx
# Find the IP of container-a
docker inspect container-a | grep IPAddress
# Output: "IPAddress": "172.17.0.2"
# Ping container-a from container-b
docker exec container-b ping 172.17.0.2
This works, but using IP addresses is fragile. IPs can change when containers restart.
Custom Networks — Service Names as Hostnames
A better approach is to create a custom network. On custom networks, containers can reach each other by name.
# Create a custom network
docker network create my-network
# Start containers on the same network
docker run -d --name web --network my-network nginx
docker run -d --name api --network my-network nginx
# Now "web" can reach "api" by name
docker exec web ping api
# Output: PING api (172.18.0.3): 56 data bytes ...
The container name becomes the hostname. This is much more reliable than using IP addresses.
Networking in Docker Compose
Docker Compose creates a custom network automatically. All services in the same docker-compose.yml can reach each other by service name.
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://alex:secret@db:5432/mydb
# ^^
# service name = hostname
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: alex
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
The app connects to the database using db as the hostname. Docker Compose resolves db to the correct container IP automatically.
This is one of the biggest benefits of Compose — you do not need to manage networks manually.
Network Types
Docker has three network types:
| Type | Description | Use Case |
|---|---|---|
bridge | Default. Containers communicate via an internal network | Most applications |
host | Container shares the host’s network. No port mapping needed | High-performance apps |
none | No networking. Container is completely isolated | Security-sensitive tasks |
For most applications, the default bridge network (or the auto-created Compose network) is what you need.
Using Host Networking
docker run --network host nginx
With host networking, the container does not get its own IP. It uses the host’s network directly. If Nginx listens on port 80, it is available on port 80 of the host — no -p flag needed.
Host networking is slightly faster because it skips the network layer. But it means the container’s ports can conflict with the host’s ports.
Exposing Ports vs Internal Communication
There is an important difference between ports you expose and communication that happens inside Docker.
services:
app:
build: .
ports:
- "3000:3000" # Exposed to your computer
db:
image: postgres:16-alpine
# No ports section — only accessible inside Docker
In this example:
appis accessible from your browser athttp://localhost:3000dbis not accessible from your computerappcan connect todbon port 5432 using the service name
If you add ports: - "5432:5432" to the db service, the database becomes accessible from your computer too. This is useful during development (for example, to connect a database GUI). But in production, you usually do not expose the database port.
Practical Example: Full Setup
Let’s put volumes and networking together. Here is a setup with a web app, a database with persistent storage, and a Redis cache:
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://alex:secret@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
volumes:
- ./src:/app/src # Bind mount for development
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: alex
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data # Named volume for data
cache:
image: redis:7-alpine
volumes:
- redisdata:/data # Named volume for cache data
volumes:
pgdata:
redisdata:
How the networking works:
Your Computer Docker Network
┌─────────────────┐ ┌─────────────────────────┐
│ │ │ │
│ Browser │◄──3000────►│ app (port 3000) │
│ │ │ │ │ │
│ │ │ │5432 │6379 │
│ │ │ ▼ ▼ │
│ │ │ db cache │
│ │ │ │
└─────────────────┘ └─────────────────────────┘
- Your browser reaches the app through port 3000
- The app reaches the database by its service name
dbon port 5432 - The app reaches Redis by its service name
cacheon port 6379 - The database and Redis are not accessible from your computer (no ports mapped)
- Database data is safe in the
pgdatavolume - Redis data is safe in the
redisdatavolume - Your source code is synced via a bind mount
Inspecting Networks
Sometimes you need to debug networking. Here are useful commands:
# List all networks
docker network ls
# Inspect a network (shows connected containers)
docker network inspect my-project_default
# Check which networks a container is connected to
docker inspect app --format '{{json .NetworkSettings.Networks}}'
When you use Docker Compose, the network is named <project>_default. The project name is usually the folder name.
Common Mistakes
1. Losing data by not using volumes. Databases, uploaded files, and logs should always use volumes. Without a volume, removing the container deletes everything. Check docker compose down -v carefully — the -v removes volumes too.
2. Port conflicts. If two containers (or a container and a host service) try to use the same port, you get an error. Check which ports are in use with docker compose ps or lsof -i :5432 on the host.
3. Using IP addresses instead of service names. Container IPs change when containers restart. Always use service names in Compose. Instead of postgres://172.17.0.3:5432, use postgres://db:5432.
What We Learned
In this tutorial, you learned:
- Containers lose data when removed — volumes keep data safe
- Named volumes are managed by Docker — best for databases and persistent data
- Bind mounts link a host directory to a container — best for development
- Docker creates a default bridge network for containers
- On custom networks (or Compose networks), containers reach each other by service name
- Docker Compose creates a network automatically for all services
- Only expose ports you need — internal services do not need port mapping
Related Articles
- Docker Tutorial #1: What is Docker — containers, images, and registries
- Docker Tutorial #2: Dockerfile — building custom images
- Docker Tutorial #3: Docker Compose — running multiple containers
- Docker Cheat Sheet — quick reference for all Docker commands
What’s Next?
In the next tutorial, we will learn how to deploy with Docker — pushing images to a registry, deploying to a server, and running containers in production with health checks, restart policies, and security best practices.