Docker Deev Dive #4: Network — Container giao tiếp với nhau

15 phút đọc

Overview

Bạn đã có 3 mảnh ghép: Image (đóng gói), Container (chạy), Volume (lưu data). Nhưng một Laravel app không chỉ có 1 container — bạn cần PHP-FPM + Nginx + MySQL + Redis chạy đồng thời và nói chuyện với nhau.

Ở Bài 1, bạn đã biết mỗi container có NET namespace riêng — tức là IP, port, routing table riêng biệt. Điều này có nghĩa:

  • Container A không thể gọi container B qua localhost
  • Mỗi container "tưởng" nó là máy riêng trong mạng
  • Cần một cơ chế để các container tìm thấygiao tiếp với nhau

Docker Network chính là cơ chế đó.

💡 Mental model: Mỗi container là một căn hộ với địa chỉ riêng. Docker Network là hệ thống ống nước/dây điện kết nối các căn hộ lại với nhau. Không có network = các căn hộ cô lập hoàn toàn.


Key Concepts

Vấn đề cốt lõi: Tại sao DB_HOST=127.0.0.1 không hoạt động?

Khi chạy Laravel trên máy local (không Docker):

# .env (không Docker)
DB_HOST=127.0.0.1   # MySQL chạy trên cùng máy → OK
DB_PORT=3306

Khi chạy Laravel trong Docker:

# .env (Docker)
DB_HOST=mysql        # ← Service name, KHÔNG phải 127.0.0.1
DB_PORT=3306

Tại sao? Vì mỗi container có NET namespace riêng (Bài 1):

  • 127.0.0.1 trong container PHP = chính container PHP (loopback của nó)
  • MySQL chạy trong container khác → có IP khác
  • Docker Network cho phép dùng service name (mysql) thay vì IP — Docker tự resolve tên thành IP
┌─────────────────────────────────────────────────────┐
│              Docker Bridge Network                   │
│                  172.18.0.0/16                       │
│                                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────┐  │
│  │  php-fpm     │  │   nginx      │  │  mysql    │  │
│  │  172.18.0.2  │  │  172.18.0.3  │  │ 172.18.0.4│  │
│  │              │  │              │  │           │  │
│  │ localhost =  │  │ localhost =  │  │localhost =│  │
│  │ chính nó     │  │ chính nó     │  │ chính nó  │  │
│  └──────┬───────┘  └──────┬───────┘  └─────┬─────┘  │
│         │                 │                │         │
│         └────────────┬────┴────────────────┘         │
│                      │                               │
│            Bridge (docker0 / br-xxx)                 │
└──────────────────────┬───────────────────────────────┘
                       │
                  Host Network

Docker Network Driver — 3 loại cần biết

Driver Cơ chế Container ↔ Container Container ↔ Host Use case
bridge (mặc định) Virtual switch nội bộ, mỗi container có IP riêng ✅ Qua bridge ✅ Qua port mapping (-p) 90% trường hợp — multi-container app
host Container dùng trực tiếp network stack của host ✅ Qua localhost ✅ Trực tiếp (không cần -p) Performance cao, không cần cô lập network
none Không có network — container bị cô lập hoàn toàn Security, batch processing không cần network

⚠️ Với Laravel dev, bạn sẽ dùng bridge gần như 100% thời gian. host dùng khi cần benchmark network performance. none hiếm khi dùng.


How It Works

1. Default Bridge Network

Khi cài Docker, nó tự tạo một network tên bridge (hay docker0):

# Xem danh sách networks
docker network ls
# NETWORK ID     NAME      DRIVER    SCOPE
# a1b2c3d4e5f6   bridge    bridge    local
# f6e5d4c3b2a1   host      host      local  
# 1a2b3c4d5e6f   none      null      local

Khi chạy docker run không chỉ định network, container tự động vào default bridge:

docker run -d --name web nginx:alpine
docker run -d --name db mysql:8.0 -e MYSQL_ROOT_PASSWORD=secret

# Cả 2 đều ở default bridge → có thể ping nhau bằng IP
docker inspect web --format '.NetworkSettings.IPAddress'
# → 172.17.0.2

docker inspect db --format '.NetworkSettings.IPAddress'
# → 172.17.0.3

# Ping bằng IP: OK
docker exec web ping -c 2 172.17.0.3
# → 64 bytes from 172.17.0.3

# Ping bằng tên: KHÔNG HOẠT ĐỘNG trên default bridge!
docker exec web ping -c 2 db
# → ping: bad address 'db'

