---
id: networking-nginx-reverse-proxy
title: "Nginx reverse proxy"
track: networking
trackTitle: "Networking Fundamentals"
order: 7
level: core
status: published
estimatedMinutes: 40
prerequisites:
  - networking-firewall
  - linux-process-management
next: webserver-apache
reviewedAgainst: 2026-10-15
sources:
  - title: "KodeKloud Engineer — 100 Days of DevOps"
    url: https://engineer.kodekloud.com/practice
  - title: "ngx_http_proxy_module"
    url: https://nginx.org/en/docs/http/ngx_http_proxy_module.html
  - title: "ngx_http_core_module — server"
    url: https://nginx.org/en/docs/http/ngx_http_core_module.html
  - title: "ngx_http_upstream_module"
    url: https://nginx.org/en/docs/http/ngx_http_upstream_module.html
  - title: "ngx_http_fastcgi_module"
    url: https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html
---

# Nginx reverse proxy

Mẫu dưới đây chạy HTTP cho lab. Site public thì cần TLS; phần certificate và config `listen 443 ssl` nằm ở [ngx_http_ssl_module](https://nginx.org/en/docs/http/ngx_http_ssl_module.html).

## Để làm gì

Bạn đặt Nginx đứng trước app Node.js, Python hoặc một process khác. User kết nối tới Nginx, Nginx chuyển request tới process đang nghe ở địa chỉ nội bộ, rồi trả response về cho user.

## Gồm những gì

- Khối `server` và `location`.
- `proxy_pass`.
- `upstream` + `proxy_pass http://tên;`. Mặc định weighted round-robin.
- PHP-FPM: `fastcgi_pass unix:/path;` (hoặc `127.0.0.1:9000`).
- Header `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`.
- Kiểm tra cú pháp bằng `nginx -t`, nạp lại config bằng `systemctl reload`.

## Dùng khi nào

Tình huống điển hình: app nghe một port cao trên cùng máy, và bạn cần một điểm vào theo domain ở port `80` hoặc `443`.

Nếu app đang listen sai interface, reverse proxy không cứu được khi bạn còn chưa rõ địa chỉ đích. `proxy_pass` phải trỏ tới một địa chỉ mà chính Nginx kết nối được.

## Kiến thức cốt lõi

`listen 80` mở port HTTP. `server_name` quyết định khối `server` nào xử lý request, dựa trên header `Host`.

`proxy_pass http://127.0.0.1:3000;` không kèm URI path, nên Nginx chuyển nguyên request URI tới upstream. Dùng dạng này khi app tự route các path của nó.

`proxy_pass http://127.0.0.1:3000/;` có kèm path, nên Nginx thay phần location đã khớp bằng path đó. Chỉ cần thêm hoặc bỏ dấu `/` ở cuối là route mà app nhận được đã khác.

`proxy_http_version 1.1` cùng các header `Connection` cần khi app dùng WebSocket hoặc muốn keepalive rõ ràng. Với HTTP thường, thứ phải nắm trước vẫn là `Host`.

App đứng sau proxy không thấy IP public của client trên connection TCP, vì connection đó đến từ Nginx. Các header sau mang thông tin đó vào request:

| Header | Giá trị trong mẫu | Ý nghĩa |
| --- | --- | --- |
| `Host` | `$host` | Tên miền client đã dùng |
| `X-Real-IP` | `$remote_addr` | Địa chỉ TCP mà Nginx thấy |
| `X-Forwarded-For` | `$proxy_add_x_forwarded_for` | Danh sách IP chuyển tiếp |
| `X-Forwarded-Proto` | `$scheme` | `http` hoặc `https` tại Nginx |

App chỉ nên tin các header này khi request đi qua proxy của chính mình, vì client gọi thẳng vào port `3000` có thể tự gắn header giả.

Nhiều backend thì khai `upstream` ở ngoài khối `server`. Mỗi dòng `server` trong nhóm là một backend. `proxy_pass http://app_backends;` dùng đúng tên nhóm — không viết IP trong `location` nữa. Mặc định là weighted round-robin: không ghi `weight` thì mỗi backend bằng nhau. `weight=2` nhận gấp đôi lần `weight` mặc định 1. `fail_timeout` và `max_fails` chỉ backend ra khỏi vòng khi lỗi; đó là bước sau, không bắt buộc cho lab cân bằng đơn giản.

PHP-FPM không nói HTTP với Nginx. `fastcgi_pass` gửi FastCGI. Socket Unix: `fastcgi_pass unix:/run/php/php-fpm.sock;`. Docs module cũng cho `fastcgi_pass localhost:9000;` nếu FPM nghe TCP. Path socket phải trùng file `listen` trong pool FPM. Sai path thì 502, không phải 404. `index` và `SCRIPT_FILENAME` thuộc bộ `fastcgi_param`; lab tối thiểu vẫn phải có `fastcgi_pass` đúng địa chỉ FPM đang listen.

## Lệnh và code mẫu

File lab, ví dụ `/etc/nginx/sites-available/api.example.com.conf`:

```nginx
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        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;
    }
}
```

Hai backend (load balancer lab). Tên sau `http://` phải khớp khối `upstream`:

```nginx
upstream app_backends {
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://app_backends;
        proxy_set_header Host $host;
    }
}
```

PHP-FPM qua socket Unix (path phải trùng pool FPM):

```nginx
location ~ \.php$ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}
```

App lab tương ứng nghe ở `127.0.0.1:3000`. Kiểm tra rồi nạp config:

```bash
sudo nginx -t
sudo systemctl reload nginx
```

`nginx -t` kiểm tra cú pháp và các đường dẫn mà config tham chiếu tới. `reload` bảo process Nginx đang chạy nạp config mới. Nếu test fail thì đừng reload.

Nếu domain chưa có DNS, thêm một dòng vào `/etc/hosts` trên máy dev:

```text
127.0.0.1 api.example.com
```

## Thực hành

1. Chạy một process nghe port `3000`; bạn có thể cho app sẵn có của repo nghe ở port này.
2. Lưu khối `server` vào một file config lab riêng, đừng ghi đè file config của site đang phục vụ user.
3. Chạy `sudo nginx -t`.
4. Khi kết quả là `syntax is ok` và `test is successful`, chạy `sudo systemctl reload nginx`.
5. Gọi `curl -sS -D - http://api.example.com/ -o /dev/null` từ chính máy đó.
6. Đọc khối `upstream` mẫu. Gọi cùng URL vài lần và ghi lại backend nào trả (nếu app in hostname).
7. Với PHP-FPM: đối chiếu path trong `fastcgi_pass unix:` với `listen` của pool. Sai path thì 502.

## Kết quả mong đợi

- `nginx -t` báo config hợp lệ.
- `ss -tuln` thấy Nginx nghe port `80` và app nghe `3000`.
- `curl` nhận status từ app, thường là `200` nếu path `/` của app trả thành công.
- Access log của Nginx ghi request vừa gửi.

## Checklist

- [ ] Giải thích `proxy_pass` có path và không có path khác nhau chỗ nào.
- [ ] Nói được vì sao app cần `X-Forwarded-For` để biết IP client.
- [ ] Chạy `nginx -t` trước `reload`.
- [ ] Khi site đã public, để port của app ở địa chỉ mà chỉ Nginx với tới được.
- [ ] Nhiều backend: khối `upstream` + `proxy_pass http://tên;`. Mặc định round-robin.
- [ ] PHP-FPM: `fastcgi_pass unix:/path;` trùng file `listen` của pool. Sai path → 502.

## Thẻ ôn

### networking-nginx-reverse-proxy-slash
kind: distinguish
cue: `proxy_pass` không có path thì Nginx chuyển nguyên URI. Có path, kể cả một dấu `/`, thì Nginx thay phần location đã khớp. Thêm `/` cho đẹp có thể làm route sâu thành 404.

Q: Hai dòng trông gần giống: `proxy_pass http://127.0.0.1:3000;` và `proxy_pass http://127.0.0.1:3000/;`. App tự route theo path gốc. Dòng nào giữ nguyên request URI, và thêm dấu `/` cho "đẹp" có thể gây gì?

A: Dòng không có path ở cuối chuyển nguyên request URI sang upstream. User gọi `/orders/12` thì app nhận `/orders/12`. Dòng có path, kể cả chỉ một dấu `/`, bảo Nginx thay phần location đã khớp bằng path đó. Một location khớp prefix dài có thể bị cắt, app nhận một path khác và trả 404 cho route mà nó vẫn có. App tự lo routing thì dùng dạng không có path. Đừng thêm `/` chỉ vì thói quen kết thúc URL.

### networking-nginx-reverse-proxy-forwarded
kind: decide
cue: `X-Forwarded-For` chỉ đáng tin khi request tới từ proxy bạn đã khai. Client tự gửi header đó thì đó là IP họ tự viết.

Q: Ứng dụng đứng sau Nginx và đọc `X-Forwarded-For` trên mọi connection để biết IP người dùng, rồi dùng IP đó cho giới hạn tốc độ. Cách đọc đó đúng chưa?

A: Chưa. Header này tồn tại để proxy nói hộ IP client, vì app chỉ thấy IP của Nginx. Giá trị chỉ đáng tin khi kết nối tới từ một proxy đã nằm trong danh sách trusted. Client gọi thẳng vào app, hoặc gọi qua một proxy lạ, có thể tự ghi bất kỳ IP nào vào header. Tin mọi địa chỉ là để người gọi tự chọn IP. Khai đúng các proxy được tin, và chỉ lấy header trên các kết nối từ đó.

### networking-nginx-reverse-proxy-test
kind: command
cue: Sửa `nginx.conf` thì `nginx -t` trước khi reload. Config không nạp được thì worker cũ còn, nhưng lần restart sau có thể không lên.

Q: Bạn vừa sửa config Nginx và định `reload` ngay để khỏi rớt connection. Bước nào phải đứng trước, và nếu file mới sai thì lần reload này với lần restart sau khác nhau thế nào?

A: Chạy `nginx -t` trước. Lệnh kiểm tra cú pháp mà chưa đổi worker đang phục vụ. Config đúng thì reload: Nginx giữ worker cũ cho đến khi worker mới nhận được file. Config không nạp được thì lần reload này thường giữ traffic trên worker cũ, site chưa sập ngay. Nhưng file trên đĩa đã sai. Lần restart sau, khi không còn worker cũ để níu, Nginx có thể không lên. Test trước để đừng để một file hỏng ngồi chờ lần khởi động kế tiếp.

### networking-nginx-reverse-proxy-bind
kind: decide
cue: Nginx đã nhận traffic public thì port của app chỉ để Nginx với tới, loopback hoặc mạng nội bộ. Mở port app ra Internet không thay được reverse proxy.

Q: Nginx đã là cửa public của site. App vẫn nghe port 3000. Port đó nên bind ở đâu, và mở 3000 ra Internet có thay được việc đã cấu hình proxy không?

A: Bind ở địa chỉ mà chỉ Nginx với tới được: loopback nếu cùng máy, hoặc một địa chỉ nội bộ nếu khác máy. Người dùng vẫn vào Nginx. Nginx mới nối sang 3000. Mở 3000 ra Internet tạo một đường thứ hai, không qua các rule, header và TLS bạn đã đặt ở proxy. Reverse proxy không còn là điểm vào duy nhất. Đừng mở port app ra ngoài chỉ vì "cho chắc là tới được".

### networking-nginx-reverse-proxy-upstream
kind: command
cue: Nhiều backend: khối `upstream` + `proxy_pass http://tên;`. Không ghi `weight` thì round-robin đều.

Q: Hai process app nghe `3001` và `3002`. Nginx phải chia request. Khai nhóm ở đâu, `proxy_pass` viết thế nào, và mặc định chia ra sao nếu không ghi `weight`?

A: Khai `upstream tên { server 127.0.0.1:3001; server 127.0.0.1:3002; }` ngoài khối `server`. Trong `location`, `proxy_pass http://tên;` — đúng tên nhóm, không nhét từng IP vào `location`. Mặc định weighted round-robin; không ghi `weight` thì mỗi backend bằng nhau. `weight=2` nhận gấp đôi lần mặc định 1. Sai tên nhóm thì Nginx không resolve được upstream.

### networking-nginx-reverse-proxy-fastcgi-unix
kind: distinguish
cue: PHP-FPM: `fastcgi_pass unix:/path;`, không `proxy_pass`. Path phải trùng `listen` của pool. Sai socket → 502.

Q: Nginx phục vụ PHP qua PHP-FPM. Dùng `proxy_pass` được không, directive nào trỏ socket Unix, và sai path socket user thấy mã nào?

A: Không dùng `proxy_pass`. FPM nói FastCGI, không phải HTTP. Dùng `fastcgi_pass unix:/run/php/php-fpm.sock;` — path phải trùng file `listen` trong pool FPM. Docs cũng cho `fastcgi_pass localhost:9000;` nếu FPM nghe TCP. Sai path, socket không tồn tại, hoặc FPM down: Nginx trả 502 Bad Gateway, không phải 404. 404 là thiếu file PHP. 502 là không nói được với FPM.

## Lỗi thường gặp

**Thêm dấu `/` vào cuối `proxy_pass` cho đẹp.** Path mà app nhận bị đổi, và các route sâu bắt đầu trả `404`. Nếu app tự xử lý route, dùng dạng không có path: `proxy_pass http://127.0.0.1:3000;`.

**Reload khi file config vừa sửa bị sai cú pháp.** Nginx vẫn giữ worker cũ nếu config mới không nạp được, nhưng lần restart sau có thể không lên. Luôn chạy `nginx -t` trước.

**Tin `X-Forwarded-For` từ mọi connection.** Chỉ lấy IP client từ header khi request đến từ proxy đã biết. Trong app, hãy khai báo cụ thể danh sách trusted proxy, đừng tin mọi địa chỉ.

**`proxy_pass` vào PHP-FPM.** FPM không phải HTTP upstream. Dùng `fastcgi_pass`. Sai socket → 502.

**Viết IP từng backend trong nhiều `location` thay vì `upstream`.** Đổi port phải sửa nhiều chỗ. Một khối `upstream` + `proxy_pass http://tên;`.

## Nguồn

1. [KodeKloud Engineer — 100 Days of DevOps](https://engineer.kodekloud.com/practice) — đề cương ngày Install and Configure Nginx as LBR, Configure Nginx + PHP-FPM Using Unix Sock. Trang practice cần đăng nhập. Bài này viết lại lệnh, không chép lab.
2. [ngx_http_proxy_module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) — Nginx, `proxy_pass` và `proxy_set_header`.
3. [ngx_http_core_module](https://nginx.org/en/docs/http/ngx_http_core_module.html) — `server`, `listen`, `location`, `server_name`.
4. [ngx_http_upstream_module](https://nginx.org/en/docs/http/ngx_http_upstream_module.html) — nhóm backend, `weight`, round-robin.
5. [ngx_http_fastcgi_module](https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html) — `fastcgi_pass`, kể cả `unix:`.
6. [ngx_http_ssl_module](https://nginx.org/en/docs/http/ngx_http_ssl_module.html) — TLS termination.

## Tiếp theo

[Apache virtual host](../webserver/01-apache.md)
