Một vấn đề quen thuộc

Hai container chạy chung một Compose network, gọi nhau bằng service name — mọi thứ hoạt động bình thường. Rồi một ngày curl api:3000 trả về Connection timed out. Không ai deploy gì, không ai đụng config. Bạn bắt đầu debug:

Debug bắt đầu:

$ docker exec -it web sh
/ # curl api:3000
curl: (28) Connection timed out after 30000 ms
/ # nslookup api
Server:    127.0.0.11
Address:   127.0.0.11:53

Name:   api
Address: 172.20.0.3
/ # ping 172.20.0.3
PING 172.20.0.3 (172.20.0.3): 56 data bytes
--- 172.20.0.3 ping statistics ---
5 packets transmitted, 0 packets received, 100% packet loss

DNS resolve ra 172.20.0.3 — nhưng IP đó không còn tồn tại nữa. Container api đã bị restart, nhận IP mới 172.20.0.5. DNS cache trong container web vẫn trỏ về IP cũ. Bốn tiếng sau mới tìm ra nguyên nhân.

Bài này đi sâu vào Docker networking từ góc độ debug: hiểu để biết phải bắt đầu từ đâu khi mọi thứ không hoạt động.

Bài này tập trung vào debug và hands-on. Nếu bạn cần nắm fundamentals về các khái niệm mạng trong container, hãy đọc Mạng trong Docker — bài này đi sâu hơn vào công cụ và tình huống thực tế, bài kia cover background nền tảng.

1. Network drivers — chọn đúng driver cho đúng bài toán

Docker cung cấp 5 network driver chính. Chọn sai driver là nguyên nhân phổ biến nhất của các vấn đề connectivity.

Bridge (default)

Default khi bạn chạy container không chỉ định --network. Tất cả container trên cùng một bridge network có thể giao tiếp với nhau qua IP. Custom bridge (do user tạo) có thêm embedded DNS — khác biệt quan trọng mà ta sẽ phân tích kỹ ở mục 3.

# Xem tất cả network
docker network ls

# Tạo custom bridge network
docker network create --driver bridge mynet

# Chạy container trong network đó
docker run -d --network mynet --name web nginx

Host

Container dùng trực tiếp network stack của host — không namespace riêng, không NAT, không bridge. Hiệu năng cao nhất vì không có overhead ảo hóa mạng, nhưng port conflict là vấn đề: hai container không thể cùng bind port 80.

docker run --network host nginx
# Container NGINX sẽ listen trực tiếp trên port 80 của host

Chỉ nên dùng khi bạn cần maximum network throughput (high-frequency trading, real-time video streaming) và chấp nhận đánh đổi isolation.

None

Container không có network interface nào ngoài loopback. Dùng cho batch job xử lý file cục bộ, hoặc container chỉ giao tiếp qua volume mount — khi bạn muốn chắc chắn container không thể kết nối ra ngoài.

docker run --network none alpine

Overlay

Dành cho multi-host — Docker Swarm hoặc Kubernetes (qua CNI plugin). Dùng VXLAN tunnel để tạo layer-2 overlay trên layer-3 network vật lý. Hỗ trợ encryption với --opt encrypted.

# Khởi tạo Swarm trước
docker swarm init

# Tạo overlay network
docker network create --driver overlay --opt encrypted my-overlay
Overlay network yêu cầu các port sau mở giữa các node Swarm: TCP 2377 (cluster management), TCP/UDP 7946 (control plane), UDP 4789 (VXLAN data plane). Firewall chặn các port này là bug kinh điển khi setup Swarm cluster.

Macvlan / IPvlan

Container có địa chỉ MAC và IP riêng, xuất hiện như một thiết bị vật lý thực sự trên mạng. Dùng khi container cần giao tiếp trực tiếp với các thiết bị vật lý khác trong cùng subnet — ví dụ: container cần join Active Directory domain, hoặc ứng dụng legacy yêu cầu MAC address cố định.

docker network create -d macvlan \
  --subnet=192.168.1.0/24 \
  --gateway=192.168.1.1 \
  -o parent=eth0 \
  my-macvlan
Macvlan vs IPvlan: Macvlan gán mỗi container một MAC riêng — clean nhưng nhiều cloud provider (AWS, GCP) không cho phép multiple MAC trên một interface ảo, dẫn đến container không nhận được traffic. IPvlan dùng chung MAC với host interface và phân biệt bằng IP — tương thích tốt hơn với cloud. Nếu bạn thấy container macvlan chạy local OK nhưng lên cloud không hoạt động, 90% là do hạn chế MAC filtering của cloud provider.

2. Bridge network deep dive

