Container-to-Container Communication

Container-to-container communication is how one container reaches another container without routing through your laptop browser or a public port. It matters because real applications are usually made of several services: an API talks to PostgreSQL, a worker talks to Redis, and a frontend talks to a backend. Docker gives these services private networks and built-in DNS so they can find each other by name instead of by fragile IP addresses.

Overview: How Container Communication Works

A Docker container is more than a process from an image. The image provides read-only filesystem layers, Docker adds a thin writable container layer, and the daemon starts the process inside isolation boundaries such as namespaces and cgroups. One of those boundaries is the network namespace. Each container gets its own interfaces, routes, loopback device, and listening ports.

By default, standalone containers attach to Docker’s bridge networking system. The older built-in bridge network named bridge exists automatically, but serious container-to-container communication should use a user-defined bridge network. On a user-defined bridge, Docker provides an embedded DNS server. Containers on the same network can resolve each other by container name, by network alias, or in Compose by service name.

This is the central rule: containers that share a Docker network can talk to each other directly on the container port. They do not need -p. Port publishing with -p HOST_PORT:CONTAINER_PORT is for traffic entering from the host or outside the host. If an API container connects to a database container on the same network, it should use db:5432, not localhost:5432 and not the host’s published port.

Container IP addresses exist, but they are implementation details. Docker assigns an address when a container joins a network, and that address can change when the container is recreated. Names are the stable application interface. If you remove and recreate shop-db on the same network, another container that connects to hostname shop-db can continue using the same configuration.

Docker networks also provide isolation. A container attached only to frontend-net cannot directly reach a database attached only to backend-net. You can deliberately attach one service to both networks when it is supposed to bridge the two areas, such as an API that accepts frontend traffic and also queries the database.

On Docker Desktop for macOS and Windows, these Linux networking details live inside Docker Desktop’s Linux VM. The practical behavior is the same: service names work inside Docker networks, and published ports appear on localhost for host access. On native Linux Engine, you can also inspect the bridge interfaces and packet forwarding rules more directly.

Syntax

The main standalone command forms are:

docker network create NETWORK_NAME
docker run --network NETWORK_NAME --name CONTAINER_NAME IMAGE
docker network connect NETWORK_NAME CONTAINER_NAME
Form Meaning
docker network create app-net Create a user-defined bridge network named app-net.
--network app-net Attach a new container to app-net when it starts.
--name api Give the container a stable name that other containers can resolve on the same user-defined network.
--network-alias db-primary Add another DNS name for the container on that network.
docker network connect app-net api Attach an already-created container to another network.
docker network inspect app-net Show network driver, subnet, gateway, and attached containers.
ports: in Compose Publish a service to the host. This is not required for another service on the same Compose network.

In Compose, Docker creates a default project network automatically, and each service name becomes a DNS name on that network. A service named db is reachable as hostname db from other services in the same Compose project.

Examples

Example 1: An App Container Checks a Database by Name

Create a private network, start PostgreSQL on it, and use a second container to reach the database by container name:

docker network create shop-net
docker run -d --name shop-db --network shop-net -e POSTGRES_PASSWORD=changeme postgres:16-alpine
docker run --rm --network shop-net postgres:16-alpine pg_isready -h shop-db -U postgres

Output:

shop-net
8b2b0a8f2d13
shop-db:5432 - accepting connections

The checking container does not know the database IP address. It asks Docker DNS for shop-db, receives the current address for that container on shop-net, and connects to PostgreSQL’s normal container port 5432. No host port is published, so this database is private to containers on that network.

Example 2: Use a Network Alias

A network alias gives a container an extra DNS name. This is useful when application configuration expects a generic name such as cache:

docker network create aliases-net
docker run -d --name inventory-redis --network aliases-net --network-alias cache redis:7.2-alpine
docker run --rm --network aliases-net redis:7.2-alpine redis-cli -h cache ping

Output:

aliases-net
1e4ad9f24c55
PONG

The Redis container is named inventory-redis, but on aliases-net it also answers to cache. Aliases are scoped to the network, so another network could use the same alias for a different container without conflict.

Example 3: Compose Service Names

Compose handles the network and DNS names for multi-service projects:

services:
  api:
    image: alpine:3.20
    command: ["sleep", "3600"]
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: changeme
docker compose up -d
docker compose exec api getent hosts db
docker compose down

Output:

[+] Running 3/3
 ✔ Network container-communication_default  Created
 ✔ Container container-communication-db-1   Started
 ✔ Container container-communication-api-1  Started
