Docker Networking Guide

A comprehensive guide to understanding Docker networking, network modes, security configurations, and common pitfalls.


Table of Contents

  1. Network Modes Overview
  2. Mixing Network Modes in Docker Compose
  3. Network Security
  4. Accessing Internal Containers from Host
  5. Understanding 0.0.0.0 vs 127.0.0.1 in Containers
  6. Quick Reference

1. Network Modes Overview

What are the different Docker network modes and when should each be used?

Docker provides several network modes for different use cases:

ModeDescription
bridgeDefault. Containers get their own IP on an isolated network
hostContainer shares the host’s network stack directly
noneNo networking
overlayMulti-host networking (Swarm)
macvlanAssigns MAC address, container appears as physical device

When to Use Each Mode

Use CaseRecommended Mode
Most applicationsbridge
Need maximum network performancehost
Container needs to access all host portshost
Network isolation requiredbridge
Multiple containers need same internal portbridge

Visual Overview

┌─────────────────────────────────────────────────────────┐
│                        HOST                             │
│  ┌──────────────────────────────────────────────────┐   │
│  │              Host Network (eth0)                 │   │
│  │                    │                             │   │
│  │         ┌──────────┴──────────┐                  │   │
│  │         │                     │                  │   │
│  │   ┌─────▼─────┐         ┌─────▼─────┐            │   │
│  │   │ Container │         │   Host    │            │   │
│  │   │  (host    │         │  Services │            │   │
│  │   │   mode)   │         │           │            │   │
│  │   └───────────┘         └───────────┘            │   │
│  └──────────────────────────────────────────────────┘   │
│                                                         │
│  ┌──────────────────────────────────────────────────┐   │
│  │           Bridge Network (docker0)               │   │
│  │                    │                             │   │
│  │    ┌───────────────┼───────────────┐             │   │
│  │    │               │               │             │   │
│  │ ┌──▼───┐       ┌───▼──┐       ┌───▼──┐           │   │
│  │ │ web  │       │  db  │       │ app  │           │   │
│  │ └──────┘       └──────┘       └──────┘           │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

2. Mixing Network Modes in Docker Compose

Can I have some containers running in bridge mode while others use host mode in the same Docker Compose file?

Yes! You can use different network modes for different services in the same compose file.

Example Configuration

services:
  # Service using default bridge network
  webapp:
    image: nginx
    networks:
      - my_bridge_network

  # Service using host networking
  monitoring:
    image: prometheus
    network_mode: host

  # Another service on bridge network
  database:
    image: postgres
    networks:
      - my_bridge_network

networks:
  my_bridge_network:

Important Limitations with Host Mode

When using network_mode: host:

Communication Between Modes

Important: A container in host mode and a container in bridge mode cannot communicate via Docker’s internal DNS (service names).

The host-mode container would need to connect via:


3. Network Security

3.1 Basic Principle: Don’t Publish Ports

services:
  # ✅ SECURE - No ports exposed to outside world
  database:
    image: postgres
    networks:
      - internal_net
    # No "ports:" section = not accessible from outside

  # ❌ EXPOSED - This publishes port to host (and potentially outside)
  webapp:
    image: nginx
    ports:
      - "8080:80"  # Accessible from outside!
    networks:
      - internal_net

networks:
  internal_net:

3.2 Internal Networks

Use internal: true to completely block external access:

services:
  database:
    image: postgres
    networks:
      - internal_net  # Only on internal network

  nginx:
    image: nginx
    ports:
      - "80:80"  # Only nginx exposed to outside
    networks:
      - internal_net

networks:
  internal_net:
    internal: true  # 🔒 No external connectivity AT ALL


┌───────────────────────────────────────────────────┐
│                     HOST                          │
│                                                   │
│   ┌─────────────────────────────────────────┐     │
│   │     Internal Network (internal: true)   │     │
│   │                                         │     │
│   │   ┌─────────┐         ┌─────────┐       │     │
│   │   │  nginx  │◄───────►│   db    │       │     │
│   │   │ :80     │         │  :5432  │       │     │
│   │   └────┬────┘         └─────────┘       │     │
│   │        │                   ✗            │     │
│   └────────│───────────────────│────────────┘     │
│            │                   │                  │
│            ▼                   ▼                  │
│     Port 80 exposed      No external access       │
└───────────────────────────────────────────────────┘