Đây là phần quan trọng nhất để debug. Hiểu được cách bridge network hoạt động ở tầng Linux giúp bạn biết chính xác phải kiểm tra gì.

Mô hình tổng quan


  flowchart TB
    subgraph HOST[Host Network Stack]
        eth0[eth0<br/>192.168.1.10]
        docker0[docker0 bridge<br/>172.17.0.1]
        iptables[iptables<br/>NAT + Filter]
    end

    subgraph C1[Container A]
        eth0_c1[eth0@ifX<br/>172.17.0.2]
        app1[app :3000]
    end

    subgraph C2[Container B]
        eth0_c2[eth0@ifY<br/>172.17.0.3]
        app2[app :8080]
    end

    eth0_c1 <-->|veth pair| docker0
    eth0_c2 <-->|veth pair| docker0
    docker0 -->|DNAT port 8080| iptables
    iptables --> eth0
    eth0 -->|Internet| OUT[Outside World]

Cốt lõi: Docker tạo một Linux bridge (docker0), mỗi container kết nối vào bridge này qua một cặp veth (virtual Ethernet). Một đầu của veth nằm trong container namespace (thành eth0), đầu kia cắm vào bridge. Bridge hoạt động như một switch layer-2.

Các câu lệnh debug chính

# Xem topology của network
docker network inspect mynet

# Kết quả cho thấy: subnet, gateway, danh sách container + IP
# "Containers": {
#   "abc123...": {
#     "Name": "web",
#     "IPv4Address": "172.20.0.2/16"
#   }
# }

Trên host Linux, bạn có thể dùng các công cụ native:

# Xem bridge
ip link show docker0

# Xem iptables rules
iptables -t nat -L DOCKER -n -v

# Xem veth pair — match bằng ifindex
ip link | grep veth

Port publishing và iptables

Khi bạn chạy docker run -p 8080:80, Docker thêm một DNAT rule vào iptables:

iptables -t nat -A PREROUTING  -p tcp --dport 8080 -j DNAT \
         --to-destination 172.17.0.2:80
iptables -t nat -A DOCKER     -p tcp --dport 8080 -j DNAT \
         --to-destination 172.17.0.2:80
iptables -t filter -A DOCKER  -p tcp -d 172.17.0.2 --dport 80 -j ACCEPT

Đây là lý do port publishing hoạt động ngay cả khi container không expose port nào trong Dockerfile. Rule cuối cùng (filter) là quan trọng nhất: nó cho phép forward gói tin đến container — nếu không có rule này, container nhận được gói tin nhưng response bị drop.

Dùng -p 127.0.0.1:8080:80 để bind port về localhost thay vì 0.0.0.0. Port của container sẽ chỉ accessible từ chính host đó — không expose ra network bên ngoài. Đây là best practice cho database container hoặc internal service mà bạn không muốn public.

3. Custom bridge vs default bridge — tại sao DNS chỉ hoạt động ở một bên

Đây là “gotcha” kinh điển gây mất thời gian nhất khi debug Docker network.

Default bridge (docker run không có --network): không có DNS resolution. Container chỉ có thể giao tiếp qua IP, không dùng được service name. Bạn vẫn thấy /etc/resolv.conf trỏ về nameserver của host, nhưng Docker không inject embedded DNS vào default bridge.

Custom bridge (docker network create + --network mynet): có embedded DNS server tại 127.0.0.11. Tự động resolve container name và service name (trong Compose) thành IP.

# Trên default bridge — fail
$ docker run --name web nginx
$ docker run --name app alpine ping web
ping: bad address 'web'

# Trên custom bridge — OK
$ docker network create mynet
$ docker run -d --network mynet --name web nginx
$ docker run --network mynet alpine ping web
PING web (172.20.0.2): 56 data bytes
64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.120 ms
Nếu bạn chạy docker-compose up thì Compose tự động tạo một custom bridge network (mặc định tên <project>_default), nên DNS hoạt động. Nhưng nếu bạn dùng docker run thủ công mà không chỉ định network, container sẽ rơi vào default bridge — và ping service-name sẽ không hoạt động. Đây là lý do hàng đầu khiến người mới nghĩ “Docker network bị lỗi” trong khi thực ra họ đang dùng default bridge.

4. DNS & Service discovery — deep dive

Embedded DNS hoạt động thế nào

Mỗi container trong custom bridge network có /etc/resolv.conf trỏ về 127.0.0.11 — đây là embedded DNS server do dockerd quản lý. Khi container gửi query DNS:

  1. Query đến 127.0.0.11.
  2. Dockerd kiểm tra cache: nếu service name trùng tên container hoặc service alias trong network đó, trả về IP ngay.
  3. Nếu không match, forward query lên external DNS (thường là nameserver của host hoặc custom DNS config).
