Docker for Developers · Part 4 — Networking: Bridge, Ports & Service Discovery
How containers talk: the default bridge vs user-defined networks, automatic DNS by container name, publishing vs exposing ports, and wiring an app container to a database container.
This is Part 4 of a 10-part series that takes you from “I’ve heard of Docker” to confidently building, running, and debugging containerized apps — Docker, Docker Compose, and Kubernetes {Đây là Phần 4 của series 10 bài đưa bạn từ “mới nghe nói về Docker” đến tự tin build, chạy và debug ứng dụng container — Docker, Docker Compose và Kubernetes}. If you skipped persistence, catch up in Part 3 — Volumes, Bind Mounts & Env {Nếu bạn bỏ qua phần lưu dữ liệu, xem lại Phần 3 — Volume, bind mount & biến môi trường}. Every part ends with exercises; do them, don’t just read {Mỗi phần kết thúc bằng bài tập; hãy làm, đừng chỉ đọc}.
Containers are isolated by design — each gets its own network namespace, so it cannot casually talk to your host, the internet, or a neighbor container unless you wire that up {Container được cô lập theo thiết kế — mỗi cái có network namespace riêng, nên không thể tự nhiên nói chuyện với host, internet hay container bên cạnh trừ khi bạn nối dây}. This part answers the practical question: how do they reach each other and the outside world? {Phần này trả lời câu thực tế: làm sao chúng nói chuyện với nhau và ra ngoài?}
The Docker network model {Mô hình mạng Docker}
By default, every container joins a virtual Ethernet network managed by Docker on the host {Mặc định, mọi container tham gia một mạng Ethernet ảo do Docker quản lý trên host}. Traffic between containers on the same bridge stays on the host; traffic to the internet is NAT’d out through the host {Lưu lượng giữa các container trên cùng bridge ở lại host; ra internet được NAT qua host}.
HOST (your laptop / server)
┌──────────────────────────────────────────────────────────────────┐
│ Browser / curl ──► localhost:8080 ──► port publish (-p) │
│ │ │
│ ┌──────────────────────────────────────────▼─────────────────┐ │
│ │ docker0 (Linux bridge, 172.17.0.0/16) │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ container │ eth0 │ container │ eth0 │ │
│ │ │ web :80 │◄────────────►│ db :5432 │ │ │
│ │ │ 172.17.0.2 │ same │ 172.17.0.3 │ │ │
│ │ └─────────────┘ bridge └─────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ▲ NAT to internet │
└─────────┼────────────────────────────────────────────────────────┘
└── physical NIC (Wi‑Fi / ethernet)
Docker picks a network driver when you create or attach a network {Docker chọn network driver khi bạn tạo hoặc gắn mạng}:
| Driver | Role {Vai trò} |
|---|---|
| bridge (default) | Private network on a single host; containers get virtual NICs {Mạng riêng trên một host; container có NIC ảo}. |
| host | Container shares the host’s network stack — no isolation, no -p needed {Container dùng chung stack mạng host — không cô lập, không cần -p}. |
| none | No networking — only lo inside the container {Không mạng — chỉ lo trong container}. |
| overlay | Multi-host networks (Swarm/Kubernetes territory) — not daily docker run {Mạng đa host (Swarm/K8s) — không phải docker run hằng ngày}. |
Default bridge vs user-defined bridge {Bridge mặc định vs bridge tự tạo}
When you docker run without --network, Docker attaches the container to the built-in bridge network (often shown as bridge in docker network ls) {Khi docker run không có --network, Docker gắn container vào mạng bridge sẵn có (thường hiện là bridge trong docker network ls)}. That is the default bridge — one shared LAN for containers that didn’t ask for anything special {Đó là default bridge — một LAN chung cho container không chỉ định mạng}.
For real apps with two or more containers, create a user-defined bridge {Với app từ hai container trở lên, hãy tạo user-defined bridge}:
docker network create appnet
The crucial difference {Khác biệt cốt lõi}:
| Default bridge | User-defined bridge (e.g. appnet) | |
|---|---|---|
| DNS by container name | No — use IP (fragile) or --link (legacy) | Yes — Docker embedded DNS resolves db → container IP |
| Isolation from other stacks | Weak — everything lands on one LAN | Strong — only containers you attach can talk |
| Best practice for multi-container | Avoid | Always use this |
On a user-defined bridge, containers reach each other with the hostname = container name (or network alias). On the default bridge, name resolution does not work — you’ll chase changing IPs and wonder why
ping dbfails {Trên bridge tự tạo, container gọi nhau bằng hostname = tên container (hoặc alias). Trên default bridge, phân giải tên không chạy — bạn sẽ đuổi IP đổi liên tục và thắc mắc vì saoping dbfail}.
Managing networks {Quản lý mạng}
docker network create appnet # user-defined bridge
docker network ls # list networks
docker network inspect appnet # subnets, connected containers, IPs
docker run -d --name web --network appnet nginx
docker network connect appnet db # attach a running container
docker network disconnect appnet db
docker network rm appnet # only if no containers use it
docker network inspect is your X-ray: which containers are plugged in, which IP each got, and the gateway {docker network inspect là X-quang: container nào đang cắm, IP nào, gateway là gì}. When debugging “can’t connect”, inspect both ends and confirm they share the same network name {Khi debug “không kết nối được”, inspect cả hai đầu và xác nhận chúng cùng tên mạng}.
Publishing vs exposing ports {Publish vs expose cổng}
Two different ideas often mixed up {Hai khái niệm hay bị trộn}:
EXPOSE 5432in a Dockerfile — documentation only. It does not open a port on your host or even guarantee the port is reachable from another container {EXPOSE 5432trong Dockerfile — chỉ là metadata. Không mở cổng trên host hay đảm bảo container khác truy cập được}.-p 8080:80(publish) — Docker sets up port forwarding fromhost:8080→container:80. This is how your browser on the laptop reaches nginx inside a container {-p 8080:80(publish) — Docker forward từhost:8080→container:80. Đây là cách trình duyệt trên laptop tới nginx trong container}.
docker run -d --name web -p 8080:80 nginx # publish — works from host browser
docker run -d --name web --expose 80 nginx # metadata only — host cannot reach it
localhost inside a container means the container itself, not your laptop and not another container {localhost trong container là chính container đó, không phải laptop và không phải container khác}. If your app container tries postgres://localhost:5432, it looks for Postgres inside itself — usually empty {Nếu app container dùng postgres://localhost:5432, nó tìm Postgres trong chính nó — thường không có gì}. Point at the database container’s name on a shared user network instead {Hãy trỏ tới tên container database trên cùng user network}.
Container-to-container communication {Giao tiếp container với container}
Worked example: app network + Postgres + psql client {Ví dụ thực hành: mạng app + Postgres + client psql}.
1. Create a user-defined network and start Postgres {Tạo mạng tự định nghĩa và chạy Postgres}:
docker network create appnet
docker run -d \
--name db \
--network appnet \
-e POSTGRES_PASSWORD=secret \
postgres:16-alpine
2. Run a client on the same network — connect by hostname db {Chạy client cùng mạng — kết nối bằng hostname db}:
docker run --rm -it --network appnet postgres:16-alpine \
psql -h db -U postgres -c 'SELECT version();'
# password when prompted: secret
Docker’s embedded DNS resolves db to the database container’s IP on appnet {DNS nhúng của Docker phân giải db thành IP của container database trên appnet}. You did not hard-code 172.18.0.2 — and you should not {Bạn không hard-code 172.18.0.2 — và không nên làm vậy}.
3. Prove default bridge breaks name resolution {Chứng minh default bridge làm hỏng phân giải tên}:
docker run -d --name db-default -e POSTGRES_PASSWORD=secret postgres:16-alpine
# no --network → lands on default bridge
docker run --rm -it postgres:16-alpine \
psql -h db-default -U postgres -c 'SELECT 1;'
# often: could not translate host name "db-default" to address
Both containers are on the default bridge, yet db-default is not a DNS name there — use docker inspect to find an IP (brittle) or move them to appnet {Cả hai trên default bridge, nhưng db-default không phải tên DNS ở đó — phải docker inspect lấy IP (dễ gãy) hoặc chuyển sang appnet}.
USER-DEFINED appnet DEFAULT bridge
┌────────┐ DNS: db ──► IP ┌────────┐ ┌────────┐
│ app │ ─────────────────────────► │ db │ │ db2 │
└────────┘ hostname works └────────┘ └────────┘
ping db2 by name → FAIL
Connection string pattern for apps {Mẫu connection string cho app}:
postgres://postgres:secret@db:5432/mydb
▲
container NAME on shared user network
host and none networks {Mạng host và none}
--network host — the container uses the host’s IP addresses and port space directly {Container dùng trực tiếp IP và cổng của host}. A process listening on :3000 inside the container is reachable at :3000 on the host without -p {Tiến trình listen :3000 trong container truy cập được :3000 trên host không cần -p}. Trade-off: no network isolation, port collisions with host services, behavior differs on Docker Desktop (macOS/Windows) vs Linux {Đổi lại: không cô lập mạng, trùng cổng với dịch vụ host, hành vi khác Docker Desktop vs Linux}. Occasionally used for high-performance or legacy tooling on Linux servers {Thỉnh thoảng dùng cho hiệu năng cao hoặc tool cũ trên Linux server}.
--network none — only the loopback interface inside the container {Chỉ có loopback trong container}. Useful for batch jobs that never need the network, or security-sensitive steps that must not phone home {Hữu ích cho job batch không cần mạng, hoặc bước nhạy cảm bảo mật không được gọi ra ngoài}.
Common pitfalls {Các bẫy thường gặp}
- Using
localhostto reach another container — wrong namespace. Use the service name on a user-defined network {Dùnglocalhostđể gọi container khác — sai namespace. Dùng tên dịch vụ trên user network}. - Bookmarking container IPs — IPs change when containers are recreated. DNS names on user-defined bridges are stable {Lưu IP container — IP đổi khi tạo lại container. Tên DNS trên user bridge ổn định hơn}.
- Forgetting to put both containers on the same network —
docker network connect appnet myappfixes a running app without restart {Quên cho cả hai cùng mạng —docker network connect appnet myappsửa app đang chạy không cần restart}. - Expecting default-bridge DNS — multi-container stacks belong on
docker network create ..., not the implicit default {Kỳ vọng DNS trên default bridge — stack nhiều container nên ởdocker network create ..., không phải default ngầm}. - Publishing the DB port
-p 5432:5432in dev — convenient for GUI clients on the host, but in production prefer internal-only DB on a private network {Publish DB-p 5432:5432khi dev — tiện cho GUI trên host, nhưng production nên để DB chỉ trong mạng riêng}.
Cheat sheet {Bảng tra nhanh}
# networks
docker network create appnet
docker network ls
docker network inspect appnet
docker network connect appnet mycontainer
docker network disconnect appnet mycontainer
docker network rm appnet
# run on a network + publish ports
docker run -d --name web --network appnet -p 8080:80 nginx
docker run -d --name db --network appnet -e POSTGRES_PASSWORD=s postgres:16-alpine
# debug from inside a container
docker exec -it web sh
# ping db, curl http://db:5432, cat /etc/resolv.conf
# cleanup
docker stop web db && docker rm web db
docker network rm appnet
Bài tập / Exercises
Do these in order; each reinforces the rules you’ll use in Compose (Part 5) {Làm theo thứ tự; mỗi bài củng cố quy tắc bạn sẽ dùng trong Compose (Phần 5)}.
1. Create a user-defined bridge network named labnet and verify it exists {Tạo bridge tên labnet và xác nhận nó tồn tại}.
Solution {Lời giải}
docker network create labnet
docker network ls | grep labnet
docker network inspect labnet # see driver "bridge", empty containers until you attach some2. On labnet, run two alpine containers named ping-a and ping-b. From ping-a, ping ping-b by name (not IP) {Trên labnet, chạy hai alpine ping-a và ping-b. Từ ping-a, ping ping-b bằng tên (không dùng IP)}.
Solution {Lời giải}
docker run -d --name ping-a --network labnet alpine sleep 3600
docker run -d --name ping-b --network labnet alpine sleep 3600
docker exec ping-a ping -c 3 ping-b
# 3 packets transmitted, 3 received3. Run a container named solo on the default bridge (no --network). Start another throwaway alpine and try ping solo — observe name resolution failing {Chạy solo trên default bridge (không --network). Dùng alpine dùng-một-lần thử ping solo — quan sát phân giải tên fail}.
Solution {Lời giải}
docker run -d --name solo alpine sleep 3600
docker run --rm alpine ping -c 1 solo
# ping: bad address 'solo' (or unknown host) — default bridge has no embedded DNS by name
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' solo
# use that IP instead — works, but breaks when solo is recreated
docker run --rm alpine ping -c 1 <solo-ip-from-inspect>4. Publish nginx on host port 9090 and curl it from your machine (not from inside the container) {Publish nginx cổng host 9090 và curl từ máy bạn (không phải trong container)}.
Solution {Lời giải}
docker run -d --name webpub -p 9090:80 nginx
curl -sI http://localhost:9090 | head -n 1
# HTTP/1.1 200 OK5. Wire Postgres on labnet as db, then connect with psql -h db from a one-off client on the same network {Gắn Postgres trên labnet tên db, rồi psql -h db từ client một lần cùng mạng}.
Solution {Lời giải}
docker run -d --name db --network labnet \
-e POSTGRES_PASSWORD=secret postgres:16-alpine
docker run --rm -it --network labnet postgres:16-alpine \
psql -h db -U postgres -c "SELECT current_database();"
# enter password: secretStretch {Nâng cao}: run webpub and db both on labnet, docker network connect labnet webpub, then docker exec webpub ping -c 2 db — same image as production topology, still no Compose file {Chạy webpub và db trên labnet, docker network connect labnet webpub, rồi docker exec webpub ping -c 2 db — giống topology production, chưa cần Compose}.
Solution {Lời giải}
docker network connect labnet webpub
docker exec webpub ping -c 2 dbKey takeaways {Điểm chính}
- Containers are isolated; networks are how you grant controlled connectivity {Container bị cô lập; mạng là cách bạn cấp kết nối có kiểm soát}.
- User-defined bridges give embedded DNS by container name; the default bridge does not {User-defined bridge có DNS theo tên container; default bridge thì không}.
-p host:containerpublishes to the host;EXPOSEis metadata only {-p host:containerpublish ra host;EXPOSEchỉ là metadata}.localhostinside a container is not the host or a sibling — use service names on a shared user network {localhosttrong container không phải host hay container anh em — dùng tên dịch vụ trên user network chung}.- Multi-container apps:
docker network createfirst, then--network(orconnect) — never rely on default-bridge name resolution {App nhiều container:docker network createtrước, rồi--network(hoặcconnect) — đừng trông chờ DNS trên default bridge}.
Next up {Tiếp theo}
Part 5 — Docker Compose Fundamentals: stop typing long docker run chains — Compose declares networks, volumes, and images in one YAML file and brings the whole stack up with docker compose up {Phần 5 — Docker Compose cơ bản: bỏ chuỗi docker run dài — Compose khai báo mạng, volume và image trong một file YAML và bật cả stack bằng docker compose up}.