Skip to content
IT related

Docker Compose Networking: Enterprise Architecture & Reference Guide

Updated 5 min read

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.0 is immediately exposed on the host's interface.
  • No Namespacing: You lose the ability to reference services by hostname (DNS). You're forced to use localhost or 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.

  1. Public Gateway (External): A specific network used solely for traffic from the Reverse Proxy to the Service Endpoint.
  2. 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

  1. Internet → Host (443) → Proxy Container
  2. Proxy Container → proxy_gateway → Wiki App
  3. 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: true for 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.

IT related RSS feed Published with Oblish

Keyboard shortcuts

/
Search
[
Previous page
]
Next page
?
Show this list
Esc
Close