$ docker exec web cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0

DNS cache và TTL — thủ phạm của bug mở đầu

Embedded DNS của Docker không phải lúc nào cũng refresh kịp thời. Khi container restart và nhận IP mới, DNS record cũ vẫn tồn tại trong cache của các container khác — đặc biệt là với các ứng dụng dùng connection pool hoặc DNS cache nội bộ (Node.js dns.lookup, Java InetAddress).

Debug DNS:

# Kiểm tra DNS từ trong container
docker exec web nslookup api

# Hoặc dùng dig nếu có
docker exec web dig api

# Kiểm tra IP thực tế của container api
docker inspect api | grep IPAddress

Giải pháp: dùng depends_on với condition: service_healthy trong Compose để đảm bảo thứ tự khởi động, và implement retry logic với backoff ở application layer. Đừng dựa vào DNS cache để đảm bảo freshness — luôn có cơ chế reconnect khi connection failed.

ndots:0 trong resolv.conf: Với setting này, mọi query DNS đều được gửi trực tiếp đến nameserver mà không thêm domain suffix. Điều này khác với Kubernetes (ndots:5) — nơi short name như api có thể được resolve thành api.default.svc.cluster.local. Hiểu sự khác biệt này giúp bạn tránh “nó hoạt động trong K8s sao trong Docker không hoạt động” và ngược lại.

Service discovery trong Docker Compose

Compose tự động tạo DNS record cho mỗi service name và cả container name. Ngoài ra, bạn có thể định nghĩa alias:

services:
  api:
    networks:
      mynet:
        aliases:
          - backend
          - api.internal

Lúc này, backendapi.internal đều resolve ra IP của container api. Cực kỳ hữu ích khi bạn muốn có multiple DNS names cho cùng một service (ví dụ: legacy code gọi backend, code mới gọi api.internal).

5. Network troubleshooting — workflow debug từng bước

Khi container không kết nối được, đây là checklist debug theo thứ tự từ ngoài vào trong:

Bước 1: Kiểm tra network topology

docker network inspect <network-name>

Kiểm tra: subnet, gateway, danh sách container, IP của từng container. Xác nhận container bạn muốn kết nối có trong danh sách này không, và IP có đúng không.

Bước 2: Ping từ trong container

docker exec <container> ping <target-ip>
docker exec <container> ping <service-name>

Ping được IP nhưng không ping được service name = vấn đề DNS. Ping không được cả IP = vấn đề routing hoặc firewall.

Bước 3: Kiểm tra DNS

docker exec <container> nslookup <service-name>
docker exec <container> cat /etc/resolv.conf

nslookup trả về IP cũ trong khi container đã restart = DNS cache stale. Restart container client hoặc flush DNS cache trong ứng dụng.

Bước 4: Kiểm tra interface và IP

docker exec <container> ip addr
docker exec <container> ip route

Xác nhận container có IP trong đúng subnet và default route trỏ về đúng gateway.

Bước 5: Join network namespace để debug nâng cao

Khi bạn cần debug ở tầng network mà container không có sẵn công cụ:

# Chạy container debug dùng chung network namespace với container cần debug
docker run --rm -it --network container:<container-name> nicolaka/netshoot

# Lúc này bạn có tcpdump, strace, iptables, ip, ss, netstat...
# và đang ở trong chính network namespace của container đó
tcpdump -i eth0 port 3000

Đây là kỹ thuật mạnh nhất để debug network mà không cần cài thêm tool vào container production.

Bước 6: Dùng nsenter (Linux host)

# Lấy PID của container
PID=$(docker inspect -f '{{.State.Pid}}' <container>)

# Enter network namespace của container đó
nsenter -t $PID -n ip addr
nsenter -t $PID -n tcpdump -i eth0
Image nicolaka/netshoot là “Swiss Army knife” cho Docker network troubleshooting. Nó bao gồm: iperf, tcpdump, ngrep, strace, iproute2, ethtool, curl, dig, nmap, socat, và hàng chục tool khác. Luôn có sẵn một netshoot container trong môi trường dev/staging — nó sẽ cứu bạn hàng giờ debug.

6. MTU issues — vấn đề khó thấy nhưng gây hậu quả lớn

Container mặc định kế thừa MTU từ docker0 bridge (thường là 1500). Nhưng nếu bạn dùng overlay network (VXLAN) hoặc network interface của host có MTU thấp hơn (VPN tunnel, PPPoE, jumbo frame trên switch mà container không biết), packet sẽ bị fragment hoặc drop — biểu hiện là: kết nối TCP thiết lập được, HTTP request gửi đi được, nhưng response lớn bị treo hoặc timeout.