2. Custom Bridge Network (recommended)

Custom bridge network giải quyết mọi hạn chế của default bridge:

# Tạo custom network
docker network create my-laravel-net

# Chạy containers trong custom network
docker run -d --name app --network my-laravel-net php:8.2-fpm-alpine
docker run -d --name mysql --network my-laravel-net \
  -e MYSQL_ROOT_PASSWORD=secret \
  mysql:8.0
docker run -d --name redis --network my-laravel-net redis:7-alpine

# Bây giờ có thể ping bằng TÊN!
docker exec app ping -c 2 mysql
# → 64 bytes from 172.18.0.3 (172.18.0.3): seq=0 ttl=64

docker exec app ping -c 2 redis
# → 64 bytes from 172.18.0.4 (172.18.0.4): seq=0 ttl=64

Tại sao custom bridge tốt hơn?

Tính năng Default Bridge Custom Bridge
DNS resolution (gọi bằng tên) ❌ Không ✅ Có — Docker embedded DNS
Tự động kết nối ✅ Tất cả container vào chung ❌ Phải chỉ định --network
Cô lập giữa các project ❌ Tất cả container thấy nhau ✅ Chỉ container cùng network mới thấy nhau
Connect/Disconnect runtime docker network connect/disconnect
Production ready ❌ Chỉ dùng để test nhanh ✅ Recommended cho mọi project

3. Docker Embedded DNS — Cơ chế phân giải tên

Khi container gọi mysql (service name), đây là flow xảy ra bên trong:

1. Container app gọi: mysql_connect("mysql", ...)
                          │
2. OS resolve "mysql"     │
                          ▼
3. DNS query đến Docker DNS Server (127.0.0.11)
                          │
4. Docker DNS kiểm tra:   │
   "mysql" → container nào│trong cùng network?
                          │
5. Trả về IP: 172.18.0.3  │
                          ▼
6. Container app kết nối đến 172.18.0.3:3306

Kiểm chứng:

# Xem DNS config bên trong container
docker exec app cat /etc/resolv.conf
# → nameserver 127.0.0.11
# → options ndots:0

# 127.0.0.11 = Docker embedded DNS server
# Nó tự động map container name → IP trong cùng network

4. Port Mapping — Expose service ra host

Container mặc định không expose port ra ngoài host. Để truy cập từ browser hoặc host tools:

# Cú pháp: -p <host_port>:<container_port>
docker run -d --name web -p 8080:80 nginx:alpine

# Bây giờ truy cập http://localhost:8080 → nginx container

Cơ chế bên trong:

Browser → localhost:8080
              │
     iptables DNAT rule
     (Docker tự tạo)
              │
              ▼
   Container web:80 (172.18.0.2:80)

Docker sử dụng iptables rules (Linux) để NAT traffic từ host port vào container port.

Các biến thể port mapping:

# Map port cụ thể
-p 8080:80                  # host:8080 → container:80

# Map nhiều port
-p 8080:80 -p 8443:443      # host:8080→80, host:8443→443

# Map random host port
-p 80                       # host:<random> → container:80

# Chỉ bind localhost (không expose ra mạng ngoài)
-p 127.0.0.1:8080:80        # Chỉ truy cập từ host

# Xem port mapping
docker port web
# 80/tcp → 0.0.0.0:8080

Ví dụ Laravel stack:

                              Port mapping cần thiết
                              ─────────────────────
Browser → localhost:8000 ───→ Nginx container :80     ← CẦN -p 8000:80
                              │
                              ▼ (internal, qua network)
                              PHP-FPM container :9000  ← KHÔNG cần -p
                              │
                              ▼ (internal, qua network)
                              MySQL container :3306    ← KHÔNG cần -p *
                              
* Trừ khi bạn muốn kết nối MySQL từ host (Sequel Pro, TablePlus)
  → Lúc đó thêm -p 3306:3306

5. Host Network — Không cô lập

docker run -d --network host --name fast-nginx nginx:alpine

# Container dùng trực tiếp network của host
# Không cần -p, nginx tự listen trên host:80
curl localhost:80
# → nginx welcome page

Cơ chế: Container bỏ qua NET namespace — dùng trực tiếp network stack của host. Giống như chạy nginx trên host vậy.

Khi nào dùng:

  • Benchmark network performance (loại bỏ overhead NAT/bridge)
  • Container cần truy cập nhiều port trên host
  • Monitoring tools cần thấy toàn bộ network traffic

