Cấu hình Nginx Reverse Proxy cho Node.js: Hướng dẫn A-Z kèm SSL miễn phí

VMas-Dev-KA

Hướng dẫn chi tiết cấu hình Nginx Reverse Proxy cho ứng dụng Node.js

Reverse Proxy là gì? Tại sao Node.js cần Nginx?

Trong môi trường production, việc cho phép ứng dụng Node.js “lộ diện” trực tiếp với internet qua một cổng như 3000 là một sai lầm phổ biến của lập trình viên mới. Node.js rất mạnh mẽ trong việc xử lý logic nghiệp vụ, nhưng nó không được xây dựng để chống chọi trực tiếp với “cơn bão” traffic thô từ internet, bao gồm các cuộc tấn công từ chối dịch vụ (DoS) hoặc quét lỗ hổng bảo mật. Đây chính là lúc bạn cần đến một “người gác cổng” chuyên nghiệp: một reverse proxy.

Định nghĩa Reverse Proxy

Một reverse proxy là một máy chủ trung gian nằm giữa người dùng và máy chủ ứng dụng của bạn (Node.js). Khi một người dùng gửi yêu cầu đến website của bạn, yêu cầu đó sẽ đến reverse proxy trước. Proxy sẽ nhận yêu cầu, phân tích và chuyển tiếp nó đến ứng dụng Node.js đang chạy ở phía sau. Đối với thế giới bên ngoài, reverse proxy chính là website của bạn, giúp che giấu và bảo vệ máy chủ ứng dụng thực sự.

Ba lợi ích cốt lõi khi sử dụng Nginx làm Reverse Proxy cho Node.js

Sử dụng một reverse proxy mạnh mẽ như Nginx mang lại ba lợi ích chính: bảo mật tốt hơn, khả năng mở rộng dễ dàng và cải thiện hiệu năng đáng kể.

  1. Tăng cường bảo mật: Reverse proxy là lớp phòng thủ đầu tiên của bạn. Nó che giấu địa chỉ IP và cổng thật của ứng dụng Node.js. Bạn có thể cấu hình Nginx để chặn các địa chỉ IP độc hại, giới hạn tốc độ yêu cầu (rate limiting) và là nơi lý tưởng để kết thúc (terminate) các kết nối SSL/TLS, giảm tải xử lý cho Node.js.
  2. Tăng khả năng mở rộng và linh hoạt: Một máy chủ chỉ có thể xử lý một lượng traffic nhất định. Nginx hoạt động như một bộ cân bằng tải (load balancer), có thể phân phối các yêu cầu đến nhiều instance của ứng dụng Node.js. Điều này giúp ứng dụng của bạn không bị quá tải và sập khi lượng truy cập tăng đột biến.
  3. Cải thiện hiệu năng: Nginx cực kỳ hiệu quả trong việc phục vụ các tệp tĩnh (static files) như hình ảnh, CSS, JavaScript. Nginx sử dụng kiến trúc event-driven không chặn, cho phép nó xử lý hàng nghìn kết nối đồng thời với lượng tài nguyên rất nhỏ. Việc này giúp giải phóng Node.js, cho phép nó tập trung vào nhiệm vụ chính của mình: xử lý logic ứng dụng và các tác vụ I/O.

Tóm lại, Nginx đóng vai trò như một tấm khiên, một người quản lý lưu lượng và một người phục vụ siêu tốc, giúp ứng dụng Node.js của bạn mạnh mẽ, an toàn và chuyên nghiệp hơn trong môi trường production.

Cài đặt và cấu hình cơ bản

Trước khi bắt đầu, hãy đảm bảo bạn đã có một ứng dụng Node.js đang chạy. Ví dụ, một ứng dụng Express đơn giản lắng nghe trên cổng 3000. Bạn có thể chạy nó bằng node app.js, và lý tưởng nhất là sử dụng một process manager như PM2 để quản lý.

Cài đặt Nginx trên Ubuntu/Debian

Các bước dưới đây dành cho hệ điều hành Ubuntu/Debian. Đối với các hệ điều hành khác (CentOS, RHEL), vui lòng tham khảo tài liệu chính thức.

  1. Cập nhật danh sách gói:
    sudo apt update
  2. Cài đặt Nginx:
    sudo apt install nginx

    Lệnh này sẽ cài đặt phiên bản stable mới nhất có sẵn trong kho lưu trữ mặc định. Để có phiên bản mới nhất, bạn có thể thêm Nginx Official Repository.

  3. Kiểm tra Nginx đã hoạt động:
    sudo systemctl status nginx

    Nếu bạn thấy active (running), bạn đã cài đặt thành công. Mở trình duyệt và truy cập địa chỉ IP của máy chủ, bạn sẽ thấy trang chào mừng của Nginx.

Giải thích file config: sites-available vs sites-enabled