Triệu chứng điển hình: curl small endpoint OK, nhưng upload file hoặc response >1400 bytes thì timeout.

# Kiểm tra MTU hiện tại trong container
docker exec <container> ip link show eth0

# Tạo network với MTU tùy chỉnh
docker network create --opt com.docker.network.driver.mtu=1400 mynet-low-mtu
MTU mismatch giữa container và host thường xảy ra khi: dùng VPN (WireGuard/OpenVPN giảm MTU), chạy container trên cloud VM có jumbo frame enabled, hoặc overlay network với VXLAN header 50 bytes. Luôn test với payload lớn (curl -d @largefile) khi setup network mới — đừng chỉ test với ping.

7. Overlay network & encryption

Overlay network cho phép container trên các host khác nhau giao tiếp như đang trong cùng một subnet. Cơ chế:

  1. Mỗi container có một VXLAN tunnel endpoint (VTEP).
  2. Gói tin từ container A được đóng gói trong VXLAN header rồi gửi qua network vật lý đến host của container B.
  3. VTEP của host B giải mã VXLAN, chuyển gói tin vào namespace của container B.
# Tạo overlay network với encryption
docker network create -d overlay \
  --opt encrypted \
  --subnet=10.0.0.0/24 \
  secure-overlay

# Kiểm tra VXLAN tunnel
ip -d link show | grep vxlan

Encryption (--opt encrypted) dùng IPSec (AES-GCM) để mã hóa toàn bộ VXLAN traffic giữa các node. Đánh đổi: thêm ~10-15% CPU overhead.

Overlay network performance: VXLAN thêm 50 bytes header, nên MTU hiệu dụng giảm còn 1450. Với encryption, thêm IPSec overhead. Trong production, benchmark kỹ throughput giữa các node — có thể cần tăng MTU của underlying network lên 1550 hoặc 9000 (jumbo frames) để bù lại overhead. Nếu bạn thấy throughput container-to-container qua overlay thấp hơn 60% so với throughput host-to-host trực tiếp, đây chính là nguyên nhân.

Tổng kết

Vấn đềNguyên nhân phổ biếnCách debug
Không resolve được service nameĐang dùng default bridge thay vì custom bridgedocker network inspect, kiểm tra xem container có cùng network không
DNS resolve ra IP cũContainer restart, DNS cache stalenslookup + docker inspect so sánh IP
Kết nối TCP timeoutIP không còn tồn tại, hoặc iptables rule bị xóadocker network inspect + iptables -t nat -L DOCKER
Ping được IP nhưng không connect được portỨng dụng trong container không listen đúng interfacedocker exec netstat -tlnp hoặc ss -tlnp
Response lớn bị treoMTU mismatchip link show eth0 trong container, so sánh với host
Overlay network không hoạt độngFirewall chặn port VXLAN/control planeMở port 2377, 7946, 4789 giữa các node

Câu hỏi hay gặp

Q: Tại sao docker-compose down rồi up lại thì IP container thay đổi? A: Mỗi lần container được tạo mới, Docker cấp một IP mới từ subnet pool (trừ khi bạn gán IP tĩnh với ipv4_address trong network config). Đây là lý do bạn luôn nên dùng service name thay vì hardcode IP. Nếu bắt buộc cần IP cố định, cấu hình ipam trong Compose network.

Q: Container A ping được container B nhưng curl không được — sao vậy? A: Ping dùng ICMP, curl dùng TCP. Nếu ping OK nhưng curl fail, kiểm tra: (1) Ứng dụng trong container B có đang listen trên 0.0.0.0 không (hay chỉ listen 127.0.0.1), (2) Port đã được publish hoặc container cùng network chưa, (3) Firewall trong container (iptables, ufw) có chặn port đó không.

Q: Làm sao để container trong default bridge gọi được service name? A: Không thể — default bridge không có embedded DNS. Chuyển sang custom bridge network. Hoặc dùng --link (deprecated, không khuyến nghị). Cách đúng: tạo custom network và attach cả hai container vào đó.

Q: Overlay network có cần etcd/consul không? A: Với Docker Swarm: không, Swarm manager tự quản lý control plane và key-value store. Với Docker standalone (không Swarm): cần external KV store như Consul, etcd, hoặc ZooKeeper. Nhưng thực tế, nếu bạn cần overlay mà không dùng Swarm, thường là dấu hiệu cho thấy nên chuyển sang Kubernetes hoặc Swarm mode.


Bài tiếp theo (Production): Phần 12: Docker trong CI/CD, build cache, BuildKit, multi-arch, và registry trong pipeline.