Bạn chạy hai container — một web, một client — và muốn client gọi web. Gõ http://web và... "bad address". Đổi sang tạo một mạng riêng, y hệt lệnh đó lại chạy. Vì sao? Câu trả lời nằm ở một khác biệt ít người biết giữa mạng bridge mặc định và mạng bridge tuỳ chỉnh: chỉ cái sau có DNS nội bộ cho phép container gọi nhau bằng tên. Bài này (phần 10 loạt Docker) chạy thật để làm rõ điều đó, cùng cách -p mở cổng cho host vào container.

Mạng tuỳ chỉnh: gọi nhau bằng tên

Tạo một mạng bridge riêng rồi cho các container vào đó — Docker tự chạy một DNS server nội bộ phân giải tên container thành IP:

docker network create mynet
docker run --network mynet --name web nginx
docker run --network mynet --name cli alpine
# trong cli, gọi web bằng TÊN, không cần biết IP:
wget http://web    # Docker phân giải "web" -> IP container của web

Đo thật trên mạng tuỳ chỉnh: cli gọi wget http://web_demo nhận về <title>Welcome to nginx!</title>; nslookup web_demo phân giải thành 172.22.0.2; ping web_demo 0% packet loss. Tên container hoạt động như hostname — bạn không cần biết hay hard-code IP.

Ảnh chụp đoạn mã nền tối minh hoạ container networking gọi nhau bằng tên và mở cổng ra host, mạng bridge tuỳ chỉnh có DNS nội bộ gọi nhau bằng tên docker network create mynet docker run --network mynet --name web nginx docker run --network mynet --name cli alpine cli gọi web bằng tên không cần biết IP wget http web Docker phân giải web thành IP container tự động, bẫy mạng bridge mặc định không có DNS theo tên container không --network vào bridge default docker0 ở đó gọi nhau bằng tên không được bad address luôn tạo mạng riêng cho app nhiều container docker compose tự làm điều này đừng dựa vào bridge mặc định, publish cổng -p mở đường từ host vào container docker run -p 8099 80 nginx cổng 8099 host sang 80 container -p HOST CONTAINER từ host curl 127.0.0.1 8099 sang nginx không -p thì cổng container chỉ trong mạng docker host không thấy, hai kiểu giao tiếp khác nhau container gọi container cùng mạng gọi bằng tên không cần -p host ngoài sang container cần -p publish cổng giữa các service dùng tên chỉ publish cổng thực sự cần lộ

Hình 1: Mạng bridge tuỳ chỉnh có DNS nội bộ nên container gọi nhau bằng tên; bridge mặc định thì không; -p HOST:CONTAINER publish cổng cho host vào container; container↔container dùng tên, host→container cần -p.

Đo thật: và bẫy mạng bridge mặc định

Ảnh chụp bảng kết quả chạy thật docker network cộng -p output thật, một mạng tuỳ chỉnh cli gọi web bằng tên wget http web_demo ra title Welcome to nginx title nslookup web_demo ra Name web_demo Address 172.22.0.2 ping web_demo ra 0 phần trăm packet loss DNS nội bộ phân giải tên, hai mạng bridge mặc định gọi bằng tên hỏng wget http defbr_web ra wget bad address defbr_web ping defbr_web ra ping bad address defbr_web bridge mặc định không phân giải tên container phải dùng IP, ba publish cổng -p 8099 80 docker run -p 8099 80 nginx curl http 127.0.0.1 8099 từ host ra 200 không -p container chạy nhưng host không vào được cổng 80, kết container gọi nhau cùng mạng tuỳ chỉnh dùng tên không cần -p host vào container cần -p publish cổng cần lộ

