Trong mô hình nhiều website trên Cloud VPS, mọi request đều đi qua một cửa: tầng balancer. Đây là các máy duy nhất có IP public, chạy Nginx reverse proxy để giải mã HTTPS, chặn request rác rồi chuyển phần còn lại về đúng minion trong mạng private. Tài liệu này trình bày cách tổ chức cấu hình trên balancer: một file cho mỗi domain, các header proxy bắt buộc, timeout, giới hạn upload, gzip, chuyển HTTP sang HTTPS, cách đọc lỗi 502, 504, 413 và checklist trước khi đưa một site lên.

1. Nginx reverse proxy làm gì ở tầng balancer

Reverse proxy là máy chủ đứng trước ứng dụng, nhận request thay cho ứng dụng rồi chuyển tiếp vào trong. Trình duyệt chỉ nói chuyện với balancer, không bao giờ thấy minion. Trong mô hình Balancer – Minion – Database, Nginx reverse proxy trên balancer gánh bốn việc:

  1. SSL offload: bắt tay TLS, giữ chứng chỉ, giải mã. Minion phía sau chỉ nhận HTTP thuần qua mạng private.
  2. Định tuyến domain: dựa vào server_name để biết domain nào đi về minion nào.
  3. Chặn sớm: trả 403 cho đường dẫn nhạy cảm, giới hạn tốc độ request trước khi chúng tốn worker PHP.
  4. Nén và chuẩn hoá: gzip, ẩn phiên bản nginx, ép HTTPS.

Khi mỗi domain chỉ trỏ về một minion, Nginx reverse proxy trên balancer là cổng vào và bộ định tuyến, chưa phải bộ cân bằng tải giữa nhiều máy.

2. Cấu trúc thư mục Nginx reverse proxy: một file cho mỗi domain

/etc/nginx/
├── nginx.conf                       # cấu hình chung: worker, gzip, zone rate limit
├── conf.d/
│   ├── example.com.conf             # upstream + server :80 + server :443
│   ├── site-a.example.conf
│   └── ...
└── snippets/
    └── security-blocking.conf       # luật chặn dùng chung, include vào từng site
/etc/letsencrypt/live/<domain>/      # chứng chỉ do certbot cấp

Nguyên tắc: một domain, một file, tên file chính là domain. Thêm site là tạo một file, gỡ site là xoá một file, chuyển site là sửa một dòng. Muốn biết domain nằm trên balancer nào, chỉ cần ls /etc/nginx/conf.d/.

Script có thể gọi minion bằng tên trong DNS nội bộ, nhưng file cấu hình Nginx reverse proxy nên ghi IP private: nginx chỉ phân giải tên trong upstream một lần lúc nạp cấu hình, ghi IP giúp balancer không phụ thuộc DNS khi khởi động.