Trade-off:

  • ❌ Mất network isolation
  • ❌ Port conflict (2 container không thể cùng dùng port 80)
  • ❌ Không hoạt động trên Docker Desktop macOS/Windows (chỉ Linux)

6. None Network — Cô lập hoàn toàn

docker run -d --network none --name isolated alpine sleep 3600

# Container không có network interface (ngoài loopback)
docker exec isolated ip addr
# → Chỉ có lo (127.0.0.1), không có eth0

docker exec isolated ping 8.8.8.8
# → ping: sendto: Network unreachable

Khi nào dùng: Batch processing không cần network, chạy code untrusted, security-sensitive workloads.


Deep Dive: Cơ chế Network bên trong

Virtual Ethernet Pair (veth pair)

Mỗi container kết nối vào bridge network thông qua veth pair — một cặp virtual network interface:

Container namespace          Host namespace
┌────────────────┐          ┌────────────────────┐
│                │          │                    │
│   eth0         │──────────│  vethXXXX          │
│   172.18.0.2   │  (veth   │  (connected to     │
│                │   pair)  │   bridge br-xxx)   │
│                │          │                    │
└────────────────┘          └────────────────────┘
  • eth0 trong container = một đầu của veth pair
  • vethXXXX trên host = đầu kia, kết nối vào bridge
  • Bridge hoạt động như virtual switch — forward traffic giữa các veth
# Xem veth pairs trên host
ip link show type veth

# Xem bridge interfaces
brctl show  # hoặc: ip link show type bridge

Network Namespace chi tiết

Nhắc lại từ Bài 1: NET namespace cho mỗi container:

  • IP address riêng (172.18.0.x)
  • Routing table riêng
  • Port space riêng (container A và B đều có thể listen port 80)
  • iptables rules riêng

Điều này giải thích tại sao 3 container đều chạy trên port 9000, 80, 3306 mà không conflict — mỗi cái có port space riêng.


Quản lý Network

Các lệnh cơ bản

# Liệt kê networks
docker network ls

# Tạo custom network
docker network create my-net

# Tạo network với subnet cụ thể
docker network create --subnet=172.20.0.0/16 my-net

# Xem chi tiết network (containers đang kết nối, IP, v.v.)
docker network inspect my-net

# Kết nối container đang chạy vào network
docker network connect my-net existing-container

# Ngắt kết nối
docker network disconnect my-net existing-container

# Xóa network (phải không còn container nào kết nối)
docker network rm my-net

# Dọn networks không dùng
docker network prune

Container kết nối nhiều network

Một container có thể kết nối nhiều network cùng lúc — hữu ích cho architecture phức tạp:

# Tạo 2 networks
docker network create frontend
docker network create backend

# Nginx: chỉ frontend (nhận request từ bên ngoài)
docker run -d --name nginx --network frontend -p 80:80 nginx:alpine

# PHP-FPM: cả frontend + backend (nhận request từ nginx, gọi MySQL)
docker run -d --name app --network frontend php:8.2-fpm-alpine
docker network connect backend app

# MySQL: chỉ backend (không expose ra frontend)
docker run -d --name mysql --network backend \
  -e MYSQL_ROOT_PASSWORD=secret mysql:8.0
┌─────────────────────────────────────────┐
│           Frontend Network              │
│                                         │
│  ┌───────────┐      ┌───────────┐       │
│  │   Nginx   │─────▶│  PHP-FPM  │       │
│  │  :80      │      │  :9000    │       │
│  └───────────┘      └─────┬─────┘       │
│                           │              │
└───────────────────────────┼──────────────┘
                            │ (app kết nối cả 2 networks)
┌───────────────────────────┼──────────────┐
│           Backend Network │              │
│                           │              │
│                     ┌─────┴─────┐       │
│                     │  PHP-FPM  │       │
│                     │  :9000    │       │
│                     └─────┬─────┘       │
│                           │              │
│                     ┌─────▼─────┐       │
│                     │   MySQL   │       │
│                     │  :3306    │       │
│                     └───────────┘       │
│                                         │
└─────────────────────────────────────────┘

→ Nginx KHÔNG thể gọi MySQL (khác network)
→ PHP-FPM gọi được cả Nginx và MySQL (ở cả 2 networks)

Đây là network segmentation — pattern phổ biến trong production để giảm attack surface.


Áp dụng cho Laravel Stack

Kiến trúc network cho Laravel Docker project