172.19.0.2        db
[+] Running 3/3
 ✔ Container container-communication-api-1  Removed
 ✔ Container container-communication-db-1   Removed
 ✔ Network container-communication_default  Removed

The api service can resolve db because both services share the Compose project network. The file has no ports: entry for PostgreSQL because the API does not need the database exposed on the host. In a real app, the API would use a connection string such as postgres://postgres:changeme@db:5432/postgres.

How It Works Step by Step

  1. The Docker CLI sends the requested image, container name, environment, and network settings to the Docker daemon.
  2. The daemon ensures the image layers are present locally, pulling them from a registry if needed, then creates the container’s thin writable layer and runtime configuration.
  3. Docker creates a network endpoint for the container and connects it to the selected bridge network with a virtual Ethernet device.
  4. The container receives an IP address on that network. Docker records the container name and any aliases in its embedded DNS data for that network.
  5. When another container on the same network looks up shop-db, the DNS response points to the current container endpoint.
  6. The client container connects to the target container’s listening port directly across the bridge network.
  7. If you publish a port with -p or Compose ports:, Docker additionally creates host-side forwarding. That forwarding is separate from private container-to-container traffic.
  8. When a container is removed, Docker removes its network endpoint and DNS entry. The user-defined network remains until you remove it or run docker compose down for a Compose-created network.

Common Mistakes

Using localhost for Another Container

This is wrong inside a containerized API:

services:
  api:
    image: my-api:1.0
    environment:
      DATABASE_HOST: localhost
  db:
    image: postgres:16-alpine

Inside the API container, localhost means the API container itself. It does not mean the database container and it does not mean your host machine. Use the Compose service name instead:

services:
  api:
    image: my-api:1.0
    environment:
      DATABASE_HOST: db
  db:
    image: postgres:16-alpine

Publishing a Port for Private Traffic

This works, but it exposes PostgreSQL on the host even though only the API needs it:

services:
  api:
    image: my-api:1.0
  db:
    image: postgres:16-alpine
    ports:
      - "5432:5432"

For private service-to-service traffic, remove ports:. The API can still connect to db:5432 on the Compose network. Add ports: only when a host-side tool such as a database GUI must connect.

Hard-Coding Container IP Addresses

This is fragile because the address can change after recreation:

docker run --rm curlimages/curl:8.9.1 http://172.20.0.2:8080

The fix is to put both containers on a user-defined network and connect to a name such as api, db, or cache. Names describe the service role; IP addresses describe one temporary endpoint.

Relying on depends_on as a Readiness Check

Compose depends_on controls startup order, but a database may still need several seconds before it accepts connections. Applications should retry connections during startup, and database images can use health checks when orchestration needs readiness information.

Best Practices

  • Use user-defined bridge networks for standalone multi-container setups.
  • Use Compose service names for local multi-service applications.
  • Connect to container ports directly across the Docker network; do not route private traffic through published host ports.
  • Publish only services that host users, browsers, or host-side tools must reach.
  • Use stable names and aliases, not container IP addresses.
  • Keep frontends, APIs, databases, and administrative tools on separate networks when isolation helps.
  • Make applications retry dependent service connections during startup.
  • Use specific image tags such as postgres:16-alpine, redis:7.2-alpine, and alpine:3.20 instead of latest so examples and environments are reproducible.
  • Remember that EXPOSE is documentation and image metadata only. It does not publish a port and is not required for containers on the same network to communicate.
  • Use docker network inspect, docker compose ps, and service logs when debugging connectivity.

Practice Exercises

  1. Create a network named notes-net. Start redis:7.2-alpine as notes-cache, then use a one-off Redis container to run redis-cli -h notes-cache ping. Expected end state: the command prints PONG.
  2. Write a Compose file with web, api, and db services. Publish only web to the host. Expected end state: api can use hostnames web and db inside Docker, but the database has no host port.
  3. Create two networks, public-net and private-net. Attach an API container to both and a database container only to private-net. Hint: use docker network connect for the second network attachment.

Summary

  • Containers communicate privately when they share a Docker network.
  • User-defined bridge networks provide Docker DNS by container name and alias.
  • Compose creates a project network where service names are DNS names.
  • Use db:5432 from another container, not localhost:5432.
  • Port publishing is for host-to-container access, not required for container-to-container access.
  • Container IP addresses are temporary implementation details; names are the stable interface.
  • Network separation is a useful security and design tool for multi-service apps.