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:

  1. Volumes: How do I keep data when a container is removed?
  2. 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

FeatureNamed VolumeBind Mount
Managed byDockerYou
Location on hostDocker decidesYou choose
PortableYesNo (paths differ per OS)
Best forDatabases, persistent dataDevelopment, sharing source code
Syntax in Composemyvolume:/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:

TypeDescriptionUse Case
bridgeDefault. Containers communicate via an internal networkMost applications
hostContainer shares the host’s network. No port mapping neededHigh-performance apps
noneNo networking. Container is completely isolatedSecurity-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:

  • app is accessible from your browser at http://localhost:3000
  • db is not accessible from your computer
  • app can connect to db on 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 db on port 5432
  • The app reaches Redis by its service name cache on port 6379
  • The database and Redis are not accessible from your computer (no ports mapped)
  • Database data is safe in the pgdata volume
  • Redis data is safe in the redisdata volume
  • 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

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.