┌──────────────────── Docker Bridge Network: laravel-net ───────────────────┐
│                                                                          │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐            │
│  │  Nginx   │───▶│ PHP-FPM  │───▶│  MySQL   │    │  Redis   │            │
│  │  :80     │    │  :9000   │    │  :3306   │    │  :6379   │            │
│  │          │    │          │───▶│          │    │          │            │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘            │
│       │                                                                  │
│  -p 8000:80                                                              │
│  (expose ra host)                                                        │
└──────────────────────────────────────────────────────────────────────────┘

.env Laravel trong Docker

# Tất cả dùng SERVICE NAME, không dùng IP
DB_HOST=mysql          # ← service name của MySQL container
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=secret

REDIS_HOST=redis       # ← service name của Redis container
REDIS_PORT=6379

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

Preview docker-compose.yml (Tuần 3)

# docker-compose.yml — Docker Compose tự tạo custom bridge network
services:
  nginx:
    image: nginx:alpine
    ports:
      - "8000:80"                    # Chỉ nginx cần expose ra host
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./src:/var/www/html
    depends_on:
      - app

  app:
    build: .
    volumes:
      - ./src:/var/www/html
    # KHÔNG cần ports — nginx gọi internal qua app:9000

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: laravel
    volumes:
      - mysql_data:/var/lib/mysql
    # KHÔNG cần ports — app gọi internal qua mysql:3306
    # Thêm ports: ["3306:3306"] chỉ khi cần kết nối từ host (TablePlus)

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  mysql_data:
  redis_data:

# Docker Compose tự động:
# 1. Tạo network: <project-name>_default (custom bridge)
# 2. Kết nối tất cả services vào network đó
# 3. DNS: mỗi service có thể gọi nhau bằng service name

Nginx config — upstream PHP-FPM