Cấu trúc thư mục của Nginx được tổ chức rất logic. Hai thư mục quan trọng nhất bạn cần biết là:

  • /etc/nginx/sites-available/: Thư mục này chứa tất cả các file cấu hình “block” cho các website (hay còn gọi là server block). Bạn có thể tạo bao nhiêu file cấu hình tùy thích ở đây. Các file này được lưu trữ nhưng KHÔNG được Nginx sử dụng.
  • /etc/nginx/sites-enabled/: Thư mục này chứa các liên kết tượng trưng (symbolic link) trỏ đến các file trong thư mục sites-available mà bạn MUỐN KÍCH HOẠT. Chỉ những file có liên kết trong sites-enabled mới được Nginx đọc và sử dụng khi khởi động hoặc reload.

Cơ chế này cực kỳ hữu ích vì nó cho phép bạn dễ dàng bật/tắt một trang web chỉ bằng cách tạo hoặc xóa một symbolic link, thay vì phải xóa hoặc di chuyển cả file cấu hình.

Cấu hình Block Server tối ưu cho Node.js

Đây là phần quan trọng nhất. Chúng ta sẽ tạo một server block để chuyển tiếp mọi yêu cầu đến ứng dụng Node.js.

  1. Tạo một file cấu hình mới trong thư mục sites-available. Đặt tên theo tên miền của bạn, ví dụ: your-domain.com.

    sudo nano /etc/nginx/sites-available/your-domain.com
  2. Thêm nội dung sau vào file:

    server {
        listen 80;
        listen [::]:80;
        server_name your-domain.com www.your-domain.com;
    
        # Giới hạn kích thước upload (tuỳ chỉnh theo nhu cầu)
        client_max_body_size 10M;
    
        # Cấu hình gzip nén để tăng tốc tải trang
        gzip on;
        gzip_vary on;
        gzip_min_length 1024;
        gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
    
        location / {
            # Địa chỉ và cổng của ứng dụng Node.js của bạn
            proxy_pass http://localhost:3000;
    
            # Các header quan trọng để forward thông tin client
            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_set_header X-Forwarded-Host $host;
            proxy_set_header X-Forwarded-Port $server_port;
    
            # Các thiết lập timeout và buffer
            proxy_connect_timeout 5s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
            proxy_buffering off;
        }
    
        # Ẩn thông tin phiên bản Nginx
        server_tokens off;
    }
  3. Kích hoạt site và kiểm tra cú pháp:

    sudo ln -s /etc/nginx/sites-available/your-domain.com /etc/nginx/sites-enabled/
    sudo nginx -t                # kiểm tra cú pháp – nếu OK mới reload
    sudo systemctl reload nginx  # tải lại cấu hình mà không ngắt kết nối
  4. Kiểm tra log (khi có lỗi):

    sudo tail -f /var/log/nginx/error.log
    sudo tail -f /var/log/nginx/access.log

Giải thích chi tiết về các directive:

  • proxy_pass http://localhost:3000;: Đây là linh hồn của toàn bộ cấu hình. Nó chỉ định địa chỉ của máy chủ upstream (backend), nơi Nginx sẽ chuyển tiếp các yêu cầu. Trong trường hợp này, ứng dụng Node.js của bạn đang chạy trên cổng 3000 của máy chủ local.
  • proxy_set_header Host $host;: Directive này rất quan trọng. Nó chuyển tiếp tên miền gốc mà client yêu cầu (ví dụ: your-domain.com) đến ứng dụng Node.js của bạn. Nếu thiếu header này, nhiều ứng dụng có thể gặp lỗi khi tạo các đường dẫn tuyệt đối hoặc xác thực domain.
  • proxy_set_header X-Real-IP $remote_addr;: Header này gửi địa chỉ IP thực của client đến ứng dụng Node.js. Biến $remote_addr là IP của kết nối trực tiếp đến Nginx. Đây là IP thực của người dùng (hoặc của proxy trước đó, nếu có).
  • proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;: Đây là header mạnh mẽ nhất để theo dõi nguồn gốc của request. Nó tạo ra một danh sách các địa chỉ IP mà request đã đi qua. $proxy_add_x_forwarded_for là một biến thông minh: nếu header X-Forwarded-For đã tồn tại, nó sẽ thêm IP hiện tại vào cuối danh sách; nếu chưa, nó sẽ tự tạo mới. Đây là tiêu chuẩn để xác định client gốc khi có nhiều lớp proxy (ví dụ: client -> CDN -> Nginx -> Node.js).
  • proxy_set_header X-Forwarded-Proto $scheme;: Header này cho Node.js biết liệu kết nối gốc giữa client và Nginx là http hay https. Điều này rất quan trọng khi Nginx xử lý SSL termination, vì kết nối giữa Nginx và Node.js thường là HTTP không mã hóa. Thiếu header này, ứng dụng của bạn có thể tạo ra các URL với scheme “http” nhầm lẫn.
  • proxy_set_header X-Forwarded-Host $host;: Header này chuyển tiếp hostname gốc, hoạt động tương tự như Host nhưng là một tiêu chuẩn khác.
  • proxy_set_header X-Forwarded-Port $server_port;: Header này cho backend biết client đã kết nối đến cổng nào của Nginx (ví dụ: cổng 443 cho HTTPS).

