Bài 02
Nginx Reverse Proxy: từ web server đến lá chắn của backend
Bài 02 — Nginx Reverse Proxy: từ web server đến lá chắn của backend
Ở Bài 01 bạn đã dựng được 1 site WordPress trên 1 server. Đẹp. Nhưng có 2 vấn đề:
- Server đó die → site sập. Không có backup.
- Site đông user → 1 server không gánh nổi. Không scale được.
Câu trả lời cho cả 2: đặt 1 lớp nginx ở phía trước, phân tán traffic về nhiều backend. Đây gọi là reverse proxy + load balancing — pattern dùng ở mọi web stack production từ blog cá nhân đến Facebook.
Bài này dạy cách dựng kiến trúc 2-tier (proxy tier + backend tier), những config phải biết để vận hành, và đặc biệt là các pitfall khi đưa WordPress (hoặc bất kỳ app nào) ra sau proxy.
Reverse Proxy là gì
Reverse Proxy đóng vai trò là một trung gian giữa client (người dùng/ứng dụng) và một hoặc nhiều server backend. Cụ thể: thay vì client trực tiếp gửi request đến backend, request sẽ được gửi qua nginx, nginx chuyển tiếp đến backend phù hợp, sau đó nhận phản hồi từ backend và gửi lại cho client.
[Client]
↓
[Reverse Proxy (nginx)] ← chỉ thằng này expose ra internet
↓
[Backend 1] [Backend 2] [Backend 3] ← nằm trong mạng nội bộForward Proxy vs Reverse Proxy — đừng nhầm
Hai từ "proxy" nhưng vai trò ngược nhau:
- Forward Proxy: đứng phía client, đại diện client gửi request ra ngoài internet. Ví dụ: corporate proxy filter traffic nhân viên, VPN, Squid.
- Reverse Proxy: đứng phía server, đại diện server nhận request từ ngoài. Client không biết phía sau có bao nhiêu backend.
Trong bài này nói "proxy" = reverse proxy.
Vì sao cần reverse proxy
5 lý do chính:
- Hide backend — backend không cần expose ra internet, chỉ proxy expose. Tăng security.
- Load balancing — 1 proxy → nhiều backend, chia tải.
- SSL termination — quản lý cert 1 chỗ thay vì mọi backend.
- Caching — cache response để giảm tải backend.
- Rate limiting, WAF, header rewriting — apply policy chung 1 chỗ.
Các tính năng nginx reverse proxy hỗ trợ
- Load Balancing — phân tải nhiều backend.
- Reverse Proxy — base function.
- Caching — lưu cache response.
- SSL/TLS Termination — giải mã HTTPS tại proxy.
- WebSocket Proxying — proxy traffic WebSocket.
- Authentication — basic auth, OAuth (qua module).
- Rate Limiting — giới hạn tốc độ request.
- IP Blocking và Filtering — chặn IP, lọc.
- Custom Headers — thêm/sửa/xoá header.
- Load Balancing Health Checks — kiểm tra sức khỏe backend.
- Logging — ghi log chi tiết.
- Access Control — kiểm soát quyền truy cập.
Mental model — flow 1 request qua reverse proxy
1. Client gửi HTTPS request đến proxy
↓
2. Proxy nhận, decrypt SSL (nếu termination tại proxy)
↓
3. Proxy đọc config → chọn backend (theo algorithm load balancing)
↓
4. Proxy mở connection đến backend, forward request
+ thêm headers: X-Real-IP, X-Forwarded-For, X-Forwarded-Proto
↓
5. Backend xử lý request (chạy PHP, query DB, render HTML)
↓
6. Backend trả response về proxy
↓
7. Proxy có thể cache, modify response, rồi gửi về clientHiểu 7 bước này = debug được mọi vấn đề reverse proxy.
Setup lab — kiến trúc
Trong bài này, dựng kiến trúc:
[Client]
↓ https://test.com
[Nginx Proxy] 192.168.32.100
/ \
[Backend 1] [Backend 2]
192.168.32.133 192.168.32.134
(WordPress) (WordPress)
\ /
\ /
[MariaDB share]
192.168.32.133:3306 ← chạy trên backend 13 server:
- proxy (192.168.32.100) — nginx reverse proxy + load balancer.
- backend-1 (192.168.32.133) — WordPress + MariaDB.
- backend-2 (192.168.32.134) — WordPress, dùng chung database từ backend-1.
⚠️ Production thật, DB tách riêng server và có replication (Bài 03 sẽ làm). Bài này simplify, share DB.
Chuẩn bị backend
Cài thêm 1 backend WordPress giống Bài 01 — nhưng KHÔNG cần cài database, share database từ backend-1 đã có.
Cấu hình backend mới trỏ về database đã setup ở backend-1:
# Trên backend-2, sửa wp-config.php
sudo vim /home/www/test.com/wp-config.phpdefine('DB_NAME', 'wordpress');
define('DB_USER', 'wordpress');
define('DB_PASSWORD', 'MyStrongPass123!');
define('DB_HOST', '192.168.32.133'); ← IP của backend-1 (server có DB)⚠️ Mở port 3306 cho database trên backend-1:
# Trên backend-1
sudo firewall-cmd --permanent --add-port=3306/tcp
sudo firewall-cmd --reloadPlus, MariaDB phải listen trên 0.0.0.0 (không chỉ localhost) — sửa /etc/my.cnf.d/server.cnf:
[mysqld]
bind-address = 0.0.0.0Và user wordpress phải cho phép truy cập từ IP backend-2:
GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'192.168.32.134' IDENTIFIED BY 'MyStrongPass123!';
FLUSH PRIVILEGES;Cài nginx reverse proxy
Trên server mới (192.168.32.100):
sudo dnf install -y nginx
sudo systemctl enable nginx --now
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reloadFile config nginx — thứ tự load
Nginx đọc file config theo thứ tự:
/etc/nginx/nginx.conf— file chính, cóhttp { ... }block.- Bên trong
http { }cóinclude /etc/nginx/conf.d/*.conf;— load mọi file.conftrong thư mục đó.
File config nginx proxy vẫn cấu hình trong /etc/nginx/conf.d/.
Tạo file config:
sudo vim /etc/nginx/conf.d/proxy.confCấu hình nginx reverse proxy
Upstream block là gì
upstream là directive trong nginx dùng để định nghĩa một nhóm các backend server. Sau đó dùng tên upstream này ở proxy_pass để forward request đến nhóm.
upstream backend_servers { # định nghĩa một nhóm server backend
server 192.168.32.133:443;
server 192.168.32.134:443;
}
server {
listen 443 ssl;
server_name test.com;
# Đường dẫn đến chứng chỉ và khóa riêng
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
# Cấu hình SSL
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'HIGH:!aNULL:!MD5';
ssl_prefer_server_ciphers on;
location / {
# Chuyển tiếp yêu cầu tới nhóm backend
proxy_pass https://backend_servers;
# Thiết lập các header cần thiết cho reverse proxy
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;
}
}Headers truyền đến backend — chi tiết từng cái
Khi định tuyến, nginx gửi các header sau đến backend để đảm bảo thông tin client được truyền chính xác:
| Header | Giá trị | Mục đích |
|---|---|---|
Host |
$host |
Truyền tên miền gốc mà client request (vd: test.com). Backend dùng để serve đúng vhost. |
X-Real-IP |
$remote_addr |
IP thực của client (không phải IP proxy). |
X-Forwarded-For |
$proxy_add_x_forwarded_for |
Chuỗi IP qua các proxy trung gian. Nếu request đi qua nhiều proxy, header này tích lũy: client_ip, proxy1_ip, proxy2_ip. |
X-Forwarded-Proto |
$scheme |
Giao thức gốc (HTTP hoặc HTTPS) mà client dùng để kết nối proxy. Cực quan trọng với WordPress (xem phần dưới). |
Connection |
keep-alive |
Duy trì kết nối backend (xem phần keepalive). |
⚠️ X-Real-IP và X-Forwarded-For chỉ đáng tin nếu set bởi proxy bạn kiểm soát. Client có thể giả mạo 2 header này khi gửi trực tiếp. Backend nên trust IP từ X-Real-IP chỉ khi request đến từ IP của proxy.
Test load balancing
Lúc này chỉ cần truy cập qua IP của nginx proxy (192.168.32.100). Nginx sẽ định tuyến tới một trong hai backend.
Sửa file /etc/hosts máy client để truy cập qua domain:
192.168.32.100 test.comVào https://test.com → WordPress hiển thị (từ một trong 2 backend).
Log để xem load balancing có hoạt động không
Thêm log format trong nginx.conf hoặc đầu file proxy.conf (ngoài server block):
log_format upstreamlog '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'to: $upstream_addr';Trong server block, dùng log format này:
access_log /var/log/nginx/access.log upstreamlog;Xem log:
sudo tail -f /var/log/nginx/access.logLog access vào server nào sẽ có IP của backend ở cuối dòng (to: 192.168.32.133:443). Refresh trang vài lần → thấy IP backend luân phiên = load balancing chạy.
Các thuật toán load balancing
Nginx hỗ trợ nhiều algorithm phân tải. 4 cái chính bạn cần biết:
1. Round Robin (mặc định)
Round Robin là thuật toán phân phối request tuần tự lần lượt đến từng server trong nhóm backend. Request 1 → server 1, request 2 → server 2, request 3 → server 1, ...
- Ưu: đơn giản, fair, phù hợp khi backend có cấu hình và hiệu suất tương đương.
- Đây là thuật toán mặc định — không cấu hình gì thì nginx tự dùng cái này.
upstream backend_servers {
server 192.168.32.133;
server 192.168.32.134;
}2. Least Connections
Least Connections phân phối request đến server đang có số lượng kết nối hiện tại ít nhất.
- Thích hợp cho ứng dụng có thời gian xử lý không đều (vd: một số request mất 100ms, một số mất 5s).
- Tránh trường hợp 1 server bị stuck với connection lâu trong khi server khác rảnh.
upstream backend_servers {
least_conn;
server 192.168.32.133;
server 192.168.32.134;
}3. IP Hash
IP Hash dựa trên địa chỉ IP của client — nginx luôn chuyển tiếp request từ cùng một client đến cùng một backend.
- Phù hợp cho ứng dụng yêu cầu duy trì trạng thái (stateful) — vd: app dùng PHP session lưu trong filesystem của backend.
- Còn gọi là sticky session kiểu nhẹ.
upstream backend_servers {
ip_hash;
server 192.168.32.133;
server 192.168.32.134;
}⚠️ IP Hash có vấn đề:
- User cùng văn phòng (cùng IP NAT) → tất cả vào 1 backend → mất cân bằng.
- User di động đổi IP → mất session.
→ Production hiện đại không khuyến khích ip_hash. Thay vào đó:
- Cookie-based sticky (nginx Plus).
- App stateless + state ra Redis/DB (recommend).
4. Weighted Round Robin
Weighted Round Robin là biến thể của Round Robin, mỗi server được gán một trọng số (weight) quyết định số lượng request server đó xử lý. Server có weight cao hơn nhận nhiều request hơn.
- Thích hợp khi server có cấu hình tài nguyên khác nhau (vd: 1 server 16GB RAM, 1 server 8GB RAM).
upstream backend_servers {
server 192.168.32.133 weight=3; # Server mạnh hơn, xử lý 3 phần
server 192.168.32.134 weight=1; # Server yếu hơn, xử lý 1 phần
}Tỉ lệ 3:1 → 3/4 traffic vào server đầu, 1/4 vào server sau.
Các thuật toán khác
- Least Time (Nginx Plus only) — request đến server có response time thấp nhất.
- Random (Nginx Plus) — phân phối ngẫu nhiên.
- Generic Hash — hash theo bất kỳ variable nào (URL, header...).
Khi nào chọn thuật toán nào
| Algorithm | Khi nào dùng |
|---|---|
| Round Robin | Backend đồng nhất về cấu hình và hiệu suất (default). |
| Least Connections | Backend xử lý request có thời gian không đồng đều. |
| IP Hash | App yêu cầu duy trì phiên với cùng một server (legacy stateful). |
| Weighted Round Robin | Backend có cấu hình tài nguyên khác nhau. |
| Least Time (Plus) | Yêu cầu tối ưu thời gian phản hồi. |
| Random | Test hoặc cân bằng ngẫu nhiên. |
Health check — phát hiện backend chết
Backend chết, nginx phải biết để không forward request đến nó nữa, nếu không user sẽ thấy 502 sporadically.
Passive health check (open source default)
Passive health check = nginx phát hiện backend chết qua chính traffic thật. Nếu request đến backend fail (timeout, connect refused, 5xx), nginx đánh dấu backend đó "tạm down" trong một khoảng thời gian.
Cấu hình qua 2 tham số trong upstream:
upstream backend_servers {
server 192.168.32.133 max_fails=3 fail_timeout=30s;
server 192.168.32.134 max_fails=3 fail_timeout=30s;
}Giải thích:
max_fails=3— số lần fail liên tiếp trước khi nginx coi backend là DOWN.fail_timeout=30s— sau khi mark DOWN, đợi 30s rồi thử lại. Nếu request mới thành công → mark UP lại.
Logic: 3 lần fail trong 30s → DOWN → đợi 30s → thử lại.
Active health check (Nginx Plus only)
Active health check = nginx chủ động ping endpoint /health của backend mỗi N giây.
# CHỈ NGINX PLUS
upstream backend_servers {
zone backends 64k;
server 192.168.32.133;
server 192.168.32.134;
}
server {
location / {
proxy_pass http://backend_servers;
health_check interval=5s fails=3 passes=2 uri=/health;
}
}Open source nginx không có health_check directive. Workaround:
- Dùng passive như trên + monitoring riêng (Prometheus blackbox-exporter ping backend).
- Hoặc dùng module bên ngoài như
nginx_upstream_check_module.
Backup server — fallback
Khi muốn 1 server chỉ chạy khi tất cả backend khác down:
upstream backend_servers {
server 192.168.32.133;
server 192.168.32.134;
server 192.168.32.135 backup; # chỉ active khi 133 và 134 đều down
}Use case: 1 server cấu hình thấp để cover khi disaster.
Keepalive upstream — tối ưu hiệu năng
Mỗi request đến backend mặc định mở connection mới rồi đóng → tốn TCP handshake, tốn CPU.
keepalive directive cho phép nginx reuse connection đến backend:
upstream backend_servers {
server 192.168.32.133;
server 192.168.32.134;
keepalive 32; # giữ 32 connection idle với mỗi worker process
}
server {
location / {
proxy_pass http://backend_servers;
proxy_http_version 1.1; # bắt buộc cho keepalive
proxy_set_header Connection ""; # xoá Connection header (default là close)
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;
}
}2 dòng quan trọng:
proxy_http_version 1.1— HTTP/1.1 hỗ trợ keepalive, HTTP/1.0 không.proxy_set_header Connection ""— xóa headerConnection: closemặc định.
Quên 2 dòng này thì keepalive 32 vô tác dụng. Production: bật keepalive ↔ backend giảm latency rõ rệt.
Timeout và buffer — biết để debug
Timeout
3 timeout quan trọng giữa proxy ↔ backend:
location / {
proxy_pass http://backend_servers;
proxy_connect_timeout 5s; # thời gian chờ nginx mở connection đến backend
proxy_send_timeout 60s; # thời gian nginx gửi request đến backend
proxy_read_timeout 60s; # thời gian chờ backend trả response
}proxy_connect_timeout: backend chết → nginx fail nhanh, không treo.proxy_read_timeout: backend slow (vd: query DB lâu) → nginx return 504 sau timeout này.
Default 60s — nhiều app cần raise lên với background job, nhưng raise quá cao thì connection treo lâu.
Buffer
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 8 16k;proxy_buffering on(default) — nginx đọc full response từ backend rồi mới trả client. Backend "free" sớm, không phải chờ client chậm.- Tắt buffering (
proxy_buffering off) khi cần real-time streaming (Server-Sent Events, WebSocket).
Upload file lớn: cần raise client_max_body_size ở server block, không thì 413 Request Entity Too Large:
server {
client_max_body_size 100M;
# ...
}SSL Termination — 3 pattern phổ biến
Đây là phần base về vận hành cần hiểu rõ — chọn sai pattern, backend bị tải nặng SSL không cần thiết hoặc security policy không đúng.
Pattern 1: Terminate ở proxy, HTTP backend (phổ biến nhất)
Client --HTTPS--> Proxy --HTTP--> Backend- Proxy decrypt SSL, gửi HTTP plain đến backend.
- Backend KHÔNG cần cert, nhẹ tải.
- Cert chỉ quản lý 1 chỗ (proxy).
- Bắt buộc: backend phải nằm trong mạng nội bộ (private network) — không expose HTTP ra internet.
# Proxy config
upstream backend {
server 192.168.32.133:80; # HTTP port
server 192.168.32.134:80;
}
server {
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
# ...
location / {
proxy_pass http://backend; # http, không https
# ...
}
}Recommend cho 90% trường hợp.
Pattern 2: Re-encrypt — SSL cả proxy ↔ backend
Client --HTTPS--> Proxy --HTTPS--> Backend- Proxy decrypt, re-encrypt trước khi gửi backend.
- Backend CẦN cert.
- Use case: zero-trust network, compliance yêu cầu encryption end-to-end (PCI-DSS, HIPAA).
upstream backend {
server 192.168.32.133:443;
server 192.168.32.134:443;
}
server {
listen 443 ssl;
location / {
proxy_pass https://backend; # https
proxy_ssl_verify off; # nếu backend dùng self-signed
}
}Tài liệu của bài này dùng pattern này (mọi server đều có SSL).
Pattern 3: SSL Passthrough (Layer 4)
Client --HTTPS--> Proxy (không decrypt) --HTTPS--> Backend- Proxy không decrypt, chỉ forward TCP gói nguyên trạng.
- Backend handle SSL.
- Proxy không nhìn được HTTP content → không route theo URL, không cache, không header rewrite.
- Use case: mTLS, hoặc khi không muốn proxy thấy data.
Dùng stream module (Layer 4):
stream {
upstream backend {
server 192.168.32.133:443;
server 192.168.32.134:443;
}
server {
listen 443;
proxy_pass backend;
}
}WordPress sau reverse proxy — 3 vấn đề kinh điển
Đây là phần mọi DevOps đưa WordPress ra proxy đều gặp. Hiểu trước để không phải đau khổ debug.
Vấn đề 1: Mixed content — CSS/JS load qua HTTP trong trang HTTPS
Triệu chứng: Vào https://test.com → trang load nhưng CSS không apply, browser console báo:
Mixed Content: The page at 'https://test.com/' was loaded over HTTPS,
but requested an insecure resource 'http://test.com/wp-content/style.css'.Nguyên nhân: WordPress backend nhận request HTTP (vì proxy → backend qua HTTP). WordPress sinh URL với scheme HTTP. Trang HTML chứa <link href="http://..."> → browser block.
Fix — sửa wp-config.php của backend, thêm trước dòng require_once:
// Detect HTTPS từ proxy header
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}$_SERVER['HTTPS'] là biến WordPress check để biết request là HTTPS hay không. Set thủ công từ X-Forwarded-Proto mà proxy đã forward.
Vấn đề 2: Redirect loop khi vào /wp-admin
Triệu chứng: Login admin → redirect loop, browser báo "ERR_TOO_MANY_REDIRECTS".
Nguyên nhân: WordPress option siteurl và home trong database vẫn lưu http:// (từ lúc cài), nhưng request đến qua HTTPS. WordPress thấy mismatch → redirect → vòng lặp.
Fix — vào wp-config.php, ép URL:
define('WP_HOME', 'https://test.com');
define('WP_SITEURL', 'https://test.com');Hoặc update trực tiếp DB:
UPDATE wp_options SET option_value = 'https://test.com' WHERE option_name IN ('siteurl', 'home');Vấn đề 3: WordPress log IP của proxy, không phải IP user
Triệu chứng: Plugin security (Wordfence, Limit Login Attempts) ban toàn bộ user vì thấy mọi request đến từ IP của proxy.
Nguyên nhân: Backend nhận request từ proxy → $_SERVER['REMOTE_ADDR'] = IP proxy, không phải client thật.
Fix — sửa wp-config.php, lấy IP từ X-Real-IP hoặc X-Forwarded-For:
if (isset($_SERVER['HTTP_X_REAL_IP'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_REAL_IP'];
}⚠️ Chỉ làm điều này khi backend chỉ nhận traffic từ proxy (firewall block external). Không thì hacker có thể fake X-Real-IP header để bypass IP-based security.
Luồng hoạt động chi tiết
Bước 1: Client gửi yêu cầu đến Nginx
- User truy cập website hoặc API qua URL (vd:
https://test.com). - Yêu cầu HTTP/HTTPS được gửi đến máy chủ Nginx proxy.
Bước 2: Nginx xử lý yêu cầu
- Nginx nhận yêu cầu và kiểm tra file cấu hình (
nginx.confhoặc các file site-specific trongconf.d/). - Nginx sử dụng module
proxy_passđể quyết định chuyển tiếp yêu cầu đến backend nào.
Bước 3: Định tuyến tới Backend
Dựa trên các quy tắc định tuyến, Nginx chọn backend phù hợp:
- Load Balancing: phân phối yêu cầu giữa các backend theo thuật toán (mặc định round-robin).
- Sticky Session: đảm bảo yêu cầu của cùng một client luôn được xử lý bởi cùng một backend.
- Health Check: nếu một backend không khả dụng, Nginx tự động chuyển sang backend khác.
Nginx chuyển tiếp yêu cầu đến backend qua HTTP hoặc HTTPS.
Bước 4: Backend xử lý yêu cầu
- Backend nhận request từ Nginx, xử lý (vd: truy vấn database, trả về nội dung tĩnh hoặc động).
- Backend gửi response HTTP trở lại Nginx.
Bước 5: Nginx gửi phản hồi tới Client
- Nginx nhận response từ backend và chuyển tiếp đến client.
- Nếu cần, Nginx thực hiện thêm các bước như nén dữ liệu (
gzip) hoặc caching trước khi trả response.
Giao thức định tuyến tới backend
Trong bài này dùng proxy_pass để định tuyến qua HTTP/HTTPS — hoạt động ở Layer 7 (Application Layer của OSI).
Nginx hỗ trợ nhiều giao thức khác nhau cho các ứng dụng phù hợp:
| Giao thức | Câu lệnh Nginx | Ứng dụng backend |
|---|---|---|
| HTTP | proxy_pass |
Web server, ứng dụng HTTP |
| HTTPS | proxy_pass |
Web server, ứng dụng HTTPS |
| FastCGI | fastcgi_pass |
PHP-FPM, Python qua FastCGI |
| uWSGI | uwsgi_pass |
Python ứng dụng qua uWSGI |
| SCGI | scgi_pass |
Backend sử dụng giao thức SCGI |
| gRPC | grpc_pass |
Microservices hoặc ứng dụng gRPC |
| Unix | proxy_pass |
Bất kỳ backend nào qua Unix socket |
Layer 4 vs Layer 7 proxy
Nginx có thể proxy ở 2 layer khác nhau:
Layer 7 (HTTP-aware) — dùng trong block http { ... }:
- Hiểu HTTP — đọc URL, header, method.
- Có thể route theo path, host, header.
- Cache, rewrite, compress.
- Use case: web app, API.
Layer 4 (TCP/UDP) — dùng trong block stream { ... }:
- Chỉ thấy TCP/UDP, không hiểu HTTP.
- Forward gói nguyên trạng.
- Cách duy nhất proxy database (MySQL, Postgres), MQTT, SMTP.
# Trong /etc/nginx/nginx.conf, NGOÀI block http
stream {
upstream mysql_backend {
server 192.168.32.133:3306;
server 192.168.32.134:3306;
}
server {
listen 3306;
proxy_pass mysql_backend;
}
}Một server chứa nhiều website
Nếu một server proxy chứa nhiều website, chỉ cần định tuyến request của user tới từng domain riêng — mỗi domain 1 server block:
# /etc/nginx/conf.d/site1.conf
upstream site1_backend {
server 10.0.0.10;
}
server {
listen 443 ssl;
server_name site1.com;
ssl_certificate /etc/nginx/ssl/site1.crt;
ssl_certificate_key /etc/nginx/ssl/site1.key;
location / {
proxy_pass http://site1_backend;
}
}
# /etc/nginx/conf.d/site2.conf
upstream site2_backend {
server 10.0.0.20;
}
server {
listen 443 ssl;
server_name site2.com;
ssl_certificate /etc/nginx/ssl/site2.crt;
ssl_certificate_key /etc/nginx/ssl/site2.key;
location / {
proxy_pass http://site2_backend;
}
}Nginx dùng server_name để phân biệt domain → forward đến đúng backend pool.
Tối ưu hóa cấu hình
Một số tình huống user gọi tới nginx proxy và nginx proxy đã xử lý SSL và domain name — vì vậy trong cấu hình của backend không cần cấu hình SSL và domain name.
Tuy nhiên với WordPress có thể cần cấu hình chính xác domain trong database hoặc trong file cấu hình (wp-config.php). Vì vậy ở backend vẫn cần cấu hình domain name.
SSL — quyết định pattern nào
SSL trong trường hợp này có thể dùng cho cả 2 việc:
- Nginx proxy ↔ Client: mã hóa với client — cần thiết, không thể bỏ.
- Nginx proxy ↔ Backend: có thể chỉ cần HTTP nếu đã nằm trong cùng một mạng bảo mật → backend không cần cấu hình SSL.
- Nếu dùng
proxy_pass https://tới backend → backend cũng cần cấu hình SSL để chấp nhận kết nối HTTPS.
Khi nào backend vẫn cần biết domain
Mặc dù nginx đã xử lý SSL và domain, nhưng trong một số tình huống backend vẫn cần thông tin về domain:
- Ứng dụng yêu cầu xác định domain để cấu hình.
- Tạo các link tuyệt đối (vd: WordPress sinh URL absolute trong email, RSS feed).
- Multi-tenant app — phân biệt user theo domain.
Pass header Host qua proxy_set_header Host $host; để backend biết domain client request.
Câu chuyện thực tế
Một lần mình deploy WordPress sau nginx proxy theo đúng tutorial. Setup xong, vào https://test.com → load page OK. Yên tâm bàn giao cho khách.
Chiều, khách báo:
"Em ơi, trang trông như năm 1998. Không có CSS gì cả."
Mở DevTools, console đỏ:
Mixed Content: The page at 'https://test.com/' was loaded over HTTPS,
but requested an insecure resource 'http://test.com/wp-content/themes/twentytwentyfour/style.css'.Lý do: proxy nhận HTTPS, forward HTTP đến backend (pattern SSL termination tại proxy). Backend WordPress nhận request HTTP → sinh URL CSS với http://. Browser thấy trang HTTPS load resource HTTP → block.
Fix: thêm vào wp-config.php của cả 2 backend:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}Reload — CSS load đẹp. Demo lại với khách, OK.
Hôm sau khách gọi tiếp:
"Em ơi, anh login admin không được. Cứ loop trang."
Lý do mới: option siteurl và home trong database lưu http://test.com (từ lúc cài WordPress qua HTTP). User request HTTPS → WordPress redirect về http → proxy lại redirect HTTPS → loop.
Fix nữa:
define('WP_HOME', 'https://test.com');
define('WP_SITEURL', 'https://test.com');Lần này yên tâm thật. Tuần sau khách báo plugin Wordfence ban toàn bộ user vì thấy "DDoS từ 1 IP" — IP của proxy. Fix tiếp:
if (isset($_SERVER['HTTP_X_REAL_IP'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_REAL_IP'];
}3 vấn đề, 3 lần khách gọi, 3 lần fix. Tất cả đều là vấn đề kinh điển của WordPress sau proxy mà tutorial cơ bản không nhắc đến.
Bài học:
- Khi đưa app ra sau proxy, app phải biết nó đứng sau proxy. WordPress, Laravel, Django, Rails — đều có config để trust proxy header.
- 3 header sống còn:
Host,X-Real-IP,X-Forwarded-Proto. Quên 1 cái → 1 class bug. - Test HTTPS từ client thật, không tin tutorial. Curl OK ≠ browser OK.
Lab — chạy từ đầu đến cuối
Setup 3 server:
- proxy (192.168.32.100)
- backend-1 (192.168.32.133) — đã có WordPress + DB từ Bài 01
- backend-2 (192.168.32.134) — clone WordPress, share DB
Trên backend-1: mở DB cho backend-2
# Cho MariaDB listen mọi interface
sudo sed -i 's/^bind-address.*/bind-address = 0.0.0.0/' /etc/my.cnf.d/server.cnf
# Mở firewall
sudo firewall-cmd --permanent --add-port=3306/tcp
sudo firewall-cmd --reload
sudo systemctl restart mariadb
# Grant cho user wordpress từ backend-2
mariadb -u root -p <<EOF
GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'192.168.32.134' IDENTIFIED BY 'MyStrongPass123!';
FLUSH PRIVILEGES;
EOFTrên backend-2: cài LEMP nhưng skip MariaDB, dùng DB của backend-1
# Tắt SELinux
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
# Cài nginx + PHP-FPM (skip MariaDB)
sudo dnf install -y nginx epel-release
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
sudo dnf module enable php:remi-8.2 -y
sudo dnf install -y php-fpm php-cli php-mysqlnd php-gd php-mbstring php-xml php-zip
sudo systemctl enable nginx php-fpm --now
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
# Sửa user PHP-FPM
sudo sed -i 's/^user = apache/user = nginx/' /etc/php-fpm.d/www.conf
sudo sed -i 's/^group = apache/group = nginx/' /etc/php-fpm.d/www.conf
sudo systemctl restart php-fpm
# Copy SSL cert từ backend-1 (hoặc tạo mới)
sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -newkey rsa:4096 -nodes \
-keyout /etc/nginx/ssl/nginx.key \
-out /etc/nginx/ssl/nginx.crt \
-days 365 -subj "/CN=test.com"
# Vhost (same as backend-1)
sudo mkdir -p /home/www/test.com
sudo tee /etc/nginx/conf.d/test.com.conf <<'EOF'
server {
listen 443 ssl;
server_name test.com;
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
root /home/www/test.com;
index index.php index.html;
location / { try_files $uri $uri/ /index.php?$args; }
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php-fpm/www.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
EOF
# Copy WordPress từ backend-1 (rsync hoặc download tải mới)
cd /home/www/test.com
sudo curl -O https://wordpress.org/latest.tar.gz
sudo tar -xvf latest.tar.gz
sudo mv wordpress/* .
sudo rm -rf wordpress latest.tar.gz
# wp-config.php trỏ DB về backend-1
sudo cp wp-config-sample.php wp-config.php
sudo sed -i "s/database_name_here/wordpress/" wp-config.php
sudo sed -i "s/username_here/wordpress/" wp-config.php
sudo sed -i "s/password_here/MyStrongPass123!/" wp-config.php
sudo sed -i "s/localhost/192.168.32.133/" wp-config.php
# Fix WordPress for proxy
sudo tee -a wp-config.php <<'EOF'
// Reverse proxy support
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
if (isset($_SERVER['HTTP_X_REAL_IP'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_REAL_IP'];
}
define('WP_HOME', 'https://test.com');
define('WP_SITEURL', 'https://test.com');
EOF
sudo chown -R nginx:nginx /home/www/test.com
sudo systemctl reload nginx⚠️ Cũng cần thêm config reverse proxy support vào wp-config.php của backend-1 (cùng đoạn code).
Trên proxy (192.168.32.100): cài nginx proxy
sudo setenforce 0
sudo dnf install -y nginx
sudo systemctl enable nginx --now
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
# SSL cert
sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -newkey rsa:4096 -nodes \
-keyout /etc/nginx/ssl/nginx.key \
-out /etc/nginx/ssl/nginx.crt \
-days 365 -subj "/CN=test.com"
# Proxy config
sudo tee /etc/nginx/conf.d/proxy.conf <<'EOF'
log_format upstreamlog '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'to: $upstream_addr';
upstream backend_servers {
server 192.168.32.133:443 max_fails=3 fail_timeout=30s;
server 192.168.32.134:443 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 443 ssl;
server_name test.com;
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
ssl_protocols TLSv1.2 TLSv1.3;
access_log /var/log/nginx/access.log upstreamlog;
error_log /var/log/nginx/error.log warn;
client_max_body_size 100M;
location / {
proxy_pass https://backend_servers;
proxy_ssl_verify off;
proxy_http_version 1.1;
proxy_set_header Connection "";
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 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
server {
listen 80;
server_name test.com;
return 301 https://$host$request_uri;
}
EOF
sudo nginx -t
sudo systemctl reload nginxTest
Trên máy client, thêm vào /etc/hosts:
192.168.32.100 test.comVào https://test.com → WordPress hiển thị.
Refresh nhiều lần, kiểm tra log proxy:
sudo tail -f /var/log/nginx/access.logThấy to: 192.168.32.133:443 và to: 192.168.32.134:443 luân phiên → load balancing chạy.
Test failover: shutdown backend-1, refresh trang nhiều lần. Sau 3 lần fail (max_fails=3), nginx mark backend-1 DOWN, mọi request đi vào backend-2.
Pitfalls
Quên
proxy_http_version 1.1vàproxy_set_header Connection ""→ keepalive upstream không hoạt động, mỗi request mở connection mới, latency cao.proxy_set_header Host $hostthiếu → backend không biết domain client request, vhost match sai, có thể trả default site.Quên
X-Forwarded-Proto→ app backend tưởng request là HTTP → WordPress mixed content, redirect loop.IP Hash dùng cho NAT environment → tất cả user cùng văn phòng (cùng IP NAT) vào 1 backend → mất cân bằng nghiêm trọng.
TCP health check thay HTTP — backend port mở nhưng app crash → nginx vẫn forward → user 502. Tài liệu open source nginx không có HTTP health check, phải dùng monitoring riêng hoặc nginx Plus.
max_fails=3 fail_timeout=10squá ngắn trên backend slow → false positive, mark DOWN nhầm.client_max_body_sizemặc định 1MB → user upload file > 1MB nhận 413 Request Entity Too Large. Raise lên ở cả proxy và backend.Không log
$upstream_addr→ không biết request đi vào backend nào, debug load balancing mò kim đáy bể.proxy_ssl_verify onvới self-signed backend cert → 502 Bad Gateway, log "certificate verify failed". Setoffcho lab, production phải có CA chain đúng.WordPress
siteurllưu HTTP, request đến HTTPS → redirect loop. DefineWP_HOMEvàWP_SITEURLtrongwp-config.php.Backend log mọi request từ IP của proxy → security plugin ban nhầm. Cần truyền
X-Real-IPvà config backend trust.Mở 3306 ra public mà không hạn chế IP → ai cũng có thể connect MySQL. Phải
firewall-cmd --add-rich-rulechỉ cho IP backend cụ thể.
Tóm tắt
- Reverse Proxy = trung gian client ↔ backend. Hide backend, load balance, SSL termination, cache, rate limit.
- Mental model 7 bước: client → proxy decrypt → chọn backend → forward (kèm headers) → backend xử lý → response → proxy trả client.
upstreamblock định nghĩa pool backend,proxy_passforward đến pool.- 4 algorithm cốt lõi: Round Robin (default), Least Connections (request không đồng đều), IP Hash (sticky), Weighted (server khác cấu hình).
- Health check open source: passive qua
max_fails+fail_timeout. Active health check là Plus only. - Keepalive upstream:
keepalive 32+proxy_http_version 1.1+proxy_set_header Connection "". - 3 headers sống còn:
Host,X-Real-IP,X-Forwarded-Proto. Quên cái nào → 1 class bug. - SSL Termination 3 pattern: terminate at proxy + HTTP backend (default), re-encrypt (compliance), passthrough (Layer 4, mTLS).
- WordPress sau proxy = 3 vấn đề kinh điển: mixed content, redirect loop, IP log sai. Sửa trong
wp-config.php. - Layer 7 cho web, Layer 4 (
streamblock) cho DB/MQTT/SMTP.
Bài tiếp: 03 — High Availability: Keepalived VRRP + MySQL Replication. Khi 1 server die, traffic không cảm nhận.