jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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 ForbiddenWrong 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 routeMissing try_files ... /index.html {Thiếu try_files ... /index.html}
502 Bad GatewayBackend down / wrong proxy_pass host:port {Backend chết / sai host:port proxy_pass}
504 Gateway TimeoutBackend too slow → raise proxy_read_timeout {Backend quá chậm → tăng proxy_read_timeout}
413 Request Entity Too Largeclient_max_body_size too small {client_max_body_size quá nhỏ}
CSS arrives as text/plainMissing include mime.types; {Thiếu include mime.types;}
WebSocket connects then dropsMissing Upgrade/Connection headers {Thiếu header Upgrade/Connection}
Change had no effectEdited 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}:

  1. Two backend instances behind an upstream with least_conn and passive health checks {Hai instance backend sau một upstream với least_conn và health check bị động}.
  2. / serves a built SPA with try_files fallback {/ phục vụ một SPA đã build với dự phòng try_files}.
  3. /api/ is proxied, cached 30s with X-Cache-Status, and rate-limited {/api/ được proxy, cache 30s với X-Cache-Status, và giới hạn tốc độ}.
  4. HTTPS with HTTP→HTTPS redirect and the shared header snippets {HTTPS với redirect HTTP→HTTPS và các snippet header dùng chung}.
  5. The whole stack runs via docker compose up {Toàn bộ stack chạy bằng docker 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, location matching, try_files, virtual hosts {Phần 2 — mô hình config: context, khớp location, 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}

  1. Assemble the config {Lắp ráp config}: build the full server block from §1 with the two include snippets and confirm nginx -t passes {xây block server đầy đủ ở §1 với hai snippet include và xác nhận nginx -t pass}.
  2. Dump the truth {Trút sự thật}: run nginx -T and find your proxy_cache_path line in the merged output {chạy nginx -T và tìm dòng proxy_cache_path của bạn trong output đã gộp}.
  3. Dockerize {Đóng gói Docker}: write the docker-compose.yml, mount your config :ro, and reload with docker compose exec nginx nginx -s reload {viết docker-compose.yml, mount config :ro, và reload bằng docker compose exec nginx nginx -s reload}.
  4. Force a 502 {Ép một lỗi 502}: point proxy_pass at a dead port, reproduce the 502, then read the error log line that explains it {trỏ proxy_pass tới một cổng chết, tái hiện 502, rồi đọc dòng error log giải thích nó}.
  5. Force a 403 {Ép một lỗi 403}: chmod 000 your index.html, reproduce the 403, and confirm the cause in the error log {chmod 000 file index.html, tái hiện 403, và xác nhận nguyên nhân trong error log}.
  6. 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}.