Secure your application architecture by isolating services in separate networks. Not every service needs to talk to every other service!
Default behavior: All connected
By default, all services share the same network:
# All services can communicate
services:
web:
image: nginx
api:
image: myapi
database:
image: postgres
Problem: web can directly access database - potential security risk!
Network isolation pattern
Create separate networks for different tiers:
services:
# Frontend tier
web:
image: nginx
networks:
- frontend
- backend
depends_on:
- api
# Application tier
api:
image: myapi
networks:
- backend
- database
environment:
DB_HOST: postgres
# Data tier
postgres:
image: postgres
networks:
- database
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
networks:
frontend:
name: frontend-network
backend:
name: backend-network
database:
name: database-network
Now:
webcan reachapi(via backend network)apican reachpostgres(via database network)webCANNOT reachpostgresdirectly ✅
Internal networks
Use internal networks to isolate from host network interfaces:
services:
cache:
image: redis
networks:
- internal
worker:
image: worker
networks:
- internal
- public
networks:
internal:
internal: true # No connection to host network interfaces
public:
# Regular network connected to host
The internal: true flag creates a network without a connection to the host’s network interfaces - it has no default gateway for external connectivity. Containers can still reach the internet if they’re also connected to other non-internal networks (like the worker service above via the public network).
Service discovery
Services can only discover each other on shared networks:
services:
api1:
image: api:v1
networks:
- api-network
# Can ping: api2
# Cannot ping: db1, db2
api2:
image: api:v2
networks:
- api-network
# Can ping: api1
# Cannot ping: db1, db2
db1:
image: postgres
networks:
- db-network
# Can ping: db2
# Cannot ping: api1, api2
db2:
image: postgres
networks:
- db-network
# Can ping: db1
# Cannot ping: api1, api2
networks:
api-network:
db-network:
Complete example: Microservices
services:
# Public-facing services
nginx:
image: nginx
ports:
- "80:80"
networks:
- dmz
- application
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
# Application services
user-service:
image: user-service
networks:
- application
- user-db
order-service:
image: order-service
networks:
- application
- order-db
- messaging
# Databases (isolated)
user-db:
image: postgres
networks:
- user-db
volumes:
- user-data:/var/lib/postgresql/data
order-db:
image: postgres
networks:
- order-db
volumes:
- order-data:/var/lib/postgresql/data
# Message queue
rabbitmq:
image: rabbitmq:management
networks:
- messaging
# Monitoring (observes all)
prometheus:
image: prom/prometheus
networks:
- application
- user-db
- order-db
- messaging
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
networks:
dmz:
name: dmz-network
application:
name: app-network
user-db:
name: user-db-network
internal: true
order-db:
name: order-db-network
internal: true
messaging:
name: messaging-network
internal: true
volumes:
user-data:
order-data:
Pro tip
Use docker network inspect to verify isolation:
# List all networks used by your compose project
docker compose ps --format json | jq -r '.[].Networks | keys[]' | sort -u
# Inspect which services share a network
docker network inspect <network-name> --format '{{json .Containers}}' | jq
# Quick connectivity test between services
docker compose exec web ping api -c 1 # Should work if on same network
docker compose exec web ping postgres -c 1 # Should fail if isolated
Defense in depth starts with network segmentation!