Xác thực: Các directive proxy_set_header ở trên là những tiêu chuẩn công nghiệp và được khuyến khích sử dụng theo tài liệu chính thức của Nginx.

Xử lý WebSocket với Nginx

Nếu ứng dụng Node.js của bạn sử dụng WebSocket (ví dụ: với Socket.IO), bạn cần thêm một vài dòng cấu hình để Nginx hiểu và chuyển tiếp đúng giao thức này.

Hãy thay đổi location / block của bạn thành:

location / {
    proxy_pass http://localhost:3000;

    # Các header cơ bản
    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;

    # Cấu hình cho WebSocket
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    # Tăng timeout cho các kết nối WebSocket lâu dài
    proxy_read_timeout 7d;

    # Tắt buffering cho WebSocket
    proxy_buffering off;
}

Giải thích các directive cho WebSocket:

  • proxy_http_version 1.1;: WebSocket yêu cầu HTTP/1.1 hoặc cao hơn. Mặc định của Nginx cho các kết nối proxy là HTTP/1.0, vì vậy bạn cần phải thay đổi directive này.
  • proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "upgrade";: Đây là hai header quan trọng nhất. Chúng thông báo cho Nginx rằng đây là một yêu cầu “nâng cấp” kết nối lên WebSocket. Nginx sẽ chuyển tiếp các header này đến backend, cho phép thiết lập kết nối WebSocket thành công.
  • proxy_read_timeout 7d;: WebSocket là các kết nối lâu dài. Timeout mặc định 60 giây của Nginx sẽ giết chết các kết nối WebSocket đang không hoạt động. Giá trị 7d (7 ngày) là một lựa chọn phổ biến, nhưng bạn nên điều chỉnh dựa trên hành vi của ứng dụng và cơ chế ping/pong từ phía client.

Cài đặt SSL miễn phí với Let’s Encrypt và Certbot

Bảo mật là điều bắt buộc cho bất kỳ ứng dụng production nào. Chúng ta sẽ sử dụng Let’s Encrypt, một tổ chức cung cấp chứng chỉ SSL/TLS miễn phí, và Certbot, một công cụ tự động hóa quy trình này.

  1. Cài đặt Certbot và plugin cho Nginx:
    sudo apt install certbot python3-certbot-nginx
  2. Chạy Certbot để lấy và cài đặt chứng chỉ:

    sudo certbot --nginx -d your-domain.com -d www.your-domain.com

    Certbot sẽ tự động:

    • Xác thực quyền sở hữu domain của bạn.
    • Lấy chứng chỉ từ Let’s Encrypt.
    • Tự động cập nhật file cấu hình Nginx của bạn để sử dụng chứng chỉ SSL vừa lấy.
    • Cấu hình chuyển hướng từ HTTP sang HTTPS.

    Certbot cũng tự động thiết lập một tác vụ lên lịch (cron job hoặc systemd timer) để gia hạn chứng chỉ 90 ngày một lần. Bạn có thể kiểm tra cơ chế gia hạn bằng lệnh:

    sudo certbot renew --dry-run
  3. Kiểm tra file cấu hình: Sau khi chạy Certbot, hãy mở lại file cấu hình của bạn (ví dụ: /etc/nginx/sites-available/your-domain.com). Bạn sẽ thấy nó đã được Certbot cập nhật để lắng nghe trên cổng 443 (HTTPS) và có thêm các dòng chỉ định đường dẫn đến chứng chỉ.

Các lỗi thường gặp và cách khắc phục

❌ Lỗi 502 Bad Gateway

Nguyên nhân: Đây là lỗi phổ biến nhất. Nó xảy ra khi Nginx không thể kết nối đến máy chủ upstream (ứng dụng Node.js của bạn). Nguyên nhân thường gặp:

  • Ứng dụng Node.js của bạn chưa được khởi động hoặc đã bị dừng.
  • Ứng dụng Node.js đang chạy trên một cổng khác (ví dụ: 4000), nhưng trong config Nginx bạn lại khai báo proxy_pass http://localhost:3000;.
  • Ứng dụng Node.js bị lỗi và tự crash. Hãy kiểm tra log của ứng dụng.
  • Tường lửa chặn kết nối từ Nginx đến cổng ứng dụng của bạn.

