Nginx from Zero to Production · Part 5 — Production Config & Debugging
Tie it together: a complete real-world config (SPA + API proxy + cache + TLS), reusable includes, Docker & Compose deploy, worker tuning, a debugging cheat-sheet for the errors you will actually hit, and a capstone project.
You’ve learned the pieces {Bạn đã học từng mảnh}: the config model, reverse proxy, load balancing, TLS, caching, rate limiting {mô hình config, reverse proxy, cân bằng tải, TLS, caching, giới hạn tốc độ}. This final part assembles them into one production setup, ships it with Docker, and teaches you to debug Nginx when (not if) something breaks {Phần cuối này ghép chúng thành một setup production, ship bằng Docker, và dạy bạn debug Nginx khi (không phải nếu) có gì hỏng}.
1. A complete production config {Một config production hoàn chỉnh}
This is the canonical full-stack layout: static SPA at /, API proxied at /api, cached, over HTTPS {Đây là bố cục full-stack kinh điển: SPA tĩnh ở /, API proxy ở /api, có cache, qua HTTPS}.
First, two reusable snippets so we don’t repeat ourselves {Trước tiên, hai snippet tái dùng để khỏi lặp lại}:
# /etc/nginx/snippets/proxy.conf — shared proxy headers
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;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
# /etc/nginx/snippets/security.conf — shared security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Now the site, reading top to bottom {Giờ là site, đọc từ trên xuống}:
# http context
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m
max_size=1g inactive=60m;
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
upstream app_pool {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=10s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=10s;
}
# Redirect all HTTP → HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/nginx/certs/local.crt;
ssl_certificate_key /etc/nginx/certs/local.key;
ssl_protocols TLSv1.2 TLSv1.3;
include snippets/security.conf;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
root /var/www/spa; # built SPA files (index.html + /assets)
# 1) Long-cache fingerprinted assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# 2) API → proxy to the backend pool, cached + rate-limited
location /api/ {
limit_req zone=api burst=40 nodelay;
include snippets/proxy.conf;
proxy_pass http://app_pool;
proxy_cache api_cache;
proxy_cache_valid 200 30s;
proxy_cache_use_stale error timeout updating;
proxy_cache_bypass $cookie_session; # never cache logged-in users
add_header X-Cache-Status $upstream_cache_status always;
}
# 3) Everything else → SPA, with client-side routing fallback
location / {
try_files $uri $uri/ /index.html;
}
}
Every directive here came from Parts 1–4 {Mọi directive ở đây đến từ Phần 1–4}. That’s the whole point — production config is just the basics, composed {Đó chính là ý chính — config production chỉ là những thứ cơ bản, ghép lại}.
2. Deploy with Docker & Compose {Triển khai bằng Docker & Compose}
In production you rarely install Nginx by hand — you run it as a container {Trên production bạn hiếm khi cài Nginx bằng tay — bạn chạy nó như một container}. The official nginx image reads anything you drop into /etc/nginx/conf.d/ {Image nginx chính thức đọc mọi thứ bạn thả vào /etc/nginx/conf.d/}.
# docker-compose.yml — Nginx in front of two app replicas
services:
app:
build: ./app
deploy:
replicas: 2 # two backend instances for the upstream pool
expose:
- "3000" # internal only — NOT published to the host
nginx:
image: nginx:1.27-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro # your site configs
- ./nginx/certs:/etc/nginx/certs:ro # TLS certs
- ./spa/dist:/var/www/spa:ro # built SPA
depends_on:
- app
Inside the Docker network, services reach each other by name, so the upstream points at app:3000 instead of an IP {Bên trong mạng Docker, các service gọi nhau bằng tên, nên upstream trỏ tới app:3000 thay vì IP}:
upstream app_pool {
server app:3000; # Docker DNS resolves "app" to the replicas
}
Bring it up and reload config without restarting the container {Khởi động và reload config mà không restart container}:
docker compose up -d
docker compose exec nginx nginx -t # validate inside the container
docker compose exec nginx nginx -s reload # graceful reload
Mount configs read-only (
:ro) and reload rather than rebuild — config changes shouldn’t require a new image {Mount config chỉ-đọc (:ro) và reload thay vì rebuild — đổi config không nên cần image mới}.
3. Tuning workers & connections {Tinh chỉnh worker & kết nối}
The defaults are fine for most sites; tune only with numbers in hand {Mặc định ổn cho hầu hết site; chỉ tinh chỉnh khi có số liệu trong tay}:
worker_processes auto; # = number of CPU cores (don't overthink it)
events {
worker_connections 4096; # max connections PER worker
multi_accept on; # accept many new connections at once
}
http {
keepalive_timeout 65; # reuse client connections (saves handshakes)
sendfile on; # kernel-level file sending (fast static files)
tcp_nopush on; # send headers + file in fewer packets
}
Capacity rule of thumb {Quy tắc ước lượng dung lượng}: worker_processes × worker_connections ≈ max simultaneous connections {worker_processes × worker_connections ≈ số kết nối đồng thời tối đa}. With 4 cores × 4096 you can hold ~16k connections — plenty before you’d need to scale out {Với 4 core × 4096 bạn giữ được ~16k kết nối — quá đủ trước khi cần scale ngang}.
Also raise the OS file-descriptor limit {Cũng nâng giới hạn file-descriptor của OS}: each connection is a file descriptor, so ulimit -n must exceed your connection target {mỗi kết nối là một file descriptor, nên ulimit -n phải lớn hơn mục tiêu kết nối}.
4. Debugging cheat-sheet {Cheat-sheet debug}
When something breaks, work in this order {Khi có gì hỏng, làm theo thứ tự này}.
Step 1 — validate the config {Bước 1 — kiểm tra config}:
nginx -t # always start here; reports the exact file + line of a syntax error
nginx -T # dump the FULL effective config (all includes merged) — great for "is my change even loaded?"
Step 2 — read the error log {Bước 2 — đọc error log}:
tail -f /var/log/nginx/error.log
# raise verbosity temporarily if needed:
error_log /var/log/nginx/error.log debug; # then reload
Step 3 — match the symptom to the cause {Bước 3 — khớp triệu chứng với nguyên nhân}:
| Symptom {Triệu chứng} | Usual cause {Nguyên nhân thường gặp} |
|---|---|
| 403 Forbidden | Wrong root, missing index, or file permissions (worker user can’t read) {Sai root, thiếu index, hoặc quyền file (worker không đọc được)} |
| 404 on a SPA route | Missing try_files ... /index.html {Thiếu try_files ... /index.html} |
| 502 Bad Gateway | Backend down / wrong proxy_pass host:port {Backend chết / sai host:port proxy_pass} |
| 504 Gateway Timeout | Backend too slow → raise proxy_read_timeout {Backend quá chậm → tăng proxy_read_timeout} |
| 413 Request Entity Too Large | client_max_body_size too small {client_max_body_size quá nhỏ} |
| CSS arrives as text/plain | Missing include mime.types; {Thiếu include mime.types;} |
| WebSocket connects then drops | Missing Upgrade/Connection headers {Thiếu header Upgrade/Connection} |
| Change had no effect | Edited the wrong file, or forgot nginx -s reload {Sửa nhầm file, hoặc quên nginx -s reload} |
The 502 you’ll meet most {Lỗi 502 bạn gặp nhiều nhất}: it almost always means Nginx is fine, the backend isn’t {nó gần như luôn nghĩa là Nginx ổn, backend mới có vấn đề}. Check the backend is running and that proxy_pass points at the right address {Kiểm tra backend đang chạy và proxy_pass trỏ đúng địa chỉ}:
curl -v http://127.0.0.1:3000/ # can NGINX'S host reach the backend directly?
# in Docker, exec INTO the nginx container and curl the service name:
docker compose exec nginx wget -qO- http://app:3000/
5. Capstone project {Dự án tổng kết}
Build the whole thing yourself {Tự xây toàn bộ}. This proves you can do real Nginx work {Cái này chứng minh bạn làm được việc Nginx thật}.
Goal {Mục tiêu}: a single HTTPS domain that serves a SPA, proxies an API to two backend replicas with caching + rate limiting, and survives a backend going down {một domain HTTPS duy nhất phục vụ SPA, proxy API tới hai replica backend có cache + giới hạn tốc độ, và sống sót khi một backend chết}.
Requirements {Yêu cầu}:
- Two backend instances behind an
upstreamwithleast_connand passive health checks {Hai instance backend sau mộtupstreamvớileast_connvà health check bị động}. /serves a built SPA withtry_filesfallback {/phục vụ một SPA đã build với dự phòngtry_files}./api/is proxied, cached 30s withX-Cache-Status, and rate-limited {/api/được proxy, cache 30s vớiX-Cache-Status, và giới hạn tốc độ}.- HTTPS with HTTP→HTTPS redirect and the shared header snippets {HTTPS với redirect HTTP→HTTPS và các snippet header dùng chung}.
- The whole stack runs via
docker compose up{Toàn bộ stack chạy bằngdocker compose up}.
Acceptance tests {Bài kiểm tra nghiệm thu}:
curl -kI https://localhost/ # HTTP/2 200, security headers present
curl -kI http://localhost/ # 301 → https
curl -k https://localhost/api/ping # rotates between the two replicas
curl -kI https://localhost/api/ping # X-Cache-Status: HIT on the 2nd call
# kill one replica → API still responds (failover)
# blast /api/ in a loop → eventually 503 (rate limit works)
If all of these pass, you can configure Nginx for production {Nếu tất cả pass, bạn có thể cấu hình Nginx cho production}.
6. Series recap {Tóm tắt series}
- Part 1 — what Nginx is, the worker model, install, first static site {Phần 1 — Nginx là gì, mô hình worker, cài đặt, site tĩnh đầu tiên}.
- Part 2 — the config model: contexts,
locationmatching,try_files, virtual hosts {Phần 2 — mô hình config: context, khớplocation,try_files, virtual host}. - Part 3 — reverse proxy, forwarded headers, upstreams, load balancing, WebSockets {Phần 3 — reverse proxy, header chuyển tiếp, upstream, cân bằng tải, WebSocket}.
- Part 4 — TLS/HTTP2, compression, caching, rate limiting, security headers {Phần 4 — TLS/HTTP2, nén, caching, giới hạn tốc độ, header bảo mật}.
- Part 5 — composing it all, Docker deploy, tuning, debugging, capstone {Phần 5 — ghép tất cả, deploy Docker, tinh chỉnh, debug, dự án tổng kết}.
You started not knowing where nginx.conf lived {Bạn bắt đầu khi còn không biết nginx.conf nằm đâu}. You can now stand up a secure, cached, load-balanced reverse proxy and debug it under fire {Giờ bạn dựng được một reverse proxy bảo mật, có cache, cân bằng tải và debug nó giữa lúc dầu sôi lửa bỏng}. That’s production Nginx {Đó là Nginx production}.
Exercises {Bài tập}
- Assemble the config {Lắp ráp config}: build the full
serverblock from §1 with the twoincludesnippets and confirmnginx -tpasses {xây blockserverđầy đủ ở §1 với hai snippetincludevà xác nhậnnginx -tpass}. - Dump the truth {Trút sự thật}: run
nginx -Tand find yourproxy_cache_pathline in the merged output {chạynginx -Tvà tìm dòngproxy_cache_pathcủa bạn trong output đã gộp}. - Dockerize {Đóng gói Docker}: write the
docker-compose.yml, mount your config:ro, and reload withdocker compose exec nginx nginx -s reload{viếtdocker-compose.yml, mount config:ro, và reload bằngdocker compose exec nginx nginx -s reload}. - Force a 502 {Ép một lỗi 502}: point
proxy_passat a dead port, reproduce the 502, then read the error log line that explains it {trỏproxy_passtới một cổng chết, tái hiện 502, rồi đọc dòng error log giải thích nó}. - Force a 403 {Ép một lỗi 403}:
chmod 000yourindex.html, reproduce the 403, and confirm the cause in the error log {chmod 000fileindex.html, tái hiện 403, và xác nhận nguyên nhân trong error log}. - Capstone {Dự án tổng kết}: complete the project in §5 and pass every acceptance test {hoàn thành dự án ở §5 và pass mọi bài kiểm tra nghiệm thu}.