Intro
Docker networking decides how your services discover each other and how customers reach them. In this guide, you will:
- Understand bridge, host, and overlay networks
- Publish ports safely so traffic reaches the right container
- Use DNS between containers so services talk by name
- Define networks in Docker Compose for repeatable setups
- Troubleshoot broken connectivity fast
This is written for busy operators who want practical steps and copy-paste examples that work on real machines.
Workflow Overview
Use this sequence to choose and implement the right network mode:
- Clarify scope
- Single host: Start with a user-defined bridge network. It isolates containers and provides built-in DNS between them.
- Need bare-metal-like performance on Linux: Consider host networking. It removes NAT but exposes container ports directly on the host network stack.
- Multi-host: Use an overlay network with Swarm to connect services across nodes.
- Create the network
- Bridge (user-defined): docker network create app-net
- Host: no network create step; you attach with --network host
- Overlay: docker swarm init; then docker network create -d overlay --attachable app-ol
- Run services on the chosen network
- Bridge: attach with --network app-net or via Compose networks
- Host: run with --network host; ensure the app binds to 0.0.0.0
- Overlay: use services (docker service ...) or a stack file (docker stack deploy)
- Publish only what you need
- Bridge/overlay containers: publish host ports with -p or Compose ports
- Host network: skip -p; the container uses the host stack directly
- Verify and document
- From one container: resolve peer by name (curl http://api:8080)
- From host: hit published ports (curl http://localhost:8080)
- Note chosen ports and networks for reuse
Bridge networking in practice (local default)
A user-defined bridge network isolates traffic and enables service name DNS by default. Prefer this to the legacy default bridge called "bridge".
Create a network:
docker network create app-net
Run a database and API on the same network:
# PostgreSQL
docker run -d \
--name db \
--network app-net \
-e POSTGRES_PASSWORD=secret \
postgres:16
# Simple API listening on 8080 and using DATABASE_HOST=db
docker run -d \
--name api \
--network app-net \
-e DATABASE_HOST=db \
-p 8080:8080 \
myorg/api: latest
What you get:
- Container-to-container DNS: "db" resolves inside the network; the API can connect to db:5432
- Host access: customers or your tests hit http://localhost:8080 thanks to -p 8080:8080
- Isolation: other networks cannot reach these containers by name
Verify DNS from inside a container:
docker exec -it api sh -lc 'ping -c1 db || true'; \
docker exec -it api sh -lc 'getent hosts db || true'
Minimal API Dockerfile example
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY app.py .
RUN pip install --no-cache-dir flask psycopg2-binary
ENV PORT=8080
CMD ["python", "app.py"]
Example app.py (very small):
from flask import Flask
import os
app = Flask(__name__)
@app.get("/")
def hello():
return {"ok": True, "port": os.getenv("PORT", "8080")}
app.run(host="0.0.0.0", port=int(os.getenv("PORT", "8080")))
Build and run:
docker build -t myorg/api: latest .
Port publishing basics
- Syntax: -p hostPort: containerPort[/protocol]
- Publishing exposes a container port on the host. Inside the Docker network, other containers still connect to the container port, not the host port.
- On host network: do not use -p. Your app must bind to 0.0.0.0 and the port must be free on the host.
- You can run multiple containers that listen on the same container port if you publish different host ports (e.g., -p 8081:8080 and -p 8082:8080).
Check published ports:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
DNS between containers
On user-defined bridge and overlay networks, Docker provides an embedded DNS resolver. Containers resolve each other by name:
- Single host: use the container name on the same user-defined bridge (e.g., http://api:8080)
- Compose: use the service name (e.g., http://web:80)
Inside a container, /etc/resolv.conf typically points to 127.0.0.11, the Docker DNS server for the network.
Docker Compose networks (recommended for app stacks)
Docker Compose auto-creates a dedicated network for your project and gives each service a DNS name equal to its service key. This is the most repeatable local pattern.
compose.yaml:
version: "3.9"
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
networks: [app-net]
api:
build: .
environment:
DATABASE_HOST: db
ports:
- "8080:8080"
depends_on: [db]
networks: [app-net]
web:
image: nginx: alpine
ports:
- "8081:80"
networks: [app-net]
networks:
app-net:
driver: bridge
Usage:
docker compose up -d
# Test service-to-service
docker compose exec web sh -lc 'wget -qO- http://api:8080 || curl -s http://api:8080'
# Test from host
curl -s http://localhost:8081
Host networking (Linux-only in most cases)
Host networking puts the container on the host network stack. Pros: no NAT, lower overhead, simpler inbound traffic. Cons: less isolation, port conflicts, and it is generally Linux-only.
Example:
# Ensure your app binds to 0.0.0.0:8080
docker run -d --name fastapi --network host myorg/api: latest
# Now the app is reachable on the host at :8080 without -p
Consider host mode only when you truly need it and understand the security and operational trade-offs.
Overlay networking (multi-host with Swarm)
Overlay networks connect services across multiple Docker hosts. You need Swarm enabled.
Initialize Swarm and create an attachable overlay:
docker swarm init
# Create an overlay network that both services and standalone containers can use
docker network create -d overlay --attachable app-ol
Deploy services with published ports:
# API service
docker service create \
--name api \
--network app-ol \
-p 8080:8080 \
myorg/api: latest
# Web service
docker service create \
--name web \
--network app-ol \
-p 80:80 \
nginx: alpine
Or use a stack file (stack.yaml):
version: "3.9"
services:
api:
image: myorg/api: latest
ports: ["8080:8080"]
networks: [app-ol]
web:
image: nginx: alpine
ports: ["80:80"]
networks: [app-ol]
networks:
app-ol:
driver: overlay
Deploy the stack:
docker stack deploy -c stack.yaml mystack
Verify:
docker service ls
curl -s http://<any-node-ip>:80
Troubleshooting broken connectivity
Use this checklist to isolate common issues fast.
- Are containers on the same network?
docker network inspect app-net --format '{{json .Containers}}' | jq '.'
If the peer is not listed, connect it:
docker network connect app-net api
- Is the port actually published on the host?
docker ps --format 'table {{.Names}}\t{{.Ports}}'
# Look for 0.0.0.0:8080->8080/tcp (or your chosen host IP/port)
If missing, add -p or Compose ports.
- Does the app bind to 0.0.0.0 inside the container?
# Replace with a tool available in your image
docker exec -it api sh -lc 'ss -tln || netstat -tln'
If it binds only to 127.0.0.1, update the app to listen on 0.0.0.0.
- Can containers resolve each other by name?
docker exec -it api sh -lc 'getent hosts db || ping -c1 db || nslookup db || true'
If resolution fails, ensure you use a user-defined bridge or overlay and that both containers join the same network.
- Host firewall blocking traffic?
- Ensure your host allows inbound on the published ports (e.g., 80, 8080). Adjust your firewall rules if needed.
- Port conflicts on host?
# On Linux
ss -tulpn | grep :8080 || netstat -tulpn | grep :8080
If a process already uses the host port, pick a different one or stop the conflicting service.
- Overlay-specific checks
docker info | grep -i swarm
# Should show: Swarm: active
docker service ps api
If tasks are failing, check logs and node availability. Ensure all services attach to the same overlay.
If you used the legacy default "bridge" network, switch to a user-defined bridge for better DNS and isolation:
- Default bridge pitfalls
docker network create app-net
# Recreate containers with --network app-net
When in doubt, remove and recreate containers with explicit networks and ports:
- Clean up and recreate
docker rm -f api db web || true
docker network rm app-net || true
Then re-run known-good commands or Compose.
Local Pilot Plan
Keep the first pilot small, measurable, and easy to inspect locally.
Goal
- A 3-service stack (db, api, web) on a user-defined bridge with working DNS and published ports.
- Success metric: curl from host returns expected JSON and Nginx welcome page within 200 ms on localhost.
Steps
- Build the API image and create the network
docker build -t myorg/api: latest .
docker network create app-net
- Start services
docker run -d --name db --network app-net -e POSTGRES_PASSWORD=secret postgres:16
docker run -d --name api --network app-net -e DATABASE_HOST=db -p 8080:8080 myorg/api: latest
docker run -d --name web --network app-net -p 8081:80 nginx: alpine
- Verify
# Container-to-container DNS
docker exec web sh -lc 'wget -qO- http://api:8080 || curl -s http://api:8080'
# Host access
curl -sS http://localhost:8080
curl -sS http://localhost:8081
- Document ports and network name for reuse across future services.
Optional next pilot (overlay)
- Enable Swarm and repeat with an overlay network across two hosts or two VMs.
Conclusion
- Use a user-defined bridge for most single-host applications. You get isolation, predictable DNS between containers, and simple port publishing.
- Consider host networking only on Linux and only when you need lower overhead and understand the trade-offs.
- Use overlay networks with Swarm for multi-host setups.
- Publish only the ports you need, verify from both the host and between containers, and keep a short troubleshooting checklist at hand.
- Start with a small, measurable local pilot, then scale the same pattern to more services and, when needed, multiple hosts.