Cấu hình Nginx làm Reverse Proxy và Load Balancer cho ứng dụng Web Node.js
Trong môi trường sản xuất hiện đại, việc phục vụ ứng dụng web trực tiếp từ server chạy ứng dụng (như Node.js, Python Flask, hay Java Spring) thường không được khuyến khích do các vấn đề về bảo mật, hiệu năng và khả năng mở rộng. Apache và Nginx đóng vai trò là những cánh cổng bảo vệ và điều phối lưu lượng truy cập quan trọng. Bài viết này sẽ tập trung vào một kịch bản thực tế và rất phổ biến: sử dụng Nginx làm Reverse Proxy để định tuyến traffic vào các instance Node.js, đồng thời áp dụng thuật toán Load Balancing (cân bằng tải) để phân phối yêu cầu đến nhiều server backend giúp hệ thống chịu tải cao và sẵn sàng 24/7.
Kịch bản giả định là chúng ta có một ứng dụng Node.js chạy trên 3 port khác nhau (8080, 8081, 8082) trên cùng một máy chủ, và Nginx sẽ đứng ở cổng 80 để tiếp nhận yêu cầu từ người dùng, sau đó phân phối đều cho 3 port này. Đây là cách đơn giản nhất để hiểu nguyên lý cân bằng tải trước khi triển khai trên nhiều máy vật lý riêng biệt.
Lợi ích của việc sử dụng Nginx làm Reverse Proxy
Trước khi đi vào các bước cài đặt, chúng ta cần hiểu rõ lý do tại sao phải thêm một lớp Nginx vào giữa client và server ứng dụng. Khi Nginx hoạt động như một Reverse Proxy, nó che giấu cấu trúc thực sự của server backend khỏi mắt người dùng và kẻ tấn công. Nếu server Node.js gặp sự cố hay bị tấn công trực tiếp, Nginx vẫn có thể trả về trang lỗi tùy chỉnh hoặc tiếp tục phục vụ từ các node khác.
Quan trọng hơn, Nginx được tối ưu hóa để xử lý các kết nối đồng thời (concurrent connections) rất hiệu quả. Nó có thể nén dữ liệu (gzip), phục vụ file tĩnh (ảnh, CSS, JS) cực nhanh để giảm tải cho ứng dụng xử lý logic kinh doanh. Ngoài ra, tính năng Load Balancing cho phép bạn dễ dàng mở rộng hệ thống chỉ bằng cách thêm các instance mới vào nhóm backend mà không cần thay đổi cấu hình bên ngoài hay thông báo cho người dùng.
Chuẩn bị môi trường và cài đặt cơ bản
Để thực hiện hướng dẫn này, chúng ta cần một máy chủ Linux (Ubuntu 20.04 hoặc 22.04 là lựa chọn phổ biến nhất). Trước tiên, bạn cần cập nhật hệ thống và cài đặt Nginx. Nếu bạn chưa có Nginx, hãy chạy lệnh sau để cập nhật kho gói và cài đặt phiên bản mới nhất từ kho mặc định của hệ điều hành.
sudo apt update && sudo apt install nginx -y
Sau khi cài đặt xong, hãy đảm bảo rằng Nginx đang chạy và được cấu hình để khởi động tự động khi máy khởi động lại. Bạn có thể kiểm tra trạng thái của dịch vụ bằng lệnh systemctl. Nếu chưa thấy đang chạy, hãy khởi động nó thủ công.
sudo systemctl status nginx
Để Nginx hoạt động như một Reverse Proxy cho ứng dụng Node.js, chúng ta cần giả lập các ứng dụng này. Trong môi trường thực tế, bạn sẽ chạy các file script Node.js của mình. Ở đây, để hướng dẫn ngắn gọn và trực quan, chúng ta sẽ tạo các script giả định chạy trên các cổng 8080, 8081 và 8082. Bạn cần cài đặt Node.js và npm nếu chưa có.
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install nodejs -y
Bây giờ, hãy tạo ba thư mục riêng biệt cho ba server backend của chúng ta để dễ quản lý file cấu hình và script chạy.
mkdir -p /var/www/app1 /var/www/app2 /var/www/app3
Cấu hình các Server Backend giả định
Chúng ta cần tạo một file JavaScript đơn giản cho mỗi ứng dụng để xác minh rằng request đang được chuyển đến đúng server nào. Dưới đây là nội dung cho server đầu tiên chạy trên cổng 8080. File này sẽ trả về một thông báo "You are on Server 1".
cat > /var/www/app1/server.js << EOF
const http = require('http');
const hostname = '127.0.0.1';
const port = 8080;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello from Node.js Server 1 on Port 8080!
');
});
server.listen(port, hostname, () => {
console.log(`Server 1 running at http://${hostname}:${port}/`);
});
EOF
Lặp lại quá trình tương tự cho Server 2 (port 8081) và Server 3 (port 8082) bằng cách thay đổi biến port và thông báo trong nội dung file. Sau khi tạo xong ba file script, hãy chạy chúng trong background để Nginx có thể connect vào. Chúng ta sẽ sử dụng lệnh nohup để giữ cho các tiến trình chạy ngay cả khi đóng terminal.
cd /var/www/app1 && nohup node server.js > app1.log 2>&1 &
cd /var/www/app2 && sed 's/8080/8081/g; s/Server 1/Server 2/g' server.js > temp.js && nohup node temp.js > app2.log 2>&1 &
cd /var/www/app3 && sed 's/8080/8082/g; s/Server 1/Server 3/g' server.js > temp.js && nohup node temp.js > app3.log 2>&1 &
Lưu ý: Trong ví dụ trên, tôi đã dùng lệnh sed để nhanh chóng tạo file cho server 2 và 3 dựa trên file của server 1 để tiết kiệm thời gian gõ lệnh. Bạn có thể kiểm tra xem các server đã chạy bằng lệnh netstat hoặc ss để đảm bảo các port 8080, 8081, 8082 đang lắng nghe kết nối.
sudo ss -tlnp | grep -E '8080|8081|8082'
Cấu hình Nginx cho Load Balancing
Bây giờ là bước quan trọng nhất: cấu hình Nginx để phân phối tải. Chúng ta sẽ tạo một file cấu hình mới trong thư mục sites-available của Nginx. Thay vì sửa file mặc định nginx.conf, tốt nhất là tạo file riêng để dễ quản lý và rollback.
sudo nano /etc/nginx/sites-available/load-balancer-app
Tiếp theo, hãy nhập đoạn cấu hình sau vào file. Phần upstream là nơi chúng ta định nghĩa nhóm các server backend. Nginx sẽ tự động quản lý danh sách này và áp dụng thuật toán cân bằng tải. Mặc định, Nginx sử dụng thuật toán Round Robin (tròn xuyến), nghĩa là mỗi request mới sẽ được chuyển đến server tiếp theo trong danh sách tuần tự.
upstream node_app_servers {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}
server {
listen 80;
server_name example.com; # Thay bằng tên miền hoặc IP của bạn
location / {
proxy_pass http://node_app_servers;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_cache_bypass $http_upgrade;
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;
}
}
Đoạn mã trên bao gồm nhiều chi tiết kỹ thuật quan trọng. Tham số proxy_pass chỉ định Nginx chuyển giao request đến upstream chúng ta đã định nghĩa. Các dòng proxy_set_header là cực kỳ quan trọng để ứng dụng Node.js phía sau biết được địa chỉ IP thật của người dùng và giao thức truy cập (HTTP/HTTPS), nếu không ứng dụng có thể thấy IP của Nginx thay vì IP client. Dòng Upgrade $http_upgrade hỗ trợ WebSocket nếu ứng dụng của bạn cần sử dụng tính năng này.
Sau khi lưu file, chúng ta cần tạo một liên kết mềm (symbolic link) từ thư mục sites-available sang sites-enabled để kích hoạt cấu hình này.
sudo ln -s /etc/nginx/sites-available/load-balancer-app /etc/nginx/sites-enabled/
Trước khi khởi động lại Nginx, bắt buộc phải kiểm tra xem cú pháp cấu hình có chính xác không bằng lệnh test config. Nếu có lỗi, Nginx sẽ không khởi động lại và bạn sẽ thấy thông báo lỗi cụ thể.
sudo nginx -t
Nếu thông báo "syntax is ok" và "test is successful", hãy tiến hành reload cấu hình để áp dụng thay đổi mà không cần dừng dịch vụ, giúp giảm thiểu thời gian ngừng phục vụ.
sudo systemctl reload nginx
Kiểm tra và Xác minh kết quả
Bây giờ, bạn có thể mở trình duyệt và truy cập vào địa chỉ IP của máy chủ (hoặc tên miền đã cấu hình). Khi bạn nhấn F5 để làm mới trang, hãy quan sát kỹ nội dung trả về. Bạn sẽ thấy thông báo luân phiên thay đổi từ "Server 1", "Server 2" đến "Server 3" và lặp lại. Điều này chứng tỏ cơ chế Round Robin đã hoạt động hoàn hảo, phân phối đều lưu lượng truy cập vào 3 instance backend.
Để kiểm tra kỹ hơn bằng dòng lệnh, bạn có thể sử dụng curl với một vòng lặp nhỏ. Lệnh dưới đây sẽ gọi 6 lần liên tiếp và cho thấy kết quả phân phối tải rõ ràng.
for i in {1..6}; do echo "Request $i: "; curl -s http://YOUR_SERVER_IP; echo; done
Thay YOUR_SERVER_IP bằng địa chỉ IP thực tế của máy chủ của bạn. Kết quả sẽ cho thấy mỗi request đi đến một cổng khác nhau, xác nhận Nginx đang đóng vai trò là điểm tập trung và phân phối tải hiệu quả.
Lưu ý quan trọng và Mở rộng nâng cao
Mặc dù cấu hình trên hoạt động tốt, trong môi trường sản xuất thực tế, bạn cần lưu ý thêm một số điểm. Thứ nhất là xử lý lỗi: nếu một trong 3 server backend bị sập (ví dụ port 8081 bị treo), Nginx mặc định vẫn sẽ cố gắng gửi request đến nó và gây ra lỗi 502 Bad Gateway cho người dùng. Để khắc phục, bạn nên thêm tham số backup hoặc max_fails và fail_timeout vào phần upstream để Nginx tự động tạm thời loại bỏ server bị lỗi khỏi danh sách phân phối.
Ví dụ nâng cao cấu hình upstream:
upstream node_app_servers {
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8082 backup; # Server này chỉ nhận request khi 2 server kia lỗi
}
Thứ hai là vấn đề SSL/HTTPS. Trong ví dụ trên, Nginx đang lắng nghe trên cổng 80 không bảo mật. Để bảo vệ dữ liệu người dùng, bạn nên cấu hình SSL (tài liệu chi tiết có thể tìm thêm về Certbot hoặc Let's Encrypt). Khi đó, Nginx sẽ giải mã HTTPS từ client và chuyển tiếp kết nối HTTP nội bộ (unencrypted) sang backend, giúp giảm tải cho ứng dụng Node.js không phải xử lý chứng chỉ số tốn tài nguyên.
Thứ ba, nếu bạn triển khai trên nhiều máy chủ vật lý riêng biệt, hãy thay đổi địa chỉ IP trong phần upstream thành IP LAN của các máy chủ đó. Việc này giúp hệ thống của bạn có khả năng chịu lỗi (High Availability): nếu cả một máy chủ bị mất điện, Nginx trên các server khác sẽ tự động chuyển hướng traffic sang các máy còn sống.
Kết luận
Hướng dẫn này đã cung cấp một lộ trình rõ ràng để biến Nginx thành một Reverse Proxy mạnh mẽ và Load Balancer linh hoạt cho các ứng dụng web. Bằng cách sử dụng cấu hình upstream và proxy_pass, bạn đã tạo ra một lớp bảo vệ và điều phối lưu lượng giúp hệ thống trở nên ổn định và dễ mở rộng hơn gấp nhiều lần so với việc chạy ứng dụng trực tiếp.
Kỹ năng cấu hình Nginx là một trong những kỹ năng cốt lõi của một Sysadmin hay DevOps Engineer giỏi. Việc hiểu rõ cách Nginx xử lý request, cân bằng tải và chuyển tiếp header sẽ giúp bạn giải quyết nhiều vấn đề phức tạp về hiệu năng và bảo mật trong tương lai. Hãy thử nghiệm thêm các thuật toán cân bằng tải khác như least_conn (ít kết nối nhất) hoặc ip_hash (giữ session dựa trên IP client) để phù hợp nhất với đặc thù ứng dụng của bạn.