Một chi tiết hay bị bỏ qua: nginx.conf chỉ nạp conf.d/*.conf. Bản sao lưu nên đặt đuôi như example.com.conf.bak để không bị nạp. Nếu đặt là example.com.bak.conf, nginx sẽ nạp cả hai và báo trùng server_name, hoặc tệ hơn là dùng nhầm bản cũ.

3. Cấu hình chung trong nginx.conf

Phần cấu hình chung quyết định sức chịu tải của cả Nginx reverse proxy và áp dụng cho mọi domain trên balancer. Cấu hình mẫu:

worker_processes auto;            # một worker cho mỗi vCPU
worker_rlimit_nofile 65535;       # mỗi kết nối proxy tốn 2 file descriptor

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    server_tokens off;            # không lộ phiên bản nginx trong header và trang lỗi
    client_max_body_size 64M;     # giới hạn upload mặc định cho mọi site

    gzip on;
    gzip_comp_level 6;
    gzip_proxied any;
    gzip_types text/css application/javascript application/json image/svg+xml text/xml;

    limit_req_zone $binary_remote_addr zone=dynamic:10m rate=25r/s;
    limit_req_status 429;         # mặc định là 503, dễ nhầm với lỗi hệ thống

    include /etc/nginx/conf.d/*.conf;
}
  • worker_processes auto với epoll là lựa chọn chuẩn trên Linux. Trên balancer một vCPU chỉ có một worker, nên tổng số kết nối đồng thời gần bằng worker_connections. Mỗi request proxy tốn hai kết nối (client và minion), nên số người dùng thực tế chỉ khoảng một nửa.
  • gzip_proxied any cho phép Nginx reverse proxy nén cả response từ minion. Không cần thêm JPEG, PNG vào gzip_types vì chúng đã được nén sẵn.

4. File cấu hình một site trên Nginx reverse proxy

upstream example_com {
    server 10.0.0.21:80;
}

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    add_header Strict-Transport-Security "max-age=63072000" always;

    access_log /var/log/nginx/example.com.access;
    error_log  /var/log/nginx/example.com.error;

    include snippets/security-blocking.conf;

    location / {
        limit_req zone=dynamic burst=50;

        proxy_pass http://example_com;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 10s;
        proxy_read_timeout    300s;
    }
}

Bốn header proxy không được thiếu

Khi làm Nginx reverse proxy, nginx tạo một request mới tới minion chứ không chuyển nguyên request gốc. Những thông tin sau phải được gửi kèm một cách tường minh:

  • Host: mặc định nginx gửi tên upstream (example_com) chứ không phải domain. Minion dùng server_name để chọn site, nên thiếu dòng này thì request rơi vào site mặc định của minion.
  • X-Real-IP và X-Forwarded-For: IP thật của người dùng. Thiếu chúng, minion và ứng dụng chỉ thấy IP của balancer, log và mọi giới hạn theo IP ở phía sau đều sai.
  • X-Forwarded-Proto: cho ứng dụng biết request gốc là HTTPS. Laravel cần header này (cùng cấu hình TrustProxies) để sinh link https://.

Timeout: lỗi nên xảy ra ở đâu

Mặc định, Nginx reverse proxy chờ minion trả lời tối đa 60 giây (proxy_read_timeout). Trang quản trị xuất báo cáo hay import file có thể chạy lâu hơn, khi đó có thể nâng lên vài phút. Quy tắc: timeout ở balancer luôn lớn hơn timeout ở minion (fastcgi_read_timeout) và giới hạn thời gian của PHP. Request chậm bị cắt ở tầng gần nguyên nhân nhất, log lỗi nằm ở minion và dễ lần ra script nào chậm. proxy_connect_timeout thì ngược lại, nên ngắn: trong mạng private, vài giây không kết nối được nghĩa là minion có vấn đề, chờ thêm cũng không giúp gì.

HTTP sang HTTPS và HSTS

Khối listen 80 chỉ làm một việc là return 301 sang HTTPS. Header HSTS bảo trình duyệt chỉ dùng HTTPS cho domain này trong khoảng max-age (ví dụ trên là hai năm). Hai lưu ý:

  • Chỉ thêm includeSubDomains khi chắc chắn mọi subdomain đều có HTTPS. Nếu không, subdomain chỉ chạy HTTP sẽ không mở được trên trình duyệt đã ghi nhớ HSTS.
  • add_header chỉ được kế thừa khi khối con không có add_header nào. Nếu trong location có thêm một add_header khác, header HSTS ở cấp server sẽ biến mất khỏi response của location đó.

5. Snippet chặn cơ bản và rate limit

Một snippet dùng chung, include vào mọi site, chặn ngay tại Nginx reverse proxy các đường dẫn không bao giờ hợp lệ với website Laravel:

# /etc/nginx/snippets/security-blocking.conf
location ~ /\.(env|git|svn|htaccess) { deny all; }
location ~* \.(sql|bak|swp)$         { deny all; }
location ~* (wp-login\.php|xmlrpc\.php|phpmyadmin) { return 403; }

Các location dạng regex được nginx ưu tiên hơn location /, nên request /.env bị trả 403 ngay ở balancer, không chạm tới minion. Website Laravel không có wp-login.php, nên mọi request tới đó đều là bot dò lỗ hổng.

Rate limit dùng module ngx_http_limit_req_module. Trong ví dụ, rate=25r/s là tốc độ trung bình cho mỗi IP, burst=50 là số request được xếp hàng khi vượt tốc độ đó; request vượt cả burst nhận mã 429. Rate limit ở đây đếm mọi request, kể cả CSS, JS, ảnh. Trang có vài chục tài nguyên tĩnh có thể chạm ngưỡng nếu burst quá nhỏ, nên hãy thử trên trang nặng nhất trước khi siết.

6. Quy trình sửa cấu hình: luôn nginx -t trước

Mọi thay đổi trên Nginx reverse proxy nên kết thúc bằng đúng một lệnh:

nginx -t && systemctl reload nginx

Không dùng restart trên Nginx reverse proxy khi chỉ sửa cấu hình. reload để master process đọc cấu hình mới, khởi động worker mới và cho worker cũ phục vụ nốt các kết nối đang mở, người dùng không bị ngắt. Quan trọng hơn: nếu file mới lỗi cú pháp, nginx -t dừng ngay và cấu hình cũ vẫn chạy. Một balancer phục vụ hàng chục domain, thiếu một dấu chấm phẩy mà dùng restart là tất cả cùng ngừng.

7. Lỗi thường gặp trên Nginx reverse proxy

Triệu chứngDòng log ở balancerNguyên nhân thường gặp
502 Bad Gateway ngay lập tứcconnect() failed (111: Connection refused) while connecting to upstreamNginx trên minion chết, sai IP upstream, hoặc minion không listen trên IP private
502 sau vài giâyupstream prematurely closed connectionPHP-FPM trên minion chết hoặc worker bị kill giữa chừng
504 Gateway Timeoutupstream timed out (110: Connection timed out)Minion quá tải, query chậm, hoặc minion không phản hồi
413 Request Entity Too Largeclient intended to send too large bodyFile upload lớn hơn client_max_body_size
Trình duyệt báo "too many redirects"Không có lỗi, chỉ thấy chuỗi 301Ứng dụng không biết request là HTTPS nên redirect ngược lại

Cách phân biệt nhanh trên Nginx reverse proxy: 502 là không nói chuyện được với minion, 504 là minion nhận rồi nhưng trả lời quá chậm. Với 502, kiểm tra systemctl status nginx và PHP-FPM trên minion. Với 504, tìm request chậm ở log minion và log ứng dụng.

Lỗi 413 cần sửa ở ba nơi, vì giới hạn nhỏ nhất sẽ thắng: client_max_body_size trên balancer, client_max_body_size trên nginx của minion, và upload_max_filesize, post_max_size trong PHP. Nâng ở Nginx reverse proxy mà quên hai nơi kia, lỗi chỉ đổi tầng chứ không hết.

Vòng redirect hầu như luôn do ứng dụng tưởng request là HTTP. Kiểm tra header X-Forwarded-Proto do Nginx reverse proxy gửi, cấu hình tin proxy của ứng dụng và fastcgi_param HTTPS on trên minion. Nếu domain đi qua Cloudflare với chế độ SSL "Flexible", Cloudflare gọi balancer bằng HTTP, balancer lại redirect sang HTTPS và vòng lặp xảy ra ngay ở tầng ngoài cùng. Chế độ đúng khi balancer đã có chứng chỉ là "Full (strict)".

8. Checklist cấu hình Nginx reverse proxy cho một site mới

  • File đặt tên theo domain trong conf.d/, không để bản sao lưu có đuôi .conf.
  • Upstream ghi IP private của minion; minion chỉ listen trên IP private.
  • Đủ bốn header Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto.
  • Khối port 80 chỉ redirect 301; HSTS chỉ bật includeSubDomains khi chắc chắn.
  • Timeout balancer lớn hơn timeout minion; client_max_body_size khớp với PHP.
  • Đã include snippet chặn dùng chung và zone rate limit.
  • Log riêng cho domain, có logrotate để không đầy ổ đĩa.
  • Chạy nginx -t rồi mới reload, sau đó curl -I https://example.com kiểm tra.

Phần lớn hành vi mặc định gây lỗi khó hiểu (header Host, timeout 60 giây, buffer) đều được mô tả trong tài liệu ngx_http_proxy_module. Cấu hình Nginx reverse proxy không phức tạp; cái khó là giữ cho hàng chục file cấu hình site cùng theo một khuôn, để người trực đọc file nào cũng hiểu ngay nó làm gì, và dựng lại balancer nhanh khi có sự cố.

Bài tiếp theo

Tầng Minion: chạy nhiều phiên bản PHP song song. Cài PHP 7.2, 7.4 và 8.x từ cùng một nguồn gói, socket PHP-FPM theo phiên bản, quy ước thư mục site và supervisor cho queue worker.