The Moment Everything Clicked
I still remember the confusion on my face when my container couldn't reach another container. "They're on the same machine! They're both Docker! Why can't they talk?"
That was three years ago. Since then, I've helped dozens of teams debug networking issues that should have taken minutes but instead took hours. The problem? Most tutorials jump straight into bridge networks without explaining the mental model.
Let me save you the headache.
Understanding Docker's Network Model
Docker creates isolated network namespaces for containers. This means each container has its own network stack, IP address, and ports. By default, Docker provides several network drivers.
The Four Network Types You Need to Know
- bridge - The default network. Containers can communicate with each other and the host.
- host - Removes network isolation between container and host.
- overlay - Connects containers across multiple Docker hosts.
- none - Completely disables networking.
For most local development, you'll live in the bridge world. Let's explore that.
The Default Bridge: What You Need to Understand
When you run a container without specifying a network, Docker places it on the default bridge network. Here's the catch: containers on the default bridge can only communicate via IP addresses, not container names.
# This will FAIL - DNS resolution doesn't work on default bridge
docker run --name database postgres:15
docker run --name api myapi:latest
# From api container: ping database won't work!
This is why named networks exist.
Creating Your Own Bridge Network
Here's the fix - create a custom bridge network and DNS resolution just works:
# Create a custom bridge network
docker network create my-network
# Run containers on this network
docker run -d --name database --network my-network postgres:15
docker run -d --name redis --network my-network redis:alpine
docker run -d --name api --network my-network -p 3000:3000 myapi:latest
Now from the api container, you can reach services by name:
# These work!
docker exec api ping database
docker exec api ping redis
The docker-compose.yml Approach
In production-like setups, you'll use Docker Compose. Here's how networking works there:
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
depends_on:
- database
- redis
networks:
- app-network
database:
image: postgres:15
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- app-network
redis:
image: redis:alpine
networks:
- app-network
networks:
app-network:
driver: bridge
Key insight: Docker Compose automatically creates a network and connects all services to it. Services can reach each other by their service names.
Connecting Containers Across Compose Files
Sometimes you have multiple compose files that need to communicate. Here's how:
# In docker-compose.yml for frontend
services:
frontend:
build: ./frontend
networks:
- frontend-network
- backend-network
networks:
frontend-network:
external: true
backend-network:
driver: bridge
Create the external network first:
docker network create frontend-network
docker-compose up -d
Port Mapping: The Host vs Container Confusion
One common point of confusion: -p 3000:3000. What does that mean?
-p HOST_PORT:CONTAINER_PORT
- Container port (right side): The port inside the container listening for connections
- Host port (left side): The port on your host machine that forwards to the container
# Container listens on 5432 (PostgreSQL default)
# Host machine exposes it on 5433
ports:
- "5433:5432"
Debugging Container Networking
When things break, here are your debugging tools:
Check which networks a container is on
docker inspect container_name --format='{{json .NetworkSettings.Networks}}'
List all networks
docker network ls
Inspect a network
docker network inspect bridge
Test connectivity between containers
docker exec -it container_a ping container_b
Check DNS resolution
docker exec -it container_a nslookup container_b
A Real-World Scenario
Let's put it all together. Imagine you have a Node.js API that connects to PostgreSQL and Redis, with a frontend that talks to the API.
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: secret
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
api:
build: .
environment:
DATABASE_URL: postgresql://user:secret@postgres:5432/myapp
REDIS_URL: redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "3000:3000"
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- api
volumes:
postgres_data:
Notice how:
- Services communicate using their names (
postgres,redis,api) -
depends_onwithcondition: service_healthyensures proper startup order - Nginx reverse-proxies to the API
Quick Reference Cheat Sheet
# Create a network
docker network create my-network
# Run container on specific network
docker run --network my-network nginx
# Connect running container to network
docker network connect my-network container-name
# Disconnect from network
docker network disconnect my-network container-name
# Remove unused networks
docker network prune
The Bottom Line
Docker networking doesn't have to be mysterious. Here's the mental model:
- Custom bridge networks - Use these for most local development. They provide DNS resolution between containers.
- Service names as hostnames - In Docker Compose, the service name IS the hostname.
- Port mapping - Only needed when you want to expose services to the host.
-
Debug with inspect - When in doubt,
docker network inspectreveals everything.
Next time your containers can't talk to each other, you'll know exactly where to look.
What Docker networking challenges are you facing? Drop a comment below!
Top comments (0)