jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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}:

DriverRole {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}.
hostContainer 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}.
noneNo networking — only lo inside the container {Không mạng — chỉ lo trong container}.
overlayMulti-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 bridgeUser-defined bridge (e.g. appnet)
DNS by container nameNo — use IP (fragile) or --link (legacy)Yes — Docker embedded DNS resolves db → container IP
Isolation from other stacksWeak — everything lands on one LANStrong — only containers you attach can talk
Best practice for multi-containerAvoidAlways 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 db fails {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ì sao ping db fail}.

DEFAULT BRIDGE (docker0) USER-DEFINED: appnet app db name? ✗ no DNS — must use container IPs web db db ✓ embedded DNS — reach by service name :8080 host
On the default bridge, containers have no DNS and must use IPs; on a user-defined bridge, Docker's embedded DNS lets them reach each other by service name

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 5432 in a Dockerfile — documentation only. It does not open a port on your host or even guarantee the port is reachable from another container {EXPOSE 5432 trong 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 from host:8080container:80. This is how your browser on the laptop reaches nginx inside a container {-p 8080:80 (publish) — Docker forward từ host:8080container: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 hostnone}

--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 localhost to reach another container — wrong namespace. Use the service name on a user-defined network {Dùng localhost để 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 networkdocker network connect appnet myapp fixes a running app without restart {Quên cho cả hai cùng mạng — docker network connect appnet myapp sử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:5432 in dev — convenient for GUI clients on the host, but in production prefer internal-only DB on a private network {Publish DB -p 5432:5432 khi 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 some

2. 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-aping-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 received

3. 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 9090curl 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 OK

5. 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: secret

Stretch {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 webpubdb 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 db

Key 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 bridgeDNS theo tên container; default bridge thì không}.
  • -p host:container publishes to the host; EXPOSE is metadata only {-p host:container publish ra host; EXPOSE chỉ là metadata}.
  • localhost inside a container is not the host or a sibling — use service names on a shared user network {localhost trong 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 create first, then --network (or connect) — never rely on default-bridge name resolution {App nhiều container: docker network create trước, rồi --network (hoặc connect) — đừ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}.