3.3 Security Comparison Table

ConfigurationAccessible From
ports: "8080:80"Internet, Host, Containers
ports: "127.0.0.1:8080:80"Host only, Containers
No ports (bridge)Other containers on same network, Host (via container IP)
No ports + bridge + internal: trueOnly containers on same network

3.4 Security Checklist

services:
  secure_service:
    image: myapp
    # ✅ No "ports:" - not exposed externally
    # ✅ No "network_mode: host" - isolated from host
    networks:
      - secure_net

networks:
  secure_net:
    internal: true  # ✅ Extra isolation

3.5 Security Goals Quick Reference

GoalConfiguration
Container-to-container onlyBridge + no ports + internal: true
Container + host onlyBridge + no ports (or 127.0.0.1:port:port)
Expose to outsideBridge + ports:

4. Accessing Internal Containers from Host

Even without published ports, the host can still reach containers on bridge networks.

Method 1: Using Container IP Directly

# Find the container's IP address
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container_name

# Or for docker-compose services
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' myproject_db_1

# Now access directly from host
curl http://172.18.0.2:5432
psql -h 172.18.0.2 -U postgres

Method 2: Using docker exec

# Interactive shell
docker exec -it myproject_db_1 bash

# Run specific command
docker exec myproject_db_1 psql -U postgres -c "SELECT 1"

# For docker-compose
docker-compose exec database psql -U postgres

Method 3: List All Container IPs on a Network

# See all containers and their IPs on a specific network
docker network inspect bridge --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{println}}{{end}}'

# Output:
# myproject_web_1: 172.18.0.3/16
# myproject_db_1: 172.18.0.2/16
# myproject_redis_1: 172.18.0.4/16

Method 4: Bind to Localhost Only

If you want easy host access but no external exposure:

services:
  database:
    image: postgres
    ports:
      - "127.0.0.1:5432:5432"  # Only accessible from host localhost
# Works from host
psql -h 127.0.0.1 -U postgres

# Does NOT work from other machines
# (port not bound to external interfaces)

Practical Complete Example

services:
  nginx:
    image: nginx
    ports:
      - "80:80"  # Public access

  app:
    image: myapp
    ports:
      - "127.0.0.1:3000:3000"  # Host-only access for debugging
    environment:
      - DATABASE_URL=postgres://database:5432/mydb

  database:
    image: postgres
    environment:
      - POSTGRES_PASSWORD=secret
    # No ports - container access only

  redis:
    image: redis
    # No ports - container access only
ServiceHow to Access from Host
nginxcurl http://localhost:80
appcurl http://127.0.0.1:3000
databasepsql -h 172.18.0.x -U postgres or docker-compose exec database psql -U postgres
redisdocker-compose exec redis redis-cli

Important Note About internal: true

When using internal: true, the host CAN still access containers via their IP:

# This still works even with internal: true
curl http://172.18.0.2:8080

The internal: true flag only prevents:

Does NOT block host → container communication.

Utility Script: List All Container IPs

Save this as docker-ips.sh:

#!/bin/bash
# List all running containers with their IPs
docker inspect -f '{{.Name}} - {{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -q)
$ ./docker-ips.sh
/myproject_web_1 - 172.18.0.3
/myproject_db_1 - 172.18.0.2
/myproject_redis_1 - 172.18.0.4

5. Understanding 0.0.0.0 vs 127.0.0.1 in Containers

Why does binding to 127.0.0.1 inside a container prevent external access?

When creating a server in Node.js (or any technology), you need to listen on 0.0.0.0 to make it accessible from outside the container. Listening on 127.0.0.1 doesn’t work.

Each container has its own isolated network interfaces. The container’s 127.0.0.1 is NOT the host’s 127.0.0.1.