# nginx.conf
server {
    listen 80;
    server_name localhost;
    root /var/www/html/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app:9000;     # ← service name "app", port 9000
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

fastcgi_pass app:9000 — Nginx dùng Docker DNS resolve app → IP của PHP-FPM container → forward FastCGI request.


Trade-offs & Lưu ý

✅ Best Practices

  • Luôn dùng custom bridge network — không dùng default bridge
  • Dùng service name cho inter-container communication — không hardcode IP
  • Chỉ expose port cần thiết ra host (-p) — giảm attack surface
  • Network segmentation cho production — tách frontend/backend network

⚠️ Debugging phổ biến

Lỗi 1: "Connection refused" từ PHP đến MySQL

# Kiểm tra container có cùng network không
docker network inspect my-net
# → Xem phần "Containers" — cả app và mysql phải có mặt

# Kiểm tra DNS resolution
docker exec app ping -c 2 mysql
# Nếu "bad address" → container không cùng custom network

# Kiểm tra MySQL đã ready chưa
docker exec app nc -zv mysql 3306
# → Connection to mysql 3306 port [tcp/mysql] succeeded!

Lỗi 2: Port conflict trên host

docker run -p 3306:3306 mysql:8.0
# Error: port is already allocated

# Fix: dùng port khác
docker run -p 33060:3306 mysql:8.0
# → Kết nối từ host: mysql -h 127.0.0.1 -P 33060
# → Container khác trong cùng network vẫn dùng port 3306

Lỗi 3: Container start trước MySQL ready

MySQL mất vài giây để initialize. PHP container có thể start trước khi MySQL sẵn sàng:

# Kiểm tra MySQL health
docker exec mysql mysqladmin ping -uroot -psecret
# → mysqld is alive

# Giải pháp: dùng healthcheck + depends_on (sẽ học ở Bài 9: Compose nâng cao)

⚠️ Hiệu năng Network

Network mode Latency overhead Throughput Ghi chú
host ~0 (native) Native Không có NAT overhead
bridge (custom) ~microseconds Gần native NAT + bridge forwarding
bridge • port mapping ~microseconds thêm Gần native Thêm iptables DNAT

Trong thực tế, overhead của bridge network không đáng kể cho hầu hết ứng dụng. Chỉ cần quan tâm khi làm benchmark hoặc ứng dụng ultra-low-latency.


Thực hành

Bài 1: Chứng minh Default Bridge không có DNS

# Chạy 2 containers trên default bridge
docker run -d --name ping-a alpine sleep 3600
docker run -d --name ping-b alpine sleep 3600

# Ping bằng IP → OK
IP_B=$(docker inspect -f '.NetworkSettings.IPAddress' ping-b)
docker exec ping-a ping -c 2 $IP_B
# → OK

# Ping bằng tên → FAIL
docker exec ping-a ping -c 2 ping-b
# → ping: bad address 'ping-b'

# Cleanup
docker rm -f ping-a ping-b

Bài 2: Custom Bridge Network với DNS

# Tạo custom network
docker network create test-net

# Chạy 2 containers trên custom network
docker run -d --name ping-a --network test-net alpine sleep 3600
docker run -d --name ping-b --network test-net alpine sleep 3600

# Ping bằng tên → OK!
docker exec ping-a ping -c 2 ping-b
# → 64 bytes from 172.19.0.3: seq=0 ttl=64

# Kiểm tra DNS server
docker exec ping-a cat /etc/resolv.conf
# → nameserver 127.0.0.11

# Cleanup
docker rm -f ping-a ping-b
docker network rm test-net

Bài 3: Nginx + PHP-FPM giao tiếp qua Network

Đây là bài thực hành quan trọng nhất — mô phỏng kiến trúc Laravel:

# 1. Tạo network
docker network create laravel-net

# 2. Tạo folder project
mkdir -p ~/docker-lab/network-test/src/public

# 3. Tạo file PHP test
cat > ~/docker-lab/network-test/src/public/index.php << 'EOF'
<?php
echo "<h1>Hello from PHP-FPM!</h1>";
echo "<p>Hostname: " . gethostname() . "</p>";
echo "<p>Server IP: " . $_SERVER['SERVER_ADDR'] . "</p>";
echo "<p>Time: " . date('H:i:s') . "</p>";
EOF

# 4. Tạo nginx config
cat > ~/docker-lab/network-test/nginx.conf << 'EOF'
server {
    listen 80;
    root /var/www/html/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}
EOF

# 5. Chạy PHP-FPM container
docker run -d --name app \
  --network laravel-net \
  -v ~/docker-lab/network-test/src:/var/www/html \
  php:8.2-fpm-alpine

# 6. Chạy Nginx container
docker run -d --name nginx \
  --network laravel-net \
  -p 8080:80 \
  -v ~/docker-lab/network-test/src:/var/www/html \
  -v ~/docker-lab/network-test/nginx.conf:/etc/nginx/conf.d/default.conf \
  nginx:alpine

# 7. Test!
curl localhost:8080
# → <h1>Hello from PHP-FPM!</h1>
# → Hostname: <container-id>
# → Server IP: 172.19.0.2

# Hoặc mở browser: http://localhost:8080

Bài 4: Inspect network chi tiết

# Xem tất cả containers trong network
docker network inspect laravel-net
# → "Containers": {
#     "app": { "IPv4Address": "172.19.0.2/16" },
#     "nginx": { "IPv4Address": "172.19.0.3/16" }
#   }

# Xem network config từ phía container
docker exec app ip addr
# → eth0: 172.19.0.2/16

docker exec nginx ip addr
# → eth0: 172.19.0.3/16

# Xem routing table
docker exec app ip route
# → default via 172.19.0.1 dev eth0
# → 172.19.0.0/16 dev eth0

# Verify: nginx gọi app bằng tên
docker exec nginx ping -c 2 app
# → 64 bytes from 172.19.0.2

# Cleanup
docker rm -f app nginx
docker network rm laravel-net

Checklist kiểm tra hiểu bài

  • Giải thích được tại sao DB_HOST=127.0.0.1 không hoạt động trong Docker, mà phải dùng DB_HOST=mysql.
  • Phân biệt được 3 network drivers: bridge, host, none — khi nào dùng cái nào.
  • Giải thích được sự khác biệt giữa default bridge và custom bridge network.
  • Tạo được custom network, chạy 2+ containers, và chứng minh chúng gọi nhau bằng tên.
  • Hiểu port mapping (-p) — khi nào cần, khi nào không.
  • Thiết lập được Nginx → PHP-FPM giao tiếp qua Docker network.
  • Dùng docker network inspect để debug connectivity issues.

Kết nối bài tiếp theo

Bạn đã có đủ 3 mảnh ghép core: Volume (data persist) + Network (giao tiếp) + Dockerfile (build image). Ở Bài 7: Bài tập tổng hợp, bạn sẽ ghép toàn bộ stack Laravel (PHP-FPM + Nginx + MySQL) bằng tay — không dùng Docker Compose — để hiểu sâu Docker Compose làm gì đằng sau.


References

Bài viết liên quan

Đang cập nhật...