Overview
Bạn đã có 3 mảnh ghép: Image (đóng gói), Container (chạy), Volume (lưu data). Nhưng một Laravel app không chỉ có 1 container — bạn cần PHP-FPM + Nginx + MySQL + Redis chạy đồng thời và nói chuyện với nhau.
Ở Bài 1, bạn đã biết mỗi container có NET namespace riêng — tức là IP, port, routing table riêng biệt. Điều này có nghĩa:
- Container A không thể gọi container B qua
localhost - Mỗi container "tưởng" nó là máy riêng trong mạng
- Cần một cơ chế để các container tìm thấy và giao tiếp với nhau
Docker Network chính là cơ chế đó.
💡 Mental model: Mỗi container là một căn hộ với địa chỉ riêng. Docker Network là hệ thống ống nước/dây điện kết nối các căn hộ lại với nhau. Không có network = các căn hộ cô lập hoàn toàn.
Key Concepts
Vấn đề cốt lõi: Tại sao DB_HOST=127.0.0.1 không hoạt động?
Khi chạy Laravel trên máy local (không Docker):
# .env (không Docker)
DB_HOST=127.0.0.1 # MySQL chạy trên cùng máy → OK
DB_PORT=3306
Khi chạy Laravel trong Docker:
# .env (Docker)
DB_HOST=mysql # ← Service name, KHÔNG phải 127.0.0.1
DB_PORT=3306
Tại sao? Vì mỗi container có NET namespace riêng (Bài 1):
127.0.0.1trong container PHP = chính container PHP (loopback của nó)- MySQL chạy trong container khác → có IP khác
- Docker Network cho phép dùng service name (
mysql) thay vì IP — Docker tự resolve tên thành IP
┌─────────────────────────────────────────────────────┐
│ Docker Bridge Network │
│ 172.18.0.0/16 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ php-fpm │ │ nginx │ │ mysql │ │
│ │ 172.18.0.2 │ │ 172.18.0.3 │ │ 172.18.0.4│ │
│ │ │ │ │ │ │ │
│ │ localhost = │ │ localhost = │ │localhost =│ │
│ │ chính nó │ │ chính nó │ │ chính nó │ │
│ └──────┬───────┘ └──────┬───────┘ └─────┬─────┘ │
│ │ │ │ │
│ └────────────┬────┴────────────────┘ │
│ │ │
│ Bridge (docker0 / br-xxx) │
└──────────────────────┬───────────────────────────────┘
│
Host Network
Docker Network Driver — 3 loại cần biết
| Driver | Cơ chế | Container ↔ Container | Container ↔ Host | Use case |
|---|---|---|---|---|
| bridge (mặc định) | Virtual switch nội bộ, mỗi container có IP riêng | ✅ Qua bridge | ✅ Qua port mapping (-p) |
90% trường hợp — multi-container app |
| host | Container dùng trực tiếp network stack của host | ✅ Qua localhost | ✅ Trực tiếp (không cần -p) |
Performance cao, không cần cô lập network |
| none | Không có network — container bị cô lập hoàn toàn | ❌ | ❌ | Security, batch processing không cần network |
⚠️ Với Laravel dev, bạn sẽ dùng bridge gần như 100% thời gian.
hostdùng khi cần benchmark network performance.nonehiếm khi dùng.
How It Works
1. Default Bridge Network
Khi cài Docker, nó tự tạo một network tên bridge (hay docker0):
# Xem danh sách networks
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# a1b2c3d4e5f6 bridge bridge local
# f6e5d4c3b2a1 host host local
# 1a2b3c4d5e6f none null local
Khi chạy docker run không chỉ định network, container tự động vào default bridge:
docker run -d --name web nginx:alpine
docker run -d --name db mysql:8.0 -e MYSQL_ROOT_PASSWORD=secret
# Cả 2 đều ở default bridge → có thể ping nhau bằng IP
docker inspect web --format '.NetworkSettings.IPAddress'
# → 172.17.0.2
docker inspect db --format '.NetworkSettings.IPAddress'
# → 172.17.0.3
# Ping bằng IP: OK
docker exec web ping -c 2 172.17.0.3
# → 64 bytes from 172.17.0.3
# Ping bằng tên: KHÔNG HOẠT ĐỘNG trên default bridge!
docker exec web ping -c 2 db
# → ping: bad address 'db'
2. Custom Bridge Network (recommended)
Custom bridge network giải quyết mọi hạn chế của default bridge:
# Tạo custom network
docker network create my-laravel-net
# Chạy containers trong custom network
docker run -d --name app --network my-laravel-net php:8.2-fpm-alpine
docker run -d --name mysql --network my-laravel-net \
-e MYSQL_ROOT_PASSWORD=secret \
mysql:8.0
docker run -d --name redis --network my-laravel-net redis:7-alpine
# Bây giờ có thể ping bằng TÊN!
docker exec app ping -c 2 mysql
# → 64 bytes from 172.18.0.3 (172.18.0.3): seq=0 ttl=64
docker exec app ping -c 2 redis
# → 64 bytes from 172.18.0.4 (172.18.0.4): seq=0 ttl=64
Tại sao custom bridge tốt hơn?
| Tính năng | Default Bridge | Custom Bridge |
|---|---|---|
| DNS resolution (gọi bằng tên) | ❌ Không | ✅ Có — Docker embedded DNS |
| Tự động kết nối | ✅ Tất cả container vào chung | ❌ Phải chỉ định --network |
| Cô lập giữa các project | ❌ Tất cả container thấy nhau | ✅ Chỉ container cùng network mới thấy nhau |
| Connect/Disconnect runtime | ❌ | ✅ docker network connect/disconnect |
| Production ready | ❌ Chỉ dùng để test nhanh | ✅ Recommended cho mọi project |
3. Docker Embedded DNS — Cơ chế phân giải tên
Khi container gọi mysql (service name), đây là flow xảy ra bên trong:
1. Container app gọi: mysql_connect("mysql", ...)
│
2. OS resolve "mysql" │
▼
3. DNS query đến Docker DNS Server (127.0.0.11)
│
4. Docker DNS kiểm tra: │
"mysql" → container nào│trong cùng network?
│
5. Trả về IP: 172.18.0.3 │
▼
6. Container app kết nối đến 172.18.0.3:3306
Kiểm chứng:
# Xem DNS config bên trong container
docker exec app cat /etc/resolv.conf
# → nameserver 127.0.0.11
# → options ndots:0
# 127.0.0.11 = Docker embedded DNS server
# Nó tự động map container name → IP trong cùng network
4. Port Mapping — Expose service ra host
Container mặc định không expose port ra ngoài host. Để truy cập từ browser hoặc host tools:
# Cú pháp: -p <host_port>:<container_port>
docker run -d --name web -p 8080:80 nginx:alpine
# Bây giờ truy cập http://localhost:8080 → nginx container
Cơ chế bên trong:
Browser → localhost:8080
│
iptables DNAT rule
(Docker tự tạo)
│
▼
Container web:80 (172.18.0.2:80)
Docker sử dụng iptables rules (Linux) để NAT traffic từ host port vào container port.
Các biến thể port mapping:
# Map port cụ thể
-p 8080:80 # host:8080 → container:80
# Map nhiều port
-p 8080:80 -p 8443:443 # host:8080→80, host:8443→443
# Map random host port
-p 80 # host:<random> → container:80
# Chỉ bind localhost (không expose ra mạng ngoài)
-p 127.0.0.1:8080:80 # Chỉ truy cập từ host
# Xem port mapping
docker port web
# 80/tcp → 0.0.0.0:8080
Ví dụ Laravel stack:
Port mapping cần thiết
─────────────────────
Browser → localhost:8000 ───→ Nginx container :80 ← CẦN -p 8000:80
│
▼ (internal, qua network)
PHP-FPM container :9000 ← KHÔNG cần -p
│
▼ (internal, qua network)
MySQL container :3306 ← KHÔNG cần -p *
* Trừ khi bạn muốn kết nối MySQL từ host (Sequel Pro, TablePlus)
→ Lúc đó thêm -p 3306:3306
5. Host Network — Không cô lập
docker run -d --network host --name fast-nginx nginx:alpine
# Container dùng trực tiếp network của host
# Không cần -p, nginx tự listen trên host:80
curl localhost:80
# → nginx welcome page
Cơ chế: Container bỏ qua NET namespace — dùng trực tiếp network stack của host. Giống như chạy nginx trên host vậy.
Khi nào dùng:
- Benchmark network performance (loại bỏ overhead NAT/bridge)
- Container cần truy cập nhiều port trên host
- Monitoring tools cần thấy toàn bộ network traffic
Trade-off:
- ❌ Mất network isolation
- ❌ Port conflict (2 container không thể cùng dùng port 80)
- ❌ Không hoạt động trên Docker Desktop macOS/Windows (chỉ Linux)
6. None Network — Cô lập hoàn toàn
docker run -d --network none --name isolated alpine sleep 3600
# Container không có network interface (ngoài loopback)
docker exec isolated ip addr
# → Chỉ có lo (127.0.0.1), không có eth0
docker exec isolated ping 8.8.8.8
# → ping: sendto: Network unreachable
Khi nào dùng: Batch processing không cần network, chạy code untrusted, security-sensitive workloads.
Deep Dive: Cơ chế Network bên trong
Virtual Ethernet Pair (veth pair)
Mỗi container kết nối vào bridge network thông qua veth pair — một cặp virtual network interface:
Container namespace Host namespace
┌────────────────┐ ┌────────────────────┐
│ │ │ │
│ eth0 │──────────│ vethXXXX │
│ 172.18.0.2 │ (veth │ (connected to │
│ │ pair) │ bridge br-xxx) │
│ │ │ │
└────────────────┘ └────────────────────┘
- eth0 trong container = một đầu của veth pair
- vethXXXX trên host = đầu kia, kết nối vào bridge
- Bridge hoạt động như virtual switch — forward traffic giữa các veth
# Xem veth pairs trên host
ip link show type veth
# Xem bridge interfaces
brctl show # hoặc: ip link show type bridge
Network Namespace chi tiết
Nhắc lại từ Bài 1: NET namespace cho mỗi container:
- IP address riêng (172.18.0.x)
- Routing table riêng
- Port space riêng (container A và B đều có thể listen port 80)
- iptables rules riêng
Điều này giải thích tại sao 3 container đều chạy trên port 9000, 80, 3306 mà không conflict — mỗi cái có port space riêng.
Quản lý Network
Các lệnh cơ bản
# Liệt kê networks
docker network ls
# Tạo custom network
docker network create my-net
# Tạo network với subnet cụ thể
docker network create --subnet=172.20.0.0/16 my-net
# Xem chi tiết network (containers đang kết nối, IP, v.v.)
docker network inspect my-net
# Kết nối container đang chạy vào network
docker network connect my-net existing-container
# Ngắt kết nối
docker network disconnect my-net existing-container
# Xóa network (phải không còn container nào kết nối)
docker network rm my-net
# Dọn networks không dùng
docker network prune
Container kết nối nhiều network
Một container có thể kết nối nhiều network cùng lúc — hữu ích cho architecture phức tạp:
# Tạo 2 networks
docker network create frontend
docker network create backend
# Nginx: chỉ frontend (nhận request từ bên ngoài)
docker run -d --name nginx --network frontend -p 80:80 nginx:alpine
# PHP-FPM: cả frontend + backend (nhận request từ nginx, gọi MySQL)
docker run -d --name app --network frontend php:8.2-fpm-alpine
docker network connect backend app
# MySQL: chỉ backend (không expose ra frontend)
docker run -d --name mysql --network backend \
-e MYSQL_ROOT_PASSWORD=secret mysql:8.0
┌─────────────────────────────────────────┐
│ Frontend Network │
│ │
│ ┌───────────┐ ┌───────────┐ │
│ │ Nginx │─────▶│ PHP-FPM │ │
│ │ :80 │ │ :9000 │ │
│ └───────────┘ └─────┬─────┘ │
│ │ │
└───────────────────────────┼──────────────┘
│ (app kết nối cả 2 networks)
┌───────────────────────────┼──────────────┐
│ Backend Network │ │
│ │ │
│ ┌─────┴─────┐ │
│ │ PHP-FPM │ │
│ │ :9000 │ │
│ └─────┬─────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │ MySQL │ │
│ │ :3306 │ │
│ └───────────┘ │
│ │
└─────────────────────────────────────────┘
→ Nginx KHÔNG thể gọi MySQL (khác network)
→ PHP-FPM gọi được cả Nginx và MySQL (ở cả 2 networks)
Đây là network segmentation — pattern phổ biến trong production để giảm attack surface.
Áp dụng cho Laravel Stack
Kiến trúc network cho Laravel Docker project
┌──────────────────── Docker Bridge Network: laravel-net ───────────────────┐
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Nginx │───▶│ PHP-FPM │───▶│ MySQL │ │ Redis │ │
│ │ :80 │ │ :9000 │ │ :3306 │ │ :6379 │ │
│ │ │ │ │───▶│ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ -p 8000:80 │
│ (expose ra host) │
└──────────────────────────────────────────────────────────────────────────┘
.env Laravel trong Docker
# Tất cả dùng SERVICE NAME, không dùng IP
DB_HOST=mysql # ← service name của MySQL container
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=secret
REDIS_HOST=redis # ← service name của Redis container
REDIS_PORT=6379
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
Preview docker-compose.yml (Tuần 3)
# docker-compose.yml — Docker Compose tự tạo custom bridge network
services:
nginx:
image: nginx:alpine
ports:
- "8000:80" # Chỉ nginx cần expose ra host
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./src:/var/www/html
depends_on:
- app
app:
build: .
volumes:
- ./src:/var/www/html
# KHÔNG cần ports — nginx gọi internal qua app:9000
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: laravel
volumes:
- mysql_data:/var/lib/mysql
# KHÔNG cần ports — app gọi internal qua mysql:3306
# Thêm ports: ["3306:3306"] chỉ khi cần kết nối từ host (TablePlus)
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:
# Docker Compose tự động:
# 1. Tạo network: <project-name>_default (custom bridge)
# 2. Kết nối tất cả services vào network đó
# 3. DNS: mỗi service có thể gọi nhau bằng service name
Nginx config — upstream PHP-FPM
# nginx.conf
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000; # ← service name "app", port 9000
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
fastcgi_pass app:9000 — Nginx dùng Docker DNS resolve app → IP của PHP-FPM container → forward FastCGI request.
Trade-offs & Lưu ý
✅ Best Practices
- Luôn dùng custom bridge network — không dùng default bridge
- Dùng service name cho inter-container communication — không hardcode IP
- Chỉ expose port cần thiết ra host (
-p) — giảm attack surface - Network segmentation cho production — tách frontend/backend network
⚠️ Debugging phổ biến
Lỗi 1: "Connection refused" từ PHP đến MySQL
# Kiểm tra container có cùng network không
docker network inspect my-net
# → Xem phần "Containers" — cả app và mysql phải có mặt
# Kiểm tra DNS resolution
docker exec app ping -c 2 mysql
# Nếu "bad address" → container không cùng custom network
# Kiểm tra MySQL đã ready chưa
docker exec app nc -zv mysql 3306
# → Connection to mysql 3306 port [tcp/mysql] succeeded!
Lỗi 2: Port conflict trên host
docker run -p 3306:3306 mysql:8.0
# Error: port is already allocated
# Fix: dùng port khác
docker run -p 33060:3306 mysql:8.0
# → Kết nối từ host: mysql -h 127.0.0.1 -P 33060
# → Container khác trong cùng network vẫn dùng port 3306
Lỗi 3: Container start trước MySQL ready
MySQL mất vài giây để initialize. PHP container có thể start trước khi MySQL sẵn sàng:
# Kiểm tra MySQL health
docker exec mysql mysqladmin ping -uroot -psecret
# → mysqld is alive
# Giải pháp: dùng healthcheck + depends_on (sẽ học ở Bài 9: Compose nâng cao)
⚠️ Hiệu năng Network
| Network mode | Latency overhead | Throughput | Ghi chú |
|---|---|---|---|
| host | ~0 (native) | Native | Không có NAT overhead |
| bridge (custom) | ~microseconds | Gần native | NAT + bridge forwarding |
| bridge • port mapping | ~microseconds thêm | Gần native | Thêm iptables DNAT |
Trong thực tế, overhead của bridge network không đáng kể cho hầu hết ứng dụng. Chỉ cần quan tâm khi làm benchmark hoặc ứng dụng ultra-low-latency.
Thực hành
Bài 1: Chứng minh Default Bridge không có DNS
# Chạy 2 containers trên default bridge
docker run -d --name ping-a alpine sleep 3600
docker run -d --name ping-b alpine sleep 3600
# Ping bằng IP → OK
IP_B=$(docker inspect -f '.NetworkSettings.IPAddress' ping-b)
docker exec ping-a ping -c 2 $IP_B
# → OK
# Ping bằng tên → FAIL
docker exec ping-a ping -c 2 ping-b
# → ping: bad address 'ping-b'
# Cleanup
docker rm -f ping-a ping-b
Bài 2: Custom Bridge Network với DNS
# Tạo custom network
docker network create test-net
# Chạy 2 containers trên custom network
docker run -d --name ping-a --network test-net alpine sleep 3600
docker run -d --name ping-b --network test-net alpine sleep 3600
# Ping bằng tên → OK!
docker exec ping-a ping -c 2 ping-b
# → 64 bytes from 172.19.0.3: seq=0 ttl=64
# Kiểm tra DNS server
docker exec ping-a cat /etc/resolv.conf
# → nameserver 127.0.0.11
# Cleanup
docker rm -f ping-a ping-b
docker network rm test-net
Bài 3: Nginx + PHP-FPM giao tiếp qua Network
Đây là bài thực hành quan trọng nhất — mô phỏng kiến trúc Laravel:
# 1. Tạo network
docker network create laravel-net
# 2. Tạo folder project
mkdir -p ~/docker-lab/network-test/src/public
# 3. Tạo file PHP test
cat > ~/docker-lab/network-test/src/public/index.php << 'EOF'
<?php
echo "<h1>Hello from PHP-FPM!</h1>";
echo "<p>Hostname: " . gethostname() . "</p>";
echo "<p>Server IP: " . $_SERVER['SERVER_ADDR'] . "</p>";
echo "<p>Time: " . date('H:i:s') . "</p>";
EOF
# 4. Tạo nginx config
cat > ~/docker-lab/network-test/nginx.conf << 'EOF'
server {
listen 80;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
EOF
# 5. Chạy PHP-FPM container
docker run -d --name app \
--network laravel-net \
-v ~/docker-lab/network-test/src:/var/www/html \
php:8.2-fpm-alpine
# 6. Chạy Nginx container
docker run -d --name nginx \
--network laravel-net \
-p 8080:80 \
-v ~/docker-lab/network-test/src:/var/www/html \
-v ~/docker-lab/network-test/nginx.conf:/etc/nginx/conf.d/default.conf \
nginx:alpine
# 7. Test!
curl localhost:8080
# → <h1>Hello from PHP-FPM!</h1>
# → Hostname: <container-id>
# → Server IP: 172.19.0.2
# Hoặc mở browser: http://localhost:8080
Bài 4: Inspect network chi tiết
# Xem tất cả containers trong network
docker network inspect laravel-net
# → "Containers": {
# "app": { "IPv4Address": "172.19.0.2/16" },
# "nginx": { "IPv4Address": "172.19.0.3/16" }
# }
# Xem network config từ phía container
docker exec app ip addr
# → eth0: 172.19.0.2/16
docker exec nginx ip addr
# → eth0: 172.19.0.3/16
# Xem routing table
docker exec app ip route
# → default via 172.19.0.1 dev eth0
# → 172.19.0.0/16 dev eth0
# Verify: nginx gọi app bằng tên
docker exec nginx ping -c 2 app
# → 64 bytes from 172.19.0.2
# Cleanup
docker rm -f app nginx
docker network rm laravel-net
Checklist kiểm tra hiểu bài
- Giải thích được tại sao
DB_HOST=127.0.0.1không hoạt động trong Docker, mà phải dùngDB_HOST=mysql. - Phân biệt được 3 network drivers: bridge, host, none — khi nào dùng cái nào.
- Giải thích được sự khác biệt giữa default bridge và custom bridge network.
- Tạo được custom network, chạy 2+ containers, và chứng minh chúng gọi nhau bằng tên.
- Hiểu port mapping (
-p) — khi nào cần, khi nào không. - Thiết lập được Nginx → PHP-FPM giao tiếp qua Docker network.
- Dùng
docker network inspectđể debug connectivity issues.
Kết nối bài tiếp theo
Bạn đã có đủ 3 mảnh ghép core: Volume (data persist) + Network (giao tiếp) + Dockerfile (build image). Ở Bài 7: Bài tập tổng hợp, bạn sẽ ghép toàn bộ stack Laravel (PHP-FPM + Nginx + MySQL) bằng tay — không dùng Docker Compose — để hiểu sâu Docker Compose làm gì đằng sau.