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.

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

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
--networksẽ nằm trên bridge "default" (docker0). Ở đówget http://defbr_webvàping defbr_webđều báobad 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ềudocker composetự 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êndb. - host/ngoài → container: cần
-ppublish 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ề
- Mạng bridge tuỳ chỉnh có DNS nội bộ — gọi nhau bằng tên: đo thật
cligọiweb_demobằ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. - 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. - container↔container dùng tên, host→container cần
-p: đo thật-p 8099:80chocurlhost trả 200; chỉ publish cổng thực sự cần lộ (và cân nhắc bind127.0.0.1để không lộ ra LAN/Internet).
Nguồn
- Docker docs — Networking overview: https://docs.docker.com/engine/network/
- Docker docs — Bridge network driver / user-defined bridges: https://docs.docker.com/engine/network/drivers/bridge/
- Docker docs — Published ports: https://docs.docker.com/engine/network/#published-ports
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.