Hình 2: Chạy thật — mạng tuỳ chỉnh: cli gọi web_demo bằng tên thành công (nslookup → 172.22.0.2, ping 0% loss); mạng bridge mặc định: wget/ping defbr_web → bad address (không có DNS theo tên); -p 8099:80 cho curl 127.0.0.1:8099 từ host trả 200.

  • Mạng tuỳ chỉnh — gọi bằng tên OK: DNS nội bộ phân giải web_demo → 172.22.0.2.
  • Mạng bridge mặc định — gọi bằng tên HỎNG: hai container không dùng --network sẽ nằm trên bridge "default" (docker0). Ở đó wget http://defbr_web và ping defbr_web đều báo bad address 'defbr_web' — bridge mặc định không có DNS phân giải tên container. Bạn buộc phải dùng IP (mà IP thì đổi mỗi lần chạy lại). Đây là lý do luôn tạo mạng riêng cho app nhiều container — và cũng là điều docker compose tự làm hộ bạn.

Publish cổng: mở đường từ host vào container

Container↔container trong cùng mạng gọi nhau bằng tên, không cần publish cổng. Nhưng để host (hoặc thế giới ngoài) vào được một container, phải publish cổng bằng -p HOST:CONTAINER:

docker run -p 8099:80 nginx   # cổng 8099 của host -> cổng 80 của container

Đo thật: curl http://127.0.0.1:8099 từ host trả về 200. Không có -p, container vẫn chạy và lắng nghe cổng 80, nhưng cổng đó chỉ tồn tại trong mạng Docker — host không thấy, không gọi vào được (như bài network namespace đã giải thích: container có stack mạng riêng sau NAT).

Hai kiểu giao tiếp, đừng nhầm

Đây là mô hình cần khắc sâu:

  • container ↔ container (cùng mạng): gọi nhau bằng tên, không cần -p. Ví dụ web app gọi database container bằng tên db.
  • host/ngoài → container: cần -p publish cổng.

Hệ quả thực dụng: giữa các service nội bộ dùng tên (không publish), và chỉ publish cổng thực sự cần lộ ra ngoài (thường chỉ mỗi cổng web). Publish thừa là mở bề mặt tấn công — database không nên -p ra host nếu chỉ app trong mạng cần nó.

Đánh đổi cần cân nhắc

Publish cổng mặc định lộ ra mọi interface — cẩn thận 0.0.0.0. -p 8099:80 thực ra bind vào 0.0.0.0:8099 (mọi interface của host), nghĩa là máy khác trong mạng LAN cũng gọi vào được. Nếu chỉ muốn truy cập từ chính máy đó, dùng -p 127.0.0.1:8099:80. Nhiều sự cố lộ database/dashboard ra Internet đến từ việc publish lên 0.0.0.0 mà không để ý.

DNS nội bộ phân giải theo tên container và alias mạng. Ngoài tên container, bạn đặt được --network-alias để nhiều container trả lời cùng một tên (dùng cho load-balance đơn giản, hay đặt tên dịch vụ ổn định). Trong docker compose, tên service trở thành hostname — đó là lý do compose "vừa hoạt động" mà không cần bạn quản lý IP.

Đừng dựa vào IP container — chúng thay đổi. IP được cấp động mỗi lần container khởi động, nên hard-code IP là công thức cho bug. Luôn dùng tên (trên mạng tuỳ chỉnh). Nếu một công cụ cũ buộc phải có IP, cân nhắc gán IP tĩnh trong mạng tuỳ chỉnh — nhưng ưu tiên tên bất cứ khi nào có thể.

Ba ý mang về

  1. Mạng bridge tuỳ chỉnh có DNS nội bộ — gọi nhau bằng tên: đo thật cli gọi web_demo bằng tên thành công (nslookup → 172.22.0.2, ping 0% loss); tên container hoạt động như hostname, không cần biết IP.
  2. Bridge mặc định KHÔNG có DNS theo tên: đo thật gọi bằng tên trên mạng mặc định báo bad address — luôn tạo mạng riêng cho app nhiều container (docker compose tự làm), đừng dựa vào bridge mặc định.
  3. container↔container dùng tên, host→container cần -p: đo thật -p 8099:80 cho curl host trả 200; chỉ publish cổng thực sự cần lộ (và cân nhắc bind 127.0.0.1 để không lộ ra LAN/Internet).

Nguồn

Phần sau ta xem dữ liệu container sống sót thế nào: volume và bind mount — vì sao dữ liệu ghi vào container biến mất khi xoá, và cách giữ nó bền qua vòng đời container.