Môi Trường Docker (Phần 2): Nginx Reverse Proxy — Dynamic Routing Với map
Ở Phần 1, chúng ta đã thấy kiến trúc tổng quan. Bây giờ hãy zoom vào phần quan trọng nhất: Nginx reverse proxy — cổng vào duy nhất điều hướng mọi request đến đúng container.
Dockerfile Của Proxy
FROM nginx:1.20
RUN apt-get update -y
RUN apt-get install -y vim dnsutils net-tools iputils-ping tcpdump
ADD default.conf /etc/nginx/conf.d/default.conf
ADD ./vhosts/ /etc/nginx/conf.d/
ADD ./ssl/ /etc/nginx/
ADD ./ssl/self* /etc/nginx/ssl/
ADD ./nginx.conf /etc/nginx/
Proxy là một Nginx image chuẩn với:
- Công cụ debug (
dnsutils,tcpdump) để khắc phục sự cố DNS và mạng default.conf— config proxy chính với routing dựa trênmap- Thư mục
vhosts/— server block riêng cho từng project PHP - Chứng chỉ SSL cho HTTPS
Hai Chiến Lược Routing
Proxy sử dụng hai chiến lược khác nhau tùy thuộc loại backend:
Chiến lược 1: map + proxy_pass (cho tools và HTTP services)
Dùng cho các service như Adminer, Mailpit, Ungit — container serve HTTP trực tiếp.
Chiến lược 2: vhosts/*.conf + fastcgi_pass (cho project PHP)
Dùng cho Laravel, WordPress, và các ứng dụng PHP khác — nơi Nginx cần chuyển file .php đến PHP-FPM qua giao thức FastCGI.
Chiến Lược 1: Dynamic Routing Với map
Directive map trong Nginx tạo một bảng tra cứu — ánh xạ biến $host (domain từ request) sang tên container.
default.conf
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
client_max_body_size 3000M;
# ─── Map: domain → tên container ───────────────────
map $host $proxy_target {
adminer.dev.local adminer;
ungit.dev.local ungit;
mailpit.dev.local mailpit;
pgadmin4.dev.local pgadmin4;
mongo.dev.local mongo-express;
minio.dev.local minio;
}
# ─── Map: domain → port ────────────────────────────
map $host $proxy_port {
default 80;
ungit.dev.local 8449;
mongo.dev.local 8081;
minio.dev.local 9000;
}
Cách Hoạt Động Từng Bước
Khi có request đến http://adminer.dev.local:
1. Nginx đọc Host header: "adminer.dev.local"
2. $host = "adminer.dev.local"
3. map $host $proxy_target:
"adminer.dev.local" → $proxy_target = "adminer"
4. map $host $proxy_port:
"adminer.dev.local" → không có trong danh sách → $proxy_port = 80 (mặc định)
5. proxy_pass $scheme://$proxy_target:$proxy_port
→ proxy_pass http://adminer:80
6. Docker DNS phân giải "adminer" → 172.18.0.X
7. Request được chuyển tiếp đến container
Server Block Của Proxy
server {
server_name _; # Catch-all: bắt mọi domain
resolver 127.0.0.11 ipv6=off; # DNS nội bộ của Docker
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass $scheme://$proxy_target:$proxy_port;
proxy_read_timeout 90;
}
}
Chi tiết quan trọng:
server_name _— Catch-all. Bắt mọi domain không cóserver_namecụ thể hơn trong vhosts config.resolver 127.0.0.11— Bảo Nginx sử dụng DNS server tích hợp của Docker. Không có dòng này, Nginx sẽ cố phân giải tên container lúc khởi động và fail nếu container chưa chạy. Với resolver, tên được phân giải tại thời điểm request.proxy_set_header— Các header này đảm bảo backend biết IP gốc của client và protocol. Quan trọng cho app cần tạo URL hoặc kiểm tra HTTPS.
Chiến Lược 2: Virtual Hosts Cho Project PHP
Với project PHP, không dùng được proxy_pass vì PHP-FPM nói FastCGI, không phải HTTP. Mỗi project có file config riêng trong thư mục vhosts/.
Ví dụ: blog.conf
server {
listen 80;
server_name blog.dev.local;
root /var/www/html/blog.md/public;
index index.html index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Chuyển file PHP đến container PHP-FPM
location ~ \.(php|html)$ {
fastcgi_pass php84:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Giải Thích Chi Tiết
root /var/www/html/blog.md/public
Ánh xạ đến httpdocs/blog.md/public/ trên máy host. Cả proxy và container php84 đều mount httpdocs/ tại /var/www/html/, nên đường dẫn này tồn tại trên cả hai container.
try_files $uri $uri/ /index.php?$query_string
Pattern front-controller cổ điển của Laravel:
- Thử serve file chính xác (
$uri) — CSS, JS, ảnh - Thử serve thư mục (
$uri/) - Nếu không tìm thấy, chuyển đến
index.php— router của Laravel xử lý
fastcgi_pass php84:9000
Đây là kết nối cốt lõi. Nginx gửi yêu cầu thực thi PHP đến container php84 trên port 9000 qua giao thức FastCGI. Docker DNS phân giải php84 thành IP nội bộ của container.
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name
Cho PHP-FPM biết file nào cần thực thi. Với request đến /api/users, sau khi try_files viết lại thành /index.php, giá trị này trở thành /var/www/html/blog.md/public/index.php.
Dùng PHP Version Khác Nhau Cho Mỗi Project
Đây là điểm mạnh của kiến trúc. Các vhost config khác nhau có thể trỏ đến container PHP khác nhau:
# blog.conf — App Laravel hiện đại
location ~ \.php$ {
fastcgi_pass php84:9000; # PHP 8.4
}
# legacy.conf — Site WordPress cũ
location ~ \.php$ {
fastcgi_pass php74:9000; # PHP 7.4
}
# ancient.conf — Project rất cũ
location ~ \.php$ {
fastcgi_pass php56:9000; # PHP 5.6
}
Cùng proxy, cùng hạ tầng, khác phiên bản PHP. Không xung đột.
Pha Trộn PHP Version Trong Một Project
Bạn thậm chí có thể route các URL path khác nhau đến PHP version khác nhau trong cùng một project:
server {
server_name abc.dev.local;
root /var/www/html/abc.com/httpdocs/public;
# Phần content cũ → PHP 7.4
location /contents {
try_files $uri $uri/ /contents/index.php?q=$uri&$args;
location ~ \.(php|html)$ {
fastcgi_pass php74:9000;
}
}
# App chính → PHP 8.2
location ~ \.(php|html)$ {
fastcgi_pass php82:9000;
}
}
Thiết Lập SSL Tự Ký
Để có HTTPS local, chúng ta dùng chứng chỉ tự ký.
Tạo Chứng Chỉ
openssl req -x509 -nodes -days 3650 \
-newkey rsa:2048 \
-keyout self.key \
-out self.crt \
-subj "/C=JP/ST=Tokyo/L=Tokyo/O=Dev/CN=*.dev.local"
Đặt self.crt và self.key vào tools/proxy/ssl/.
Server Block HTTPS
server {
listen 443 ssl;
server_name blog.dev.local;
root /var/www/html/blog.md/public;
ssl_certificate /etc/nginx/ssl/self.crt;
ssl_certificate_key /etc/nginx/ssl/self.key;
index index.html index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.(php|html)$ {
fastcgi_pass php84:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Mẹo: Thêm
self.crtvào trusted certificate store của OS/trình duyệt để không bị cảnh báo "Not Secure".
Thêm Project Mới: Hướng Dẫn Từng Bước
Đây là quy trình mỗi khi thêm project mới:
1. Clone/tạo project
cd httpdocs/
git clone git@github.com:your/project.git my-project
2. Tạo vhost config
Copy từ template:
cp tools/proxy/vhosts/template.conf.example tools/proxy/vhosts/my-project.conf
Chỉnh sửa:
server {
listen 80;
server_name myproject.dev.local;
root /var/www/html/my-project/public; # ← Đường dẫn project
index index.html index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.(php|html)$ {
fastcgi_pass php84:9000; # ← Chọn phiên bản PHP
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
3. Thêm DNS
Sửa /etc/hosts:
127.0.0.1 myproject.dev.local
4. Rebuild proxy
make reup n=proxy
5. Xong
Truy cập http://myproject.dev.local trên trình duyệt.
Mẹo Debug
Kiểm tra DNS có phân giải được không bên trong container proxy
make bash n=proxy
nslookup php84
# → Address: 172.18.0.5
Xem error log của Nginx
docker logs proxy --tail 50
Kiểm tra cú pháp Nginx config
docker exec proxy nginx -t
Reload Nginx không cần rebuild
docker exec proxy nginx -s reload
Thứ Tự Load Config Của Nginx
File nginx.conf của proxy include config theo thứ tự:
http {
include /etc/nginx/conf.d/*.conf; # default.conf (map rules)
include /etc/nginx/conf.d/*/*.conf; # Không dùng trong setup này
}
default.conf chứa các block catch-all server_name _. Các file vhosts/*.conf được copy trực tiếp vào /etc/nginx/conf.d/ bởi Dockerfile. Nginx ưu tiên server_name cụ thể nhất — nên server_name blog.dev.local trong vhost file được ưu tiên hơn server_name _ trong default.conf.
Tiếp Theo
Ở Phần 3, chúng ta sẽ thiết lập PHP-FPM container với extensions, cấu hình nhiều phiên bản MySQL, và thêm dev tools như Mailpit cho test email và Adminer cho quản lý database.
Điều hướng Series:
- ← Phần 1: Kiến Trúc
- Phần 2: Nginx Reverse Proxy (Bạn đang ở đây)
- Phần 3: PHP, Database & Dev Tools →