Cách khắc phục: Đây là một quy trình debug đơn giản:

  1. Kiểm tra ứng dụng Node.js của bạn có đang chạy không: ps aux | grep node hoặc pm2 status.
  2. Kiểm tra lại cổng trong config Nginx (thường dùng lệnh sudo nginx -t để kiểm tra syntax, nhưng không phát hiện lỗi logic).
  3. Thử truy cập trực tiếp vào ứng dụng Node.js: curl http://localhost:3000. Nếu lệnh này thất bại, vấn đề chắc chắn nằm ở ứng dụng của bạn. Nếu thành công, hãy kiểm tra log lỗi của Nginx để biết thêm chi tiết:
    sudo tail -f /var/log/nginx/error.log

    Log thường sẽ hiển thị thông báo connect() failed (111: Connection refused) nếu không thể kết nối, hoặc upstream timed out nếu Node.js phản hồi quá chậm.

❌ Lỗi 413 Request Entity Too Large

Nguyên nhân: Upload file vượt quá client_max_body_size.
Sửa: Tăng giá trị trong block server.

Best Practices khi cấu hình Nginx

  • Luôn ẩn thông tin phiên bản Nginx: Đây là một biện pháp bảo mật cơ bản nhưng cực kỳ quan trọng. Thêm server_tokens off; vào trong http block của file nginx.conf để ngăn Nginx tiết lộ số phiên bản trong response header và các trang lỗi. Điều này khiến kẻ tấn công khó khai thác các lỗ hổng theo phiên bản cụ thể hơn.
  • Sử dụng Gzip compression: Bật nén Gzip giúp giảm kích thước dữ liệu truyền tải, cải thiện đáng kể tốc độ tải trang, đặc biệt là trên các thiết bị di động. Cấu hình mẫu đã bao gồm phần này, bạn có thể tinh chỉnh thêm gzip_types để phù hợp với ứng dụng của mình.
  • Cân nhắc tắt proxy_buffering cho API và WebSocket: Khi proxy_buffering được bật (mặc định), Nginx sẽ đọc toàn bộ response từ backend vào bộ nhớ đệm trước khi gửi cho client. Điều này tốt cho các response lớn nhưng sẽ gây ra độ trễ (latency) cho các API real-time hoặc WebSocket. Cấu hình mẫu đã tắt (proxy_buffering off;) cho toàn bộ location. Chỉ tắt proxy_buffering cho API real-time hoặc WebSocket. Với response tĩnh lớn, hãy bật lại và tăng proxy_buffer_size.
  • Giới hạn tốc độ (rate limiting) để chống DoS cơ bản:
      limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; # thêm ở /etc/nginx/nginx.conf
      # Trong location /
      location / {
          limit_req zone=api_limit burst=20 nodelay;
          proxy_pass http://localhost:3000;
      # ... các dòng khác
      }

Câu hỏi thường gặp (FAQ)

Có thể dùng Apache thay Nginx không?

Có, Apache cũng có thể hoạt động như một reverse proxy với các module như mod_proxymod_proxy_http. Tuy nhiên, Nginx thường được ưa chuộng hơn cho mục đích này vì kiến trúc hướng sự kiện, khả năng xử lý concurrent connections vượt trội và tiêu thụ ít bộ nhớ hơn so với Apache, vốn sử dụng mô hình xử lý theo process/thread cho mỗi kết nối.

Làm sao để trỏ nhiều tên miền vào các app Node.js khác nhau?

Rất đơn giản! Bạn chỉ cần tạo các file cấu hình riêng biệt cho từng tên miền trong thư mục sites-available, mỗi file có một server_name và một proxy_pass khác nhau. Sau đó tạo symbolic link cho từng file vào thư mục sites-enabled. Nginx sẽ tự động định tuyến các yêu cầu dựa trên giá trị của header Host.

Ví dụ, bạn có thể có app1.your-domain.com trỏ đến cổng 3001app2.your-domain.com trỏ đến cổng 3002.

Kết luận

Việc đặt Nginx làm reverse proxy phía trước ứng dụng Node.js không chỉ là một best practice mà còn là một yêu cầu bắt buộc cho bất kỳ ứng dụng production nào. Bài viết này đã cung cấp cho bạn một lộ trình hoàn chỉnh từ A đến Z: từ lý do tại sao cần sử dụng reverse proxy, cách cài đặt, cấu hình cơ bản với proxy_pass và các header quan trọng, xử lý WebSocket, đến việc bảo mật ứng dụng bằng SSL miễn phí từ Let’s Encrypt.

Bằng cách áp dụng những kiến thức và cấu hình trên, bạn đã sẵn sàng deploy ứng dụng Node.js của mình lên môi trường production một cách chuyên nghiệp, an toàn và hiệu quả.

Tham khảo chính thức:

Chia sẻ bài viết này