DEV Community

Teguh Coding
Teguh Coding

Posted on

Docker Networking Explained: From Zero to Hero with Real Examples

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

  1. bridge - The default network. Containers can communicate with each other and the host.
  2. host - Removes network isolation between container and host.
  3. overlay - Connects containers across multiple Docker hosts.
  4. 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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now from the api container, you can reach services by name:

# These work!
docker exec api ping database
docker exec api ping redis
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create the external network first:

docker network create frontend-network
docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Port Mapping: The Host vs Container Confusion

One common point of confusion: -p 3000:3000. What does that mean?

-p HOST_PORT:CONTAINER_PORT
Enter fullscreen mode Exit fullscreen mode
  • 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"
Enter fullscreen mode Exit fullscreen mode

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}}'
Enter fullscreen mode Exit fullscreen mode

List all networks

docker network ls
Enter fullscreen mode Exit fullscreen mode

Inspect a network

docker network inspect bridge
Enter fullscreen mode Exit fullscreen mode

Test connectivity between containers

docker exec -it container_a ping container_b
Enter fullscreen mode Exit fullscreen mode

Check DNS resolution

docker exec -it container_a nslookup container_b
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

Notice how:

  1. Services communicate using their names (postgres, redis, api)
  2. depends_on with condition: service_healthy ensures proper startup order
  3. 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
Enter fullscreen mode Exit fullscreen mode

The Bottom Line

Docker networking doesn't have to be mysterious. Here's the mental model:

  1. Custom bridge networks - Use these for most local development. They provide DNS resolution between containers.
  2. Service names as hostnames - In Docker Compose, the service name IS the hostname.
  3. Port mapping - Only needed when you want to expose services to the host.
  4. Debug with inspect - When in doubt, docker network inspect reveals 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)