Docker Compose Networking: Enterprise Architecture & Reference Guide
On this page 9 sections
- Docker Compose Networking: Enterprise Architecture & Reference Guide
- Part 1: Why Network Segmentation Matters
- Part 2: Architecture Overview
- Part 3: Implementation Guide
- Part 4: Docker Networking Compose Options Reference
- Part 5: Complete Multi-Stack Example
- Part 6: Best Practices for Medical Imaging Infrastructure
- Part 7: Troubleshooting Commands
- Summary
Docker Compose Networking: Enterprise Architecture & Reference Guide
Part 1: Why Network Segmentation Matters
The Problem with Host Networking (network_mode: host)
Using host networking removes isolation between the container and the Docker host.
- Port Collision Hell: You cannot run two services listening on port 80 or 8080. Managing dozens of services with random high-port mappings (
8081,9090,3000) becomes unmaintainable. - Security Bypass: Host networking bypasses Docker daemon iptables routing. A container binding to
0.0.0.0is immediately exposed on the host's interface. - No Namespacing: You lose the ability to reference services by hostname (DNS). You're forced to use
localhostor the host IP, breaking portability.
The Problem with One "Flat" Proxy Network
Placing your reverse proxy, 12 web apps, 12 databases, and 5 cache services on a single bridge network is a security liability.
- Lateral Movement: If a public-facing container (e.g., a patient portal) is exploited, the attacker immediately has network visibility and TCP access to your PACS database or RIS system.
- Unrestricted Discovery: Every container can resolve the IP of every other container. A compromised container can scan the subnet and map your entire infrastructure.
- No Compliance Path: Healthcare environments (HIPAA, GDPR) require demonstrable network segmentation. A flat topology fails audit requirements.
The Solution: Network Segmentation by Design
The goal is the Principle of Least Privilege applied to networking. A container should only see what it must see to function.
- Public Gateway (External): A specific network used solely for traffic from the Reverse Proxy to the Service Endpoint.
- App Backend (Internal): A private network, specific to that stack, linking the Service Endpoint to its Datastores.
The Reverse Proxy should never communicate directly with a backend database.
Part 2: Architecture Overview
Network Topology
Internet
↓
Host (Port 80, 443)
↓
Reverse Proxy Container (Traefik, Nginx)
↓
proxy_gateway Network (Bridge, External)
↓
Application Containers (Wiki, PACS Viewer, etc.)
↓
app_internal Networks (Bridge, Internal) [One per stack]
↓
Database Containers (Postgres, MariaDB, Redis)
Traffic Flow Example
- Internet → Host (443) → Proxy Container
- Proxy Container →
proxy_gateway→ Wiki App - Wiki App →
wiki_internal→ Wiki DB
The Wiki DB cannot be reached by any container not on wiki_internal. The Proxy cannot reach the Wiki DB.
Part 3: Implementation Guide
Step 1: Create the Gateway Network (Once)
This ensures the network ID persists even if the proxy stack is redeployed.
docker network create proxy_gateway
Verify:
docker network ls
docker network inspect proxy_gateway
Step 2: The Proxy Stack (proxy-compose.yml)
The reverse proxy (Traefik, Nginx Proxy Manager, Caddy) attaches to the gateway and exposes ports to the outside world.
version: '3.8'
services:
reverse-proxy:
image: traefik:v3.0
container_name: traefik_proxy
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
networks:
- proxy_gateway
restart: unless-stopped
networks:
proxy_gateway:
external: true
Deploy:
docker compose -f proxy-compose.yml up -d
Step 3: Application Stacks (One per application)
Scenario: A PACS viewer requires a Web UI and a PostgreSQL database.
- Web UI: Needs to talk to the Proxy (for ingress) AND the Database.
- Database: Needs to talk ONLY to the Web UI.
File: pacs-compose.yml
version: '3.8'
services:
pacs-web:
image: pacs-viewer:latest
container_name: pacs_viewer
environment:
DB_HOST: pacs-db
DB_PORT: 5432
DB_NAME: pacs_db
DB_USER: pacs_user
DB_PASSWORD: ${PACS_DB_PASSWORD}
labels:
# Traefik routing configuration
- "traefik.enable=true"
- "traefik.docker.network=proxy_gateway"
- "traefik.http.routers.pacs.rule=Host(`pacs.yourdomain.com`)"
- "traefik.http.routers.pacs.entrypoints=websecure"
- "traefik.http.services.pacs.loadbalancer.server.port=8080"
networks:
- proxy_gateway # For Traefik to route traffic
- pacs_internal # For database communication
restart: unless-stopped
depends_on:
- pacs-db
pacs-db:
image: postgres:15-alpine
container_name: pacs_db
environment:
POSTGRES_DB: pacs_db
POSTGRES_USER: pacs_user
POSTGRES_PASSWORD: ${PACS_DB_PASSWORD}
volumes:
- pacs_db_data:/var/lib/postgresql/data
networks:
- pacs_internal # ISOLATED: Proxy cannot reach this
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pacs_user"]
interval: 10s
timeout: 5s
retries: 5
pacs-redis:
image: redis:7-alpine
container_name: pacs_cache
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- pacs_cache_data:/data
networks:
- pacs_internal # ISOLATED
restart: unless-stopped
networks:
proxy_gateway:
external: true # Shares the gateway network
pacs_internal:
internal: true # No outbound internet access
driver: bridge
driver_opts:
com.docker.network.driver.mtu: 1500
volumes:
pacs_db_data:
pacs_cache_data:
Deploy:
docker compose -f pacs-compose.yml up -d
Step 4: Repeated Pattern for Other Services
Create separate Compose files for each application (Wiki, RIS, Archive, etc.). Each follows the same pattern:
- Connect frontend to
proxy_gateway(external). - Connect all backend services to a stack-specific internal network.
- Use
internal: truefor backend networks.
Part 4: Docker Networking Compose Options Reference
1. external
What it does: Tells Compose that the network lifecycle is managed outside of the current docker-compose.yml.
Behavior: Compose will look for an existing network. If found, it attaches. If not found, docker compose up fails. It will not create or delete this network.
Use Case: Your shared "Gateway" network. Allows you to restart your proxy stack without destroying network connections for application stacks.
Syntax Variations:
networks:
# Variation A: Network name matches the key name
proxy_gateway:
external: true
# Variation B: Network name differs from the key
default_net:
external:
name: my_preexisting_company_network
2. internal
What it does: Prevents the Docker daemon from setting up a default gateway for this network.
Behavior: Containers can communicate with each other on this subnet, but they cannot route packets to the internet or the host's wider network. Routes are restricted to the local subnet CIDR.
Use Case: Databases, Redis caches, or legacy applications that have no business communicating outside their stack. Prevents data exfiltration or C2 callbacks.
networks:
backend_secure:
internal: true
3. name
What it does: Overrides the default naming convention ([projectname]_[networkkey]).
Behavior: Forces a specific, predictable name for the Docker network object.
Use Case: When scripts or external tools (monitoring, firewalls) rely on static network names rather than ones derived from the folder containing docker-compose.yml.
networks:
app_net:
name: pacs_viewer_prod_v1
4. driver
What it does: Specifies the network driver.
Options:
bridge: Standard NAT networking (default). Suitable for 99% of use cases.host: Removes isolation (not recommended for medical systems).macvlan: Assigns a MAC address to the container, making it appear as a physical device on your LAN. Use if a medical modality needs to push DICOM images directly to a container IP on the physical subnet.ipvlan: Similar to macvlan but lighter (L2 or L3 modes).overlay: For Docker Swarm or multi-host Docker networking.
networks:
standard_bridge:
driver: bridge
phys_lan:
driver: macvlan
driver_opts:
parent: eth0 # Physical interface on the host
5. driver_opts
What it does: Passes specific options to the network driver.
Common Options:
com.docker.network.driver.mtu: Sets Maximum Transmission Unit. Medical imaging often requires Jumbo Frames (MTU 9000) to move large DICOM files efficiently.com.docker.network.bridge.name: Sets the bridge interface name on the host (advanced).
networks:
imaging_net:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: 9000
6. ipam (IP Address Management)
What it does: Defines specific subnets and optionally static IPs.
Behavior: Overrides Docker's auto-assignment of subnets.
Use Case: Preventing subnet overlap with your physical corporate network. If Docker picks 172.17.0.0/16 by default and your intranet uses that range, routing breaks. Always pin your Docker subnets in enterprise environments.
networks:
custom_subnet:
ipam:
driver: default
config:
- subnet: "10.10.1.0/24"
gateway: "10.10.1.1"
services:
db:
networks:
custom_subnet:
ipv4_address: 10.10.1.5 # Static IP (use sparingly)
Note: While static IPs are possible, they're generally discouraged. Use DNS names (service names) instead. Static IPs are useful only for legacy systems that expect fixed IPs.
7. enable_ipv6
What it does: Enables IPv6 addressing for the network.
Behavior: Provisions IPv6 subnets.
Constraint: Requires the Docker daemon (/etc/docker/daemon.json) to be configured for IPv6 first.
networks:
dual_stack:
enable_ipv6: true
ipam:
config:
- subnet: 2001:db8:1::/64
Part 5: Complete Multi-Stack Example
Directory Structure
docker/
├── proxy-compose.yml
├── pacs-compose.yml
├── wiki-compose.yml
├── ris-compose.yml
└── .env
.env (Shared Secrets)
PACS_DB_PASSWORD=secure_pacs_password_here
WIKI_DB_PASSWORD=secure_wiki_password_here
RIS_DB_PASSWORD=secure_ris_password_here
REDIS_PASSWORD=secure_redis_password_here
proxy-compose.yml
version: '3.8'
services:
traefik:
image: traefik:v3.0
container_name: traefik
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./acme.json:/acme.json
networks:
- proxy_gateway
restart: unless-stopped
networks:
proxy_gateway:
external: true
Deploy once:
docker network create proxy_gateway
docker compose -f proxy-compose.yml up -d
pacs-compose.yml (Already shown above)
Deploy:
docker compose -f pacs-compose.yml up -d
wiki-compose.yml (Separate Stack)
version: '3.8'
services:
wiki-app:
image: mediawiki:latest
container_name: wiki
environment:
MEDIAWIKI_DB_HOST: wiki-db
MEDIAWIKI_DB_USER: wiki_user
MEDIAWIKI_DB_PASSWORD: ${WIKI_DB_PASSWORD}
MEDIAWIKI_DB_NAME: wiki_db
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy_gateway"
- "traefik.http.routers.wiki.rule=Host(`wiki.yourdomain.com`)"
- "traefik.http.routers.wiki.entrypoints=websecure"
- "traefik.http.services.wiki.loadbalancer.server.port=80"
networks:
- proxy_gateway
- wiki_internal
restart: unless-stopped
depends_on:
- wiki-db
wiki-db:
image: mariadb:11
container_name: wiki_db
environment:
MYSQL_ROOT_PASSWORD: ${WIKI_DB_PASSWORD}
MYSQL_DATABASE: wiki_db
MYSQL_USER: wiki_user
MYSQL_PASSWORD: ${WIKI_DB_PASSWORD}
volumes:
- wiki_db_data:/var/lib/mysql
networks:
- wiki_internal
restart: unless-stopped
networks:
proxy_gateway:
external: true
wiki_internal:
internal: true
driver: bridge
volumes:
wiki_db_data:
Deploy:
docker compose -f wiki-compose.yml up -d
ris-compose.yml (Another Stack)
version: '3.8'
services:
ris-app:
image: ris-application:latest
container_name: ris
environment:
DB_HOST: ris-db
DB_USER: ris_user
DB_PASSWORD: ${RIS_DB_PASSWORD}
DB_NAME: ris_db
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy_gateway"
- "traefik.http.routers.ris.rule=Host(`ris.yourdomain.com`)"
- "traefik.http.routers.ris.entrypoints=websecure"
- "traefik.http.services.ris.loadbalancer.server.port=8000"
networks:
- proxy_gateway
- ris_internal
restart: unless-stopped
depends_on:
- ris-db
ris-db:
image: postgres:15-alpine
container_name: ris_db
environment:
POSTGRES_DB: ris_db
POSTGRES_USER: ris_user
POSTGRES_PASSWORD: ${RIS_DB_PASSWORD}
volumes:
- ris_db_data:/var/lib/postgresql/data
networks:
- ris_internal
restart: unless-stopped
networks:
proxy_gateway:
external: true
ris_internal:
internal: true
driver: bridge
ipam:
config:
- subnet: "10.10.2.0/24"
volumes:
ris_db_data:
Deploy:
docker compose -f ris-compose.yml up -d
Part 6: Best Practices for Medical Imaging Infrastructure
1. Subnet Planning
Pre-assign CIDR ranges to avoid collision with your physical network:
proxy_gateway: 10.10.0.0/24
pacs_internal: 10.10.1.0/24
wiki_internal: 10.10.2.0/24
ris_internal: 10.10.3.0/24
archive_internal: 10.10.4.0/24
Document this in your infrastructure wiki or runbook.
2. Health Checks
Always include health checks for databases:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U username"]
interval: 10s
timeout: 5s
retries: 5
This ensures the proxy doesn't route traffic to unhealthy backends.
3. Secrets Management
Never hardcode passwords. Use:
# Store in .env (and .gitignore it)
DB_PASSWORD=secure_password
# Or use Docker Secrets (if using Swarm)
echo "secure_password" | docker secret create db_password -
4. Logging & Monitoring
Attach all containers to a monitoring network if needed (separate from app networks):
networks:
- proxy_gateway
- pacs_internal
- monitoring # For Prometheus, ELK, etc.
5. Compliance Documentation
For HIPAA or GDPR audits, document:
- Which network each service belongs to.
- Which services can communicate with which.
- MTU settings for DICOM transfers.
- How data isolation is enforced.
Example audit document:
PACS Stack Network Isolation:
- pacs-web: connected to proxy_gateway (ingress) + pacs_internal (backend)
- pacs-db: connected to pacs_internal ONLY
- Backend network (pacs_internal): internal=true, no outbound internet access
- Result: Database inaccessible from proxy or other stacks
Part 7: Troubleshooting Commands
# List all networks
docker network ls
# Inspect a specific network
docker network inspect proxy_gateway
# Check container's network attachments
docker inspect pacs_viewer | grep -A 20 '"Networks"'
# Test connectivity between containers
docker exec pacs_viewer ping pacs-db
# View network-specific logs
docker logs pacs_viewer | grep -i network
# Remove orphaned networks
docker network prune
Summary
This architecture provides:
✓ Security: Each stack is isolated. Compromising one does not expose others.
✓ Compliance: Demonstrable network segmentation for healthcare regulations.
✓ Scalability: Add new applications by creating new Compose files and internal networks.
✓ Manageability: A single proxy stack routes to dozens of applications without port collision.
✓ Resilience: Redeploying a stack doesn't affect the shared gateway or other stacks.