jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Nginx from Zero to Production · Part 3 — Reverse Proxy & Load Balancing

Put Nginx in front of a real app: proxy_pass, the headers you must forward, upstream pools, load-balancing algorithms, health checks and failover, and WebSocket proxying — with a runnable Node demo and exercises.

So far Nginx has served files from disk {Đến giờ Nginx mới phục vụ file từ đĩa}. Now the real job: Nginx as a reverse proxy in front of your application {Giờ tới việc thật: Nginx làm reverse proxy đứng trước ứng dụng của bạn}. This is how almost every production Node, Python, Go, Rails, or Java app is exposed to the internet {Đây là cách gần như mọi app Node, Python, Go, Rails hay Java được đưa ra internet}.

We’ll also turn one proxy into a load balancer across several app instances {Ta cũng sẽ biến một proxy thành load balancer qua nhiều instance app}.


1. What “reverse proxy” means {“Reverse proxy” nghĩa là gì}

A forward proxy sits in front of clients (e.g. a corporate proxy hiding employees) {Forward proxy đứng trước client (vd proxy công ty che nhân viên)}. A reverse proxy sits in front of servers — the client thinks it’s talking to one server, but Nginx quietly relays to a backend behind it {Reverse proxy đứng trước server — client tưởng đang nói chuyện với một server, nhưng Nginx lặng lẽ chuyển tiếp tới backend phía sau}.

   Browser ──HTTP──►  NGINX :80  ──HTTP──►  Node app :3000
            ◄────────  (public)  ◄────────  (private, not exposed)

Why put Nginx in front of an app that can already speak HTTP? {Tại sao đặt Nginx trước một app vốn đã nói được HTTP?}

  • TLS termination — Nginx handles HTTPS so your app stays simple (Part 4) {Đầu cuối TLS — Nginx xử lý HTTPS để app của bạn đơn giản (Phần 4)}.
  • One public port — serve static files, multiple apps, and APIs under one domain {Một cổng public — phục vụ file tĩnh, nhiều app, và API dưới một domain}.
  • Load balancing — fan out to many app instances {Cân bằng tải — toả ra nhiều instance app}.
  • Protection — rate limiting, request size limits, hiding backend details {Bảo vệ — giới hạn tốc độ, giới hạn kích thước request, giấu chi tiết backend}.

2. A backend to proxy {Một backend để proxy}

Let’s create a tiny backend so the rest is concrete {Hãy tạo một backend nhỏ để phần sau cụ thể}. Any HTTP server works; here’s a dependency-free Node + TypeScript one {Server HTTP nào cũng được; đây là một cái Node + TypeScript không cần thư viện}:

// server.ts — run several copies on different ports to test load balancing
import { createServer } from 'node:http';

// PORT lets us launch the same code on 3000, 3001, 3002…
const port = Number(process.env.PORT ?? 3000);

const server = createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  // Echo the port so we can SEE which instance answered.
  res.end(JSON.stringify({ from: port, path: req.url }));
});

server.listen(port, () => console.log(`backend up on :${port}`));
npx tsx server.ts                 # one instance on :3000
PORT=3001 npx tsx server.ts       # in another terminal
PORT=3002 npx tsx server.ts       # and another

3. proxy_pass — forward to the backend {proxy_pass — chuyển tiếp tới backend}

The minimal reverse proxy is one directive {Reverse proxy tối giản chỉ một directive}:

server {
    listen 80;
    server_name localhost;

    location / {
        proxy_pass http://127.0.0.1:3000;   # forward everything to the Node app
    }
}
sudo nginx -t && sudo nginx -s reload
curl http://localhost/hello
# → {"from":3000,"path":"/hello"}

Nginx received the request on :80 and relayed it to the app on :3000 {Nginx nhận request ở :80 và chuyển tới app ở :3000}. The browser never sees :3000 {Trình duyệt không bao giờ thấy :3000}.

The trailing-slash gotcha {Cái bẫy dấu gạch chéo cuối}

This catches everyone {Cái này bẫy tất cả mọi người}:

location /api/ {
    proxy_pass http://127.0.0.1:3000;     # NO trailing slash → keeps /api/ prefix
    # request /api/users  →  backend gets /api/users
}

location /api/ {
    proxy_pass http://127.0.0.1:3000/;    # WITH trailing slash → strips /api/
    # request /api/users  →  backend gets /users
}

Rule {Quy tắc}: a trailing slash on proxy_pass strips the matched location prefix; no slash keeps it {dấu / cuối trên proxy_pass cắt bỏ prefix location đã khớp; không có dấu / thì giữ lại}. Choose based on whether your backend expects the /api prefix {Chọn tuỳ theo backend của bạn có mong đợi prefix /api hay không}.


4. The headers you MUST forward {Các header bạn BẮT BUỘC chuyển tiếp}

By default the backend loses information about the original request — it sees the connection coming from Nginx (127.0.0.1), not the real client {Mặc định backend mất thông tin về request gốc — nó thấy kết nối đến từ Nginx (127.0.0.1), không phải client thật}. Forward the truth explicitly {Chuyển tiếp sự thật một cách tường minh}:

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

    proxy_set_header Host              $host;               # original Host header
    proxy_set_header X-Real-IP         $remote_addr;        # real client IP
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;             # http or https
}

Why each matters {Vì sao mỗi cái quan trọng}:

  • Host — your app needs the real hostname for routing, links, cookies {app của bạn cần hostname thật để routing, tạo link, cookie}.
  • X-Real-IP / X-Forwarded-For — without these, every visitor looks like 127.0.0.1 in your app’s logs and rate limiters {thiếu chúng, mọi khách trông như 127.0.0.1 trong log và rate limiter của app}.
  • X-Forwarded-Proto — tells the app the user came over https even though Nginx→app is plain http; frameworks use this to build correct redirect URLs and set Secure cookies {báo cho app rằng user vào qua https dù Nginx→app là http thường; framework dùng cái này để tạo URL redirect đúng và đặt cookie Secure}.

A huge fraction of “redirect loop” and “wrong IP in logs” bugs come from forgetting these four lines {Một phần rất lớn lỗi “vòng lặp redirect” và “sai IP trong log” đến từ việc quên bốn dòng này}. Put them in a snippet you include everywhere (we’ll do that in Part 5) {Đặt chúng vào một snippet rồi include ở mọi nơi (ta sẽ làm ở Phần 5)}.


5. upstream — define a pool of backends {upstream — định nghĩa nhóm backend}

To load balance, name a group of servers with an upstream block, then proxy_pass to that name {Để cân bằng tải, đặt tên một nhóm server bằng block upstream, rồi proxy_pass tới tên đó}:

# in the http context, OUTSIDE server { }
upstream app_pool {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
    listen 80;
    location / {
        proxy_pass http://app_pool;        # proxy to the pool, not one host
        proxy_set_header Host $host;
    }
}
sudo nginx -t && sudo nginx -s reload
curl http://localhost/ ; curl http://localhost/ ; curl http://localhost/
# → {"from":3000,...}  {"from":3001,...}  {"from":3002,...}

The from value rotates — Nginx is spreading requests across all three instances {Giá trị from xoay vòng — Nginx đang chia request qua cả ba instance}.


6. Load-balancing algorithms {Thuật toán cân bằng tải}

Choose how Nginx picks the next backend {Chọn cách Nginx chọn backend tiếp theo}:

upstream app_pool {
    # round_robin;        # DEFAULT — rotate evenly through servers
    least_conn;           # send to the server with fewest active connections
    # ip_hash;            # same client IP always → same server (sticky sessions)

    server 127.0.0.1:3000 weight=3;   # gets 3× the traffic (beefier box)
    server 127.0.0.1:3001;
    server 127.0.0.1:3002 backup;     # only used when others are down
}
Algorithm {Thuật toán}When to use {Khi nào dùng}
round_robin (default)Stateless apps, evenly-sized requests {App stateless, request đều nhau}
least_connRequests vary in duration (some slow, some fast) {Request dài ngắn khác nhau}
ip_hashYou need session stickiness without shared session store {Cần dính phiên mà không có session store chung}

Modifiers {Bổ ngữ}: weight=N sends proportionally more traffic to stronger servers; backup marks a standby; down takes one out without deleting the line {weight=N gửi nhiều traffic hơn theo tỉ lệ tới server mạnh; backup đánh dấu dự phòng; down loại một cái ra mà không xoá dòng}.

Prefer stateless backends + a shared session store (Redis) so any instance can serve any user — then you don’t need ip_hash at all {Ưu tiên backend stateless + session store chung (Redis) để mọi instance phục vụ được mọi user — khi đó bạn chẳng cần ip_hash}.


7. Health checks & failover {Health check & chuyển dự phòng}

Open-source Nginx does passive health checks: if a backend fails too many times, it’s marked unavailable for a while {Nginx bản mã nguồn mở làm health check bị động: nếu một backend lỗi quá nhiều lần, nó bị đánh dấu không khả dụng một lúc}:

upstream app_pool {
    server 127.0.0.1:3000 max_fails=3 fail_timeout=10s;
    server 127.0.0.1:3001 max_fails=3 fail_timeout=10s;
}
  • max_fails=3 — after 3 failed attempts {sau 3 lần thử thất bại}…
  • fail_timeout=10s — …stop sending traffic there for 10 seconds, then try again {…ngừng gửi traffic tới đó 10 giây, rồi thử lại}.

Test it live {Test trực tiếp}: kill the :3000 instance and keep curl-ing — after a couple of failures Nginx routes everything to the survivors, with no error shown to the user {tắt instance :3000 và cứ curl tiếp — sau vài lần lỗi Nginx dồn mọi thứ sang các cái còn sống, không hiện lỗi cho user}. (Active health checks — Nginx probing /health on a schedule — are an Nginx Plus feature {Health check chủ động — Nginx thăm dò /health theo lịch — là tính năng của Nginx Plus}.)


8. Timeouts, buffering & body size {Timeout, buffering & kích thước body}

Sensible proxy defaults you’ll want almost everywhere {Mặc định proxy hợp lý mà bạn sẽ muốn ở gần như mọi nơi}:

location / {
    proxy_pass http://app_pool;

    proxy_connect_timeout 5s;     # max time to CONNECT to backend
    proxy_send_timeout    60s;    # max time to SEND the request
    proxy_read_timeout    60s;    # max time to wait for a response chunk

    client_max_body_size  10m;    # reject uploads bigger than 10 MB (413)

    proxy_buffering on;           # buffer the backend response (default on)
}

Two gotchas {Hai cái bẫy}:

  • The default client_max_body_size is 1 MB — file uploads larger than that fail with 413 Request Entity Too Large until you raise it {Mặc định client_max_body_size1 MB — upload lớn hơn sẽ lỗi 413 Request Entity Too Large cho tới khi bạn nâng lên}.
  • For streaming responses (Server-Sent Events, live logs), turn proxy_buffering off; in that location, or Nginx will hold the stream and the client sees nothing until it ends {Với response streaming (Server-Sent Events, log trực tiếp), tắt proxy_buffering off; trong location đó, nếu không Nginx sẽ giữ luồng và client không thấy gì cho tới khi kết thúc}.

9. Proxying WebSockets {Proxy WebSocket}

WebSockets start as HTTP then upgrade the connection {WebSocket bắt đầu là HTTP rồi nâng cấp kết nối}. Nginx won’t forward the upgrade unless you tell it to {Nginx sẽ không chuyển tiếp việc nâng cấp trừ khi bạn bảo nó}:

location /ws/ {
    proxy_pass http://app_pool;

    proxy_http_version 1.1;                  # upgrades require HTTP/1.1
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection "upgrade";    # the magic that allows ws://
    proxy_read_timeout 3600s;                 # keep idle sockets alive (1h)
}

Forget the Upgrade/Connection headers and your WebSocket “connects then immediately closes” — a very common, very confusing bug {Quên header Upgrade/Connection thì WebSocket của bạn “kết nối rồi đóng ngay” — một lỗi rất phổ biến, rất khó hiểu}.


10. Recap {Tóm tắt}

  • proxy_pass forwards a request to a backend; the trailing slash decides whether the location prefix is stripped {proxy_pass chuyển request tới backend; dấu gạch chéo cuối quyết định prefix có bị cắt không}.
  • Always forward Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto {Luôn chuyển tiếp Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto}.
  • upstream + proxy_pass http://name gives you load balancing; pick round_robin / least_conn / ip_hash {upstream + proxy_pass http://name cho bạn cân bằng tải; chọn round_robin / least_conn / ip_hash}.
  • max_fails + fail_timeout give passive failover {max_fails + fail_timeout cho chuyển dự phòng bị động}.
  • Mind client_max_body_size, streaming buffering, and WebSocket upgrade headers {Để ý client_max_body_size, buffering cho streaming, và header nâng cấp WebSocket}.

Next — Part 4: TLS/HTTPS, security & performance {Tiếp — Phần 4: TLS/HTTPS, bảo mật & hiệu năng}: certificates, HTTP/2, gzip/brotli, caching, and rate limiting {chứng chỉ, HTTP/2, gzip/brotli, caching, và giới hạn tốc độ}.


Exercises {Bài tập}

  1. Proxy a real app {Proxy một app thật}: run the Node server on :3000 and proxy / to it; confirm curl http://localhost/ returns the JSON from :3000 {chạy server Node ở :3000 và proxy / tới nó; xác nhận curl http://localhost/ trả JSON từ :3000}.
  2. Trailing slash {Dấu gạch chéo cuối}: prove the difference between proxy_pass http://127.0.0.1:3000; and .../; under location /api/ by inspecting the path in the JSON echo {chứng minh khác biệt giữa proxy_pass http://127.0.0.1:3000;.../; dưới location /api/ bằng cách xem path trong JSON echo}.
  3. Load balance {Cân bằng tải}: run three instances (3000/3001/3002), define an upstream, and watch from rotate across 9 requests {chạy ba instance (3000/3001/3002), định nghĩa upstream, và xem from xoay vòng qua 9 request}.
  4. Failover {Chuyển dự phòng}: with max_fails=2 fail_timeout=10s, kill one instance and confirm traffic shifts to the others with no client error {với max_fails=2 fail_timeout=10s, tắt một instance và xác nhận traffic chuyển sang các cái khác mà client không lỗi}.
  5. Real client IP {IP client thật}: add the four proxy_set_header lines, log X-Forwarded-For in your Node app, and confirm you see your real IP instead of 127.0.0.1 {thêm bốn dòng proxy_set_header, log X-Forwarded-For trong app Node, và xác nhận bạn thấy IP thật thay vì 127.0.0.1}.
  6. Stretch {Nâng cao}: set client_max_body_size 1m, upload a 2 MB file, observe the 413, then raise the limit and succeed {đặt client_max_body_size 1m, upload file 2 MB, quan sát 413, rồi nâng giới hạn và thành công}.