┌─────────────────────────────────────────────────────────────┐
│                         HOST                                │
│                                                             │
│   Network Interfaces:                                       │
│   - eth0: 192.168.1.100 (external)                          │
│   - lo: 127.0.0.1 (loopback)                                │
│   - docker0: 172.17.0.1 (bridge)                            │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐   │
│   │                    CONTAINER                        │   │
│   │                                                     │   │
│   │   Network Interfaces:                               │   │
│   │   - eth0: 172.17.0.2 (bridge)                       │   │
│   │   - lo: 127.0.0.1 (container's own loopback)        │   │
│   │                                                     │   │
│   └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Binding to 127.0.0.1 (Loopback Only)

// Node.js server
app.listen(3000, '127.0.0.1');
┌─────────────────────────────────────────────────────────┐
│                      CONTAINER                          │
│                                                         │
│   ┌─────────────┐           ┌─────────────┐             │
│   │   eth0      │           │     lo      │             │
│   │ 172.17.0.2  │           │ 127.0.0.1   │             │
│   │             │           │   ▲         │             │
│   │   Traffic   │           │   │ Server  │             │
│   │   arrives   │           │   │ listens │             │
│   │   here      │           │   │ here    │             │
│   └─────────────┘           └─────────────┘             │
│         ▲                                               │
│         │                                               │
│    Port mapping                                         │
│    from host                                            │
└─────────────────────────────────────────────────────────┘

❌ External traffic arrives on eth0, but server only listens on lo. Connection refused!

Binding to 0.0.0.0 (All Interfaces)

// Node.js server
app.listen(3000, '0.0.0.0');
// Or simply
app.listen(3000);  // Defaults to 0.0.0.0 in most frameworks
┌─────────────────────────────────────────────────────────┐
│                      CONTAINER                          │
│                                                         │
│   ┌─────────────┐           ┌─────────────┐             │
│   │   eth0      │           │     lo      │             │
│   │ 172.17.0.2  │           │ 127.0.0.1   │             │
│   │      ▲      │           │      ▲      │             │
│   │      │      │           │      │      │             │
│   │      └──────┴───────────┴──────┘      │             │
│   │                  │                    │             │
│   │            Server listens             │             │
│   │            on 0.0.0.0                 │             │
└─────────────────────────────────────────────────────────┘

✅ Server listens on all interfaces, including eth0 where external traffic arrives.

Code Examples

❌ Won’t Work From Outside Container

// Node.js / Express
app.listen(3000, '127.0.0.1');  // Only loopback
# Python / Flask
app.run(host='127.0.0.1', port=3000)  # Only loopback
// Go
http.ListenAndServe("127.0.0.1:3000", nil)  // Only loopback

✅ Works From Outside Container

// Node.js / Express
app.listen(3000, '0.0.0.0');
// or simply
app.listen(3000);
# Python / Flask
app.run(host='0.0.0.0', port=3000)
// Go
http.ListenAndServe(":3000", nil)  // Empty host = all interfaces

Common Gotcha in Development

Many frameworks default to 127.0.0.1 in development mode:

# These often default to localhost/127.0.0.1
npm run dev      # Vite, Next.js, etc.
flask run
rails server

Override the host:

vite --host 0.0.0.0
next dev -H 0.0.0.0
flask run --host=0.0.0.0
rails server -b 0.0.0.0

Summary Table

Bind AddressListens OnAccessible From
127.0.0.1Loopback onlyInside same container only
0.0.0.0All interfacesOutside container (host, other containers, network)
172.17.0.2 (specific IP)That interface onlyAnywhere that can reach that IP

6. Quick Reference

Network Mode Selection

NeedUse
Standard isolationbridge (default)
Maximum performancehost
Complete isolationnone
Block outbound + isolatebridge + internal: true
Multi-host communicationoverlay
Direct LAN access with own IPmacvlan

Port Binding Security

BindingAccessibility
"8080:80"Public (all interfaces)
"127.0.0.1:8080:80"Host only
No portsContainer network only

Server Binding

AddressResult
0.0.0.0All interfaces (accessible from outside)
127.0.0.1Loopback only (container internal)

Useful Commands

# Get container IP
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container_name

# List all containers on a network
docker network inspect network_name

# Execute command in container
docker-compose exec service_name command

A Note on Macvlan Mode

Macvlan allows containers to appear as physical devices on your network with their own MAC and IP addresses. This can be useful for:

However, macvlan has significant limitations:

For most use cases, bridge mode with proper port configuration is recommended.


Further Reading