HomeCurriculumDocker & KubernetesDocker Compose & Multi-Container Networks
Docker Compose & Multi-Container Networks

Docker Compose & Multi-Container Networks

Orchestrate multi-container applications with Docker Compose, examine user-defined bridge networks, and configure embedded DNS discovery.

DT
Authored by DevThesis Systems Editorial Team
Reviewed for Technical Accuracy & Code Determinism

While single-container management with docker run is sufficient for simple utilities or command-line tools, real-world enterprise applications are fundamentally distributed systems. A modern web platform comprises multiple interdependent services: a frontend single-page application, an API gateway, multiple microservices, background task workers, a relational database, an in-memory caching cluster, and a message broker.

Attempting to orchestrate this multi-tiered topology using manual docker run commands requires writing fragile shell scripts with dozens of port-forwarding flags, hardcoded IP addresses, and manually created virtual bridge networks. If a database fails to initialize before the backend starts, the entire stack crashes.

Docker Compose formalizes the specification of multi-container application topologies through declarative configuration. Concurrently, Docker Networking establishes the virtualized networking substrate—powered by Linux kernel virtual bridges, network namespaces, and Netfilter iptables rules—that enables containers to discover and securely communicate with one another.

This module delivers an exhaustive, university-grade architectural analysis of Docker Compose orchestration, container networking drivers (Bridge, Host, Overlay, Macvlan), embedded DNS service discovery, and production state management via volumes and bind mounts.


Docker Compose Architecture & The Compose Specification#

Docker Compose operates as a client-side declarative orchestrator. It parses a YAML specification file (typically docker-compose.yml), resolves dependencies, creates isolated networks and storage volumes, and coordinates container lifecycle transitions via the Docker Engine API.

text
+-----------------------------------------------------------------------------------+
| Docker Compose Architectural Layering                                             |
|                                                                                   |
|  [ Declarative Specification: docker-compose.yml ]                                |
|  - Services: web, api, db, redis                                                  |
|  - Networks: frontend_net, backend_net                                            |
|  - Volumes: postgres_data, redis_data                                             |
|                         |                                                         |
|                         v                                                         |
|  +-----------------------------------------------------------------------------+  |
|  | Docker Compose CLI Engine (compose v2)                                      |  |
|  | 1. Parses YAML, interpolates environment variables (.env)                   |  |
|  | 2. Computes Dependency Directed Acyclic Graph (DAG)                         |  |
|  | 3. Creates User-Defined Bridge Networks                                      |  |
|  | 4. Provisons Named Storage Volumes                                          |  |
|  +-------------------------------------+---------------------------------------+  |
|                                        | Dispatches Docker Engine API Calls       |
|                                        v                                          |
|  +-----------------------------------------------------------------------------+  |
|  | Docker Daemon (dockerd)                                                      |  |
|  | - Spawns containers on virtual bridge networks with embedded DNS aliases     |  |
|  | - Monitors container health check probes                                    |  |
|  +-----------------------------------------------------------------------------+  |
+-----------------------------------------------------------------------------------+

The Compose Lifecycle State Machine#

A service definition in Docker Compose represents not just a container image, but its complete runtime envelope:

  • Service Identity: A logical group of 1 or more identical container replicas.
  • Environment Injection: Multi-tier variable resolution (.env file -> shell environment -> compose environment: block).
  • Startup Ordering: Governing initialization sequence using depends_on paired with active health checks.

Docker Networking Deep Dive#

Container networking relies on Linux network namespaces, virtual ethernet pairs (veth), and packet routing via the kernel's Netfilter/iptables subsystem.

