Mạng trong Docker: Bridge, veth và Port Mapping
Nhớ Bài 2: mỗi container có network namespace riêng — một ngăn xếp mạng cô lập. Vậy làm sao container nói chuyện được với nhau và ra Internet? Bài này trả lời: các loại mạng Docker, cơ chế bên dưới, và cách cho container kết nối đúng.
Các network driver
Theo tài liệu Docker, có vài driver mạng, mỗi cái cho một mục đích:
- bridge — driver mặc định. Tạo một mạng riêng trên một host, container trong đó nói chuyện được với nhau; ra ngoài qua NAT.
- host — bỏ cô lập mạng: container dùng thẳng ngăn xếp mạng của host (không có network namespace riêng). Nhanh nhưng mất cô lập.
- none — không có mạng, container hoàn toàn cô lập về mạng.
- overlay — nối nhiều Docker host lại, cho container trên các máy khác nhau nói chuyện. Đây là nền của Docker Swarm (Bài 12).
- macvlan / ipvlan — cho container xuất hiện như thiết bị thật trên mạng vật lý.
Xem các mạng hiện có:
docker network ls
NAME DRIVER
bridge bridge ← mạng bridge mặc định
host host
none null
Bài này tập trung vào bridge (mạng phổ biến nhất khi chạy trên một máy); overlay để dành cho phần Swarm.
Bridge mặc định và cơ chế veth
Khi Docker khởi động trên Linux, nó tạo một bridge ảo tên docker0 — hình dung như một cái switch ảo. Mỗi container nối vào bridge bằng một veth pair: một cặp giao diện mạng ảo nối với nhau như hai đầu một sợi dây. Một đầu nằm trong network namespace của container (xuất hiện là eth0 bên trong), đầu kia cắm vào bridge docker0 trên host.
Host
┌────────────────────────────────────────────┐
│ eth0 (ra Internet) │
│ │ │
│ docker0 (bridge ảo = switch) │
│ │ │ │ │
│ veth veth veth │
│ │ │ │ │
│ ┌──┴───┐ ┌──┴───┐ ┌───┴──┐ │
│ │ eth0 │ │ eth0 │ │ eth0 │ ← trong │
│ │ ctn A│ │ ctn B│ │ ctn C│ container│
│ └──────┘ └──────┘ └──────┘ │
└────────────────────────────────────────────┘
Nhờ cùng cắm vào docker0, các container trên cùng bridge nói chuyện với nhau qua IP. Còn để ra Internet, lưu lượng từ container đi qua bridge rồi được host NAT (masquerade) ra eth0 — đó là lý do tài liệu nói container trên bridge mặc định "truy cập dịch vụ mạng bên ngoài qua masquerading" mà không cần cấu hình gì thêm.
Port publishing: cho bên ngoài vào container
Mặc định, cổng container chỉ truy cập được từ bên trong mạng Docker. Để mở ra ngoài host, dùng -p (đã gặp ở Bài 3):
docker run -d -p 8080:80 nginx:alpine
-p 8080:80 nghĩa là: lưu lượng tới cổng 8080 trên host được chuyển tiếp vào cổng 80 của container. Bên dưới, Docker thêm một luật NAT (DNAT) vào iptables để chuyển hướng gói tin.
Internet ──► host:8080 ──(iptables DNAT)──► container:80
EXPOSE trong Dockerfile (Bài 5) chỉ là tài liệu ghi "container này dùng cổng 80", không tự mở cổng ra host. Phải -p mới thật sự publish.
User-defined bridge: gọi nhau bằng tên
Đây là điểm quan trọng nhất khi chạy nhiều container cùng làm việc. Tài liệu Docker phân biệt rõ:
- Trên bridge mặc định, các container "truy cập nhau bằng địa chỉ IP", nhưng "không thể gọi nhau bằng tên".
- Trên user-defined bridge (mạng bạn tự tạo), container "dùng DNS nội bộ của Docker" và gọi được nhau bằng tên container.
Chứng minh. Tạo một mạng riêng và hai container trong đó:
docker network create appnet
docker run -d --name svc-a --network appnet alpine sleep 300
docker run --rm --network appnet alpine ping -c 2 svc-a
PING svc-a (172.20.0.2): 56 data bytes
64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.138 ms
Container ping được svc-a bằng tên — DNS nội bộ của Docker tự phân giải tên container thành IP. Giờ thử điều tương tự trên bridge mặc định:
docker run -d --name svc-b alpine sleep 300
docker run --rm alpine ping -c 1 svc-b
ping: bad address 'svc-b'
Không phân giải được tên. Đây là lý do luôn nên tạo user-defined network khi nhiều container cần nói chuyện (ví dụ web gọi database): chỉ cần gọi nhau bằng tên dịch vụ, không phải dò IP (vốn thay đổi mỗi lần container chạy lại). Docker Compose ở Bài 8 tự tạo một mạng như vậy cho bạn — đó là một lý do Compose tiện.
Kết nối container vào mạng
docker network create appnet # tạo mạng
docker run --network appnet ... # chạy container thẳng vào mạng
docker network connect appnet <ctn> # gắn thêm container đang chạy vào mạng
docker network inspect appnet # xem container nào trong mạng + IP
docker network disconnect appnet <ctn> # gỡ ra
Xem ai đang ở trong mạng:
docker network inspect appnet --format '{{range .Containers}}{{.Name}}={{.IPv4Address}} {{end}}'
svc-a=172.20.0.2/16
🧹 Dọn dẹp
docker rm -f svc-a svc-b 2>/dev/null
docker network rm appnet 2>/dev/null
docker network prune # xóa mạng không container nào dùng
Không xóa được mạng đang có container gắn vào — gỡ/xóa container trước.
Tổng kết
Container nối vào một bridge ảo (docker0) qua veth pair, nói chuyện với nhau qua đó và ra Internet qua NAT. -p publish cổng container ra host bằng luật DNAT. Quan trọng nhất: trên user-defined bridge, container gọi nhau bằng tên nhờ DNS nội bộ, còn bridge mặc định thì không — nên hãy tạo mạng riêng cho các container cần liên lạc.
Đến đây bạn đã có đủ mảnh: image, container, volume, mạng. Nhưng chạy tay từng docker run cho một ứng dụng nhiều thành phần (web + database + cache) thì rườm rà và dễ sai. Bài 8 gom tất cả vào một file với Docker Compose.