Môi Trường Docker (Phần 2): Nginx Reverse Proxy — Dynamic Routing Với map

· 9 min read

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ên map
  • 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_name cụ 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:

  1. Thử serve file chính xác ($uri) — CSS, JS, ảnh
  2. Thử serve thư mục ($uri/)
  3. 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.crtself.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.crt và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:

Bình luận