text
+-----------------------------------------------------------------------------------+
| Anatomy of a User-Defined Bridge Network                                          |
|                                                                                   |
|  HOST OPERATING SYSTEM NETWORK STACK                                              |
|                                                                                   |
|  Physical Interface: eth0 (Host IP: 192.168.1.100)                                |
|  Default Gateway: 192.168.1.1                                                     |
|                                      ^                                            |
|                                      | iptables NAT (MASQUERADE / Port Mapping)   |
|                                      v                                            |
|  Linux Virtual Bridge: br-d7a82b (Bridge IP: 172.20.0.1/16)                       |
|  - Acts as an internal Layer 2 virtual switch                                     |
|  - Embedded DNS Server listening on 127.0.0.11                                    |
|                                      |                                            |
|             +------------------------+------------------------+                   |
|             | Virtual Ethernet Pair                           |                   |
|             | (veth_host_A <-> veth_peer_A)                   |                   |
|             v                                                 v                   |
|  +---------------------------+               +---------------------------+        |
|  | Container: "api"          |               | Container: "db"           |        |
|  | Network Namespace: netns_A|               | Network Namespace: netns_B|        |
|  | Interface: eth0           |               | Interface: eth0           |        |
|  | IP: 172.20.0.2/16         |               | IP: 172.20.0.3/16         |        |
|  | Resolves "db" ->172.20.0.3|               | Listens on 172.20.0.3:5432|        |
|  +---------------------------+               +---------------------------+        |
+-----------------------------------------------------------------------------------+

1. Network Drivers Taxonomy#

Docker provides five distinct network drivers designed for different deployment topologies:

A. Bridge (`bridge`)#

  • How it works: Creates a software virtual bridge on the host (e.g., docker0 or br-<network-id>). Each container receives a veth pair: one endpoint attaches to the bridge, and the peer endpoint is moved into the container's network namespace as eth0.
  • Subnet Allocation: Assigns private IP addresses from a non-routable CIDR block (e.g., 172.17.0.0/16 or 172.20.0.0/16).
  • External Egress: Handled by the Linux kernel using iptables Network Address Translation (NAT) via the POSTROUTING chain (MASQUERADE).
  • External Ingress: Port mapping (-p 8080:80) creates DNAT (Destination NAT) rules in the PREROUTING chain, rewriting destination IP and port to the container's private address.

B. User-Defined Bridge vs Default Bridge#

This distinction is critical for production:

  • The Default `docker0` Bridge: Containers attached to the default bridge do not support automatic DNS resolution by container name. Communication between containers requires fragile legacy --link flags or hardcoded private IP addresses.
  • User-Defined Bridge Networks: Automatically activates Docker's Embedded DNS Server (listening on 127.0.0.11). Containers resolve sibling containers by service name or network alias automatically (http://database:5432). Furthermore, user-defined bridges provide complete network segmentation: containers on frontend_net cannot reach containers on backend_net unless explicitly connected to both.

C. Host (`host`)#

  • How it works: Removes the network namespace boundary entirely. The container shares the host's network stack directly.
  • Performance: Eliminates all iptables NAT translation and virtual bridge traversal latency, achieving bare-metal network throughput.
  • Trade-off: Port conflicts occur if multiple containers attempt to bind to the same host port. No network isolation exists.

D. None (`none`)#

  • How it works: Assigns the container an isolated network namespace containing only the loopback (lo) interface.
  • Use Case: Completely air-gapped batch jobs, cryptographic key generation, or offline mathematical computations that must never access the network.

E. Overlay (`overlay`)#

  • How it works: Spans multiple physical Docker hosts using VXLAN (Virtual Extensible LAN) encapsulation. It encapsulates Layer 2 ethernet frames inside Layer 4 UDP packets (port 4789), allowing containers on different physical servers to communicate over a flat virtual subnet without host-level port mapping.

Embedded DNS & Service Discovery Mechanics#

In a user-defined bridge network, how does the API service resolve db to 172.20.0.3?

text
+-----------------------------------------------------------------------------------+
| Embedded DNS Resolution Flow (127.0.0.11)                                          |
|                                                                                   |
|  1. Inside Container "api", application executes:                                 |
|     const client = new pg.Client({ host: 'db', port: 5432 });                    |
|                                                                                   |
|  2. glibc / musl resolver queries nameserver in /etc/resolv.conf:                 |
|     nameserver 127.0.0.11                                                         |
|     options ndots:0                                                               |
|                                                                                   |
|  3. Linux Netfilter intercepts packet to 127.0.0.11:53:                           |
|     iptables DNAT rewrites destination to Docker Daemon internal DNS listener.    |
|                                                                                   |
|  4. Docker Embedded DNS Server evaluates query:                                   |
|     - Is 'db' a container or alias on this network? YES!                          |
|     - Returns A Record: db -> 172.20.0.3                                          |
|     - (If 'db' has multiple replicas, returns IP list in Round-Robin order).      |
|                                                                                   |
|  5. External Queries (e.g., api.stripe.com):                                      |
|     - Embedded DNS forwards query to upstream host DNS (e.g., 8.8.8.8).           |
+-----------------------------------------------------------------------------------+

State Management: Volumes vs Bind Mounts vs tmpfs#

Containers are designed to be ephemeral and immutable. When a container process is deleted (docker rm), its writable upperdir layer is permanently destroyed. To persist application data across container recreations, Docker provides three storage mechanisms:

text
+-----------------------------------------------------------------------------------+
| Docker Storage Architectures                                                      |
|                                                                                   |
|  1. Named Volumes (Production State Standard):                                    |
|     - Managed exclusively by Docker in /var/lib/docker/volumes/<name>/_data.      |
|     - High performance; completely decoupled from host directory structures.      |
|     - Safe from accidental host user deletion or permission drift.                |
|                                                                                   |
|  2. Bind Mounts (Local Development Standard):                                     |
|     - Mounts an arbitrary host directory directly into container:                 |
|       /Users/developer/code/src -> /app/src                                       |
|     - Enables instant hot-reloading without rebuilding images.                    |
|     - Dangerous in production: leaks host paths, introduces UID permission bugs.  |
|                                                                                   |
|  3. tmpfs Mounts (High-Security / In-Memory):                                     |
|     - Stored strictly in host system RAM (Linux tmpfs /dev/shm).                  |
|     - Never written to non-volatile disk. Cleared instantly on container stop.   |
|     - Used for session secrets, cryptographic private keys, high-speed sockets.   |
+-----------------------------------------------------------------------------------+

Production Multi-Tier Docker Compose Topology#

Below is an enterprise-grade docker-compose.yml orchestrating a complete production microservice stack comprising an Nginx reverse proxy, a Next.js web frontend, an Express API, a PostgreSQL database with persistent storage and health checks, and a Redis cache.

yaml
# docker-compose.yml
version: '3.8'

# ==============================================================================
# NETWORKS: Enforces strict architectural tier isolation
# ==============================================================================
networks:
  ingress_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16
  internal_net:
    driver: bridge
    internal: true # Air-gapped: Containers on internal_net CANNOT reach public WAN!

# ==============================================================================
# VOLUMES: Persistent state stores
# ==============================================================================
volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local

# ==============================================================================
# SERVICES
# ==============================================================================
services:
  # 1. Reverse Proxy & TLS Ingress
  proxy:
    image: nginx:1.25-alpine
    container_name: devthesis_proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    networks:
      - ingress_net
    depends_on:
      web:
        condition: service_started
      api:
        condition: service_healthy

  # 2. Frontend Web Application (Next.js)
  web:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: devthesis_web
    restart: unless-stopped
    environment:
      - NODE_ENV=production
      - API_INTERNAL_URL=http://api:8080
    networks:
      - ingress_net
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M

  # 3. Backend API Gateway & Business Logic (Node.js)
  api:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: devthesis_api
    restart: unless-stopped
    environment:
      - NODE_ENV=production
      - PORT=8080
      - DATABASE_URL=postgres://postgres:{DB_PASSWORD:-secret}@db:5432/devthesis
      - REDIS_URL=redis://redis:6379
    networks:
      - ingress_net
      - internal_net
    depends_on:
      db:
        condition: service_healthy # Waits for PostgreSQL pg_isready!
      redis:
        condition: service_healthy # Waits for Redis PING!
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 15s
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1024M

  # 4. Primary Relational Database (PostgreSQL)
  db:
    image: postgres:16-alpine
    container_name: devthesis_db
    restart: unless-stopped
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD={DB_PASSWORD:-secret}
      - POSTGRES_DB=devthesis
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - internal_net # Strictly isolated from ingress_net and public Internet!
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d devthesis"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2048M

  # 5. In-Memory Cache (Redis)
  redis:
    image: redis:7-alpine
    container_name: devthesis_redis
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes", "--requirepass", "{REDIS_PASSWORD:-secret}"]
    volumes:
      - redis_data:/data
    networks:
      - internal_net # Strictly isolated!
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "{REDIS_PASSWORD:-secret}", "ping"]
      interval: 5s
      timeout: 2s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

Checkpoint Practice Problems#

Apply the formal principles of container networking, healthcheck timing, iptables packet routing, and Compose orchestration to solve these engineering challenges.

Problem 1: Linux Bridge IP Packet Routing & iptables NAT Translation#

An API client hosted on the external Internet (IP_{client} = 203.0.113.42) transmits an HTTP GET request to a web service running inside a Docker container.

The Docker host machine possesses a physical network interface:

eth0: IP_{host} = 198.51.100.10.

The Docker container is running on a user-defined bridge network br-prod:

Bridge Gateway: 172.24.0.1/16.

Container IP: IP_{container} = 172.24.0.5, listening on internal port 8080.

Docker has published the port to the host via:

-p 80:8080.

Trace the exact step-by-step path of an incoming TCP SYN packet through the Linux kernel Netfilter tables and chains:

  1. Identify the Netfilter table and chain that executes the Destination NAT (DNAT) translation.
  2. Specify the exact IP and port modifications applied to the packet headers during DNAT.
  3. Identify the routing decision and the interface through which the packet leaves the host kernel to reach the container.
  4. When the container generates a return SYN-ACK response packet, specify how the POSTROUTING / conntrack (connection tracking) table translates the source IP before transmitting the packet back across the public Internet to 203.0.113.42.

Solution:

Step 1: Ingress Packet Arrival and DNAT Execution:

  • The incoming TCP SYN packet arrives at the physical interface eth0:

* Source IP: 203.0.113.42

* Source Port: 54128 (ephemeral client port)

* Destination IP: 198.51.100.10 (Host public IP)

* Destination Port: 80 (Host published port)

  • Before the host kernel makes a routing decision, the packet enters the `nat` table in the `PREROUTING` chain.
  • Docker installs a jump rule targeting the custom DOCKER chain:

-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER

  • Inside the DOCKER chain, the DNAT rule matches:

-A DOCKER ! -i br-prod -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.24.0.5:8080

Step 2: Header Mutation:

The kernel modifies the IP and TCP headers in place:

  • Destination IP is rewritten: 198.51.100.10 → 172.24.0.5
  • Destination Port is rewritten: 80 → 8080
  • Source IP and Port remain unchanged: 203.0.113.42:54128.
  • The connection tracking subsystem (conntrack) records this tuple mapping in the kernel state table.

Step 3: Routing Decision and Forwarding:

  • The kernel consults its routing table:

Destination: 172.24.0.5 matches the subnet 172.24.0.0/16 associated with interface br-prod.

  • The packet is routed into the `filter` table in the `FORWARD` chain.
  • Docker's firewall rule permits the traffic:

-A DOCKER -d 172.24.0.5/32 ! -i br-prod -o br-prod -p tcp -m tcp --dport 8080 -j ACCEPT

  • The packet enters the bridge br-prod, which forwards the Layer 2 ethernet frame to the specific virtual ethernet peer interface (veth) attached to the container's network namespace.
  • Inside the container, the application listening on 0.0.0.0:8080 accepts the TCP SYN.

Step 4: Egress Return Path (SYN-ACK Response):

  • The container transmits a return SYN-ACK:

* Source: 172.24.0.5:8080

* Destination: 203.0.113.42:54128

  • The packet travels out of the container eth0 onto the br-prod bridge.
  • The host kernel routes the packet toward default gateway eth0.
  • As the packet traverses the `nat` table `POSTROUTING` chain, the Linux conntrack engine recognizes the packet as belonging to the active flow established in Step 1.
  • Automatic Reverse SNAT: The kernel rewrites the Source IP and Port back to the host's public identity:

Source IP: 172.24.0.5 → 198.51.100.10Source Port: 8080 → 80* The packet departs eth0 onto the public Internet. The client at 203.0.113.42 receives a response from 198.51.100.10:80, completely unaware that the application executed inside an isolated container on a private bridge.


Problem 2: Docker Compose Healthcheck Timing & Startup Cascade Analysis#

An enterprise microservice stack has three dependent services configured in Docker Compose:

  1. Database (`db`): PostgreSQL.
  2. Backend API (`api`): Express server, configured with:

depends_on: { db: { condition: service_healthy } }

  1. Frontend (`web`): Next.js client, configured with:

depends_on: { api: { condition: service_healthy } }

The health check parameters are configured as follows:

  • Database (`db`):

* Cold startup initialization time: T_{db_init} = 8.0 seconds.

* start_period: 5 seconds.

* interval: 4 seconds.

* timeout: 2 seconds.

* retries: 3.

* Probe execution duration: 100 ms.

  • Backend API (`api`):

* Cold startup initialization time: T_{api_init} = 6.0 seconds (begins only after db is marked healthy).

* start_period: 10 seconds.

* interval: 5 seconds.

* timeout: 2 seconds.

* retries: 2.

* Probe execution duration: 50 ms.

Calculate:

  1. The exact timeline of health check probe dispatches for db, and the exact elapsed time (t_{db_healthy}) when Docker marks the database as healthy.
  2. The exact timeline of health check probe dispatches for api, and the exact elapsed time (t_{api_healthy}) when Docker marks the API as healthy.
  3. The total elapsed wall-clock time from docker compose up until the web frontend service is permitted to begin starting.

Solution:

Step 1: Database (`db`) Healthcheck Timeline:

start_period = 5s, interval = 4s. The database requires 8.0s to become ready.

  • t = 0.0 s: Database container starts.
  • First probe is dispatched after interval = 4s (at t = 4.0 s):

* At t = 4.0 s, DB is still initializing (T_{init} = 8.0 s).

* Probe fails at t = 4.1 s.

Because t = 4.0 s ≤ start_period (5 s), this failure does not* count toward the retries failure quota.

  • Second probe is dispatched after next interval = 4s (at t = 4.0 + 4.0 = 8.0 s):

* At t = 8.0 s, the database has just completed initialization (T_{db_init} = 8.0 s).

* The probe (pg_isready) succeeds at t = 8.1 s!

  • Under Docker healthcheck semantics, a single successful probe marks the service as healthy.

t_{db_healthy = 8.1 seconds}Step 2: API Gateway (`api`) Healthcheck Timeline:

The API container is held in pending status until t = 8.1 s.

  • t = 8.1 s: API container spawns and begins cold boot.
  • API initialization requires T_{api_init} = 6.0 s. The API becomes ready at:

t_{ready} = 8.1 + 6.0 = 14.1 seconds* API health check configuration: interval = 5s, start_period = 10s.

  • First API probe is dispatched at 8.1 + 5.0 = 13.1 s:

* At t = 13.1 s, the API is still initializing (requires until 14.1 s).

* Probe fails at t = 13.15 s. (Within start_period, does not count against retry limit).

  • Second API probe is dispatched at 13.1 + 5.0 = 18.1 s:

* At t = 18.1 s, the API is fully operational.

* Probe succeeds at t = 18.15 s.

t_{api_healthy = 18.15 seconds}Step 3: Total Wall-Clock Startup Time:

Because web depends on api: { condition: service_healthy }:

The web frontend service begins container creation at:

T_{total = 18.15 seconds}Through this deterministic cascade, the frontend never encounters broken backend sockets or uninitialized database connections during cold deployments.


Problem 3: Complete Air-Gapped Multi-Network Compose Implementation#

Construct a production-ready docker-compose.yml that implements an Air-Gapped Multi-Tier Security Pattern:

  1. A public-facing frontend service attached to dmz_net.
  2. A middleware api service attached to both dmz_net and secure_net.
  3. A backend database service attached exclusively to secure_net.
  4. The secure_net must be configured with internal: true, proving that even if the database container is compromised, it has zero network route to download malware from the public Internet.

Solution:

yaml
# docker-compose.security.yml
version: '3.8'

networks:
  # Demilitarized Zone Network: Accessible to incoming traffic
  dmz_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.0.0/24

  # Secure Backend Network: Completely air-gapped from WAN!
  secure_net:
    driver: bridge
    internal: true # Disables default gateway and outbound NAT!
    ipam:
      config:
        - subnet: 172.31.0.0/24

volumes:
  db_vault:
    driver: local

services:
  # Tier 1: Public Frontend
  frontend:
    image: nginx:alpine
    container_name: tier1_frontend
    ports:
      - "80:80"
    networks:
      - dmz_net

  # Tier 2: Middleware API (Acts as the bridge between tiers)
  api:
    image: node:20-alpine
    container_name: tier2_api
    command: ["node", "-e", "require('http').createServer((req,res)=>res.end('API')).listen(3000)"]
    networks:
      - dmz_net     # Can communicate with frontend
      - secure_net  # Can communicate with database

  # Tier 3: Isolated Secure Database
  database:
    image: postgres:16-alpine
    container_name: tier3_database
    environment:
      - POSTGRES_PASSWORD=airgapped_secret
    volumes:
      - db_vault:/var/lib/postgresql/data
    networks:
      - secure_net  # Can communicate ONLY with tier2_api!

Security Verification:

Executing docker exec tier3_database ping 8.8.8.8 or curl https://google.com inside the database container immediately results in:

ping: sendto: Network is unreachable

Because secure_net is declared with internal: true, Docker omits the default gateway route and installs zero iptables MASQUERADE rules. The database is mathematically incapable of establishing outbound Internet connections.


Problem 4: Embedded DNS Round-Robin Load Balancing Simulation#

In Docker Compose, a service is scaled horizontally to 3 replicas:

docker compose up --scale worker=3

A client service queries the embedded DNS server at 127.0.0.11 for the hostname worker.

  1. Explain how Docker's embedded DNS server handles multi-replica service names.
  2. If the 3 worker replicas possess IP addresses:

* Worker 1: 172.20.0.4

* Worker 2: 172.20.0.5

* Worker 3: 172.20.0.6

Write the exact DNS A-record responses returned across three consecutive DNS queries.

  1. Identify why standard client HTTP libraries (such as Node.js axios or Python requests) often fail to achieve true load balancing across these 3 IPs without connection pool tuning.

Solution:

Step 1: Embedded DNS Multi-Replica Resolution:

When a service is scaled to N replicas on a user-defined bridge network:

  • All N containers register the exact same service name (worker) with the embedded DNS daemon (127.0.0.11).
  • When a DNS query arrives for worker, the embedded DNS server returns an A-record response containing the IP addresses of all healthy replicas.
  • To distribute traffic, the DNS server rotates the order of the IP addresses in the returned list on each subsequent query using a DNS Round-Robin algorithm.

Step 2: Consecutive DNS Query Outputs:

  • Query 1 Response:

```text

;; ANSWER SECTION:

worker. 600 IN A 172.20.0.4

worker. 600 IN A 172.20.0.5

worker. 600 IN A 172.20.0.6

```

  • Query 2 Response (Rotated by 1):

```text

;; ANSWER SECTION:

worker. 600 IN A 172.20.0.5

worker. 600 IN A 172.20.0.6

worker. 600 IN A 172.20.0.4

```

  • Query 3 Response (Rotated by 2):

```text

;; ANSWER SECTION:

worker. 600 IN A 172.20.0.6

worker. 600 IN A 172.20.0.4

worker. 600 IN A 172.20.0.5

```

Step 3: Client Library Failure Mode (The HTTP Keep-Alive Trap):

Most modern HTTP clients (e.g., Node.js undici, axios, Go http.Client) implement HTTP/1.1 Persistent Connections (`Keep-Alive`):

  1. When the client executes its first request, it queries DNS and receives the list of IPs.
  2. It connects to the first IP in the list (172.20.0.4) and establishes a persistent TCP socket.
  3. For all subsequent HTTP requests, the client reuses the existing, open TCP socket to eliminate connection handshake latency.
  4. The client never executes a second DNS query!
  5. Outcome: 100% of the traffic pours into Worker 1 (172.20.0.4), while Worker 2 and Worker 3 sit completely idle.
  6. Remedy: In multi-container Docker environments, true client-side load balancing requires either:

* Deploying a Layer 7 reverse proxy (Nginx or Envoy) in front of the scaled service.

* Disabling HTTP Keep-Alive or configuring socket connection max-life limits (maxRequestsPerSocket = 100).

Key Architectural Takeaways

  • Docker Compose coordinates interconnected application services via declarative YAML manifests.
  • User-defined bridge networks automatically provide embedded DNS name resolution at 127.0.0.11.
  • Service dependency resolution models startups as a Directed Acyclic Graph, enhanced with healthcheck conditions.

Hands-on Code Playground Available

Execute, edit, and experiment with code for this topic in the live sandbox.

Practice Check • Quick Quiz

Test Your Understanding

How does Docker facilitate automatic service discovery between containers connected to the same user-defined bridge network?

A
By hardcoding IP addresses in each container's /etc/hosts file before boot.
B
By operating an embedded DNS server at 127.0.0.11 that resolves service names to container IP addresses.
C
By broadcasting ARP requests across the public host network.
D
By injecting environment variables into every running container.