jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Nginx from Zero to Production · Part 2 — The Configuration Model

The grammar behind every Nginx config: directives and contexts (main/events/http/server/location), how location matching really works, try_files, virtual hosts with server_name, and the safe reload workflow — with exercises.

Phần 1 bạn đã cài Nginx và phục vụ một trang. Giờ ta học ngôn ngữ của config — vì 90% “lỗi Nginx” thực ra là “tôi chưa hiểu mô hình config”.

Nắm vững phần này thì bạn đọc được mọi config Nginx trên mạng.


1. Directive và context

Một config Nginx gồm hai thứ:

  • Directive — câu lệnh, kết thúc bằng ;. Ví dụ: listen 80;.
  • Context (block) — các phần { } nhóm directive lại và định nghĩa phạm vi.

Context lồng nhau như búp bê Nga:

main context  (the file itself — global settings)
│  worker_processes auto;

├── events { ... }          # connection-processing settings

└── http { ... }            # everything HTTP lives here
     │  include mime.types;
     │  gzip on;

     ├── server { ... }     # one virtual host (one site)
     │    │  listen 80;
     │    │  server_name example.com;
     │    │
     │    └── location / { ... }   # rules for a set of URLs

     └── server { ... }     # another site on the same Nginx

Quy tắc then chốt: context con kế thừa directive từ cha, và có thể ghi đè. Đặt gzip on; trong http thì mọi server kế thừa, trừ khi một server cụ thể nói khác.

Một config tối giản nhưng đầy đủ trông như sau:

# main context
worker_processes auto;          # one worker per CPU core

events {
    worker_connections 1024;    # max simultaneous connections per worker
}

http {
    include       mime.types;   # map file extensions → Content-Type
    default_type  application/octet-stream;

    server {
        listen 80;
        server_name localhost;

        location / {
            root  /var/www/lab;
            index index.html;
        }
    }
}

worker_processes × worker_connections xấp xỉ số kết nối đồng thời tối đa. auto worker × 1024 là quá đủ cho việc local.


2. Block server — virtual host

Một Nginx có thể host nhiều site. Mỗi block server là một virtual host. Nginx chọn server nào xử lý request bằng hai thứ: cổng/địa chỉ listen, và server_name khớp với Host header của request.

http {
    server {
        listen 80;
        server_name site-a.local;      # http://site-a.local
        root /var/www/site-a;
    }

    server {
        listen 80;
        server_name site-b.local;      # http://site-b.local
        root /var/www/site-b;
    }
}

Cả hai nghe ở cổng 80, nhưng Host header quyết định ai thắng. Test cục bộ bằng cách giả Host header:

curl -H 'Host: site-a.local' http://localhost   # → site-a
curl -H 'Host: site-b.local' http://localhost   # → site-b

Nếu không server_name nào khớp, Nginx dùng server mặc định cho cổng đó (cái đầu tiên, hoặc block đánh dấu listen 80 default_server;).


3. location — trái tim của routing

Bên trong server, block location quyết định làm gì với các đường dẫn URL khác nhau. Đây là nơi phần lớn logic config của bạn nằm.

Có vài kiểu khớp, và chúng có thứ tự ưu tiên nghiêm ngặt — không phải trên-xuống-dưới như bạn tưởng:

server {
    listen 80;
    server_name localhost;

    location = /health  { return 200 "ok\n"; }   # 1. EXACT match
    location ^~ /assets/ { root /var/www; }        # 2. PREFIX, stop regex
    location ~ \.php$    { return 403; }            # 3. REGEX (case-sensitive)
    location ~* \.(jpg|png)$ { expires 30d; }       # 3. REGEX (case-insensitive)
    location /          { root /var/www/lab; }      # 4. PREFIX (longest wins)
}

Thuật toán khớp Nginx thực sự chạy:

1. `location = /path`     exact match            → if hit, STOP. highest priority.
2. `location ^~ /prefix`  prefix, then STOP      → longest matching prefix; skip regex.
3. `location ~  regex`    regex (case-sensitive) → first matching regex IN FILE ORDER.
   `location ~* regex`    regex (case-insensitive)
4. `location /prefix`     plain prefix           → used only if no regex matched.
                                                   longest prefix wins.

Hai sự thật làm ai cũng bất ngờ:

  • Khớp chính xác (=) thắng tất cả — tuyệt cho /health hoặc /favicon.ico (khớp nhanh nhất có thể).
  • Regex được xét theo thứ tự trong file, còn prefix thường thì theo khớp dài nhất. Nên sắp xếp lại prefix thường không đổi gì, nhưng sắp xếp lại regex thì có.

Test nhanh:

curl http://localhost/health      # → ok   (exact match wins)
curl -I http://localhost/cat.jpg  # → has "Expires" header (regex ~* matched)

4. root vs alias — cái bẫy kinh điển

Cả hai ánh xạ URL tới đĩa, nhưng chúng ghép đường dẫn khác nhau. Đây là lỗi file tĩnh phổ biến nhất.

# root: the location path is APPENDED to root
location /assets/ {
    root /var/www;        # request /assets/app.js → /var/www/assets/app.js
}

# alias: the location path is REPLACED by alias
location /assets/ {
    alias /var/www/static/;   # request /assets/app.js → /var/www/static/app.js
}

Mẹo nhớ: root cộng thêm, alias thay thế. Với alias, luôn kết thúc cả location lẫn alias bằng / để tránh bất ngờ.


5. try_files — phục vụ file, dự phòng mượt mà

try_files thử một danh sách đường dẫn theo thứ tự và dùng cái đầu tiên tồn tại. Nó thiết yếu cho hai tình huống hằng ngày.

Site tĩnh với trang 404 tuỳ chỉnh:

location / {
    root /var/www/lab;
    try_files $uri $uri/ =404;
    # 1) try the exact file ($uri)
    # 2) try it as a directory ($uri/)
    # 3) otherwise return 404
}

Single Page App (SPA React/Vue/Astro) — mọi route lạ phải dự phòng về index.html để router phía client xử lý:

location / {
    root /var/www/spa;
    try_files $uri $uri/ /index.html;   # the magic line for SPAs
}

Thiếu dòng cuối đó, refresh /dashboard trong SPA sẽ 404 — vì không có file dashboard trên đĩa. try_files viết lại nó thành index.html, và router JS tiếp quản.


6. MIME type & default_type

Trình duyệt quyết định xử lý response thế nào qua Content-Type header. Nginx đặt nó từ phần mở rộng file dùng bản đồ mime.types:

http {
    include       mime.types;                 # .css → text/css, .js → text/javascript ...
    default_type  application/octet-stream;   # fallback for unknown extensions
}

Quên include mime.types; thì CSS của bạn về dưới dạng text/plain — trình duyệt từ chối áp dụng, và lỗi “mất style” của bạn chẳng liên quan gì tới chính file CSS.


7. Quy trình sửa-và-reload an toàn

Lặp vòng này cho mọi thay đổi trong phần còn lại của series:

# 1. Edit a config file
# 2. ALWAYS validate first — catches typos before they go live
sudo nginx -t
#    nginx: configuration file /etc/nginx/nginx.conf test is successful

# 3. Apply with a graceful reload (no dropped connections)
sudo nginx -s reload

# 4. If a request misbehaves, read the error log
sudo tail -f /var/log/nginx/error.log

Tại sao reload mà không restart? reload giữ kết nối hiện có sống trong khi đổi config; restart ngắt mọi thứ trong chốc lát. Mặc định dùng reload.


8. Tóm tắt

  • Config = directive (;) bên trong context ({ }); con kế thừa từ cha.
  • Block servervirtual host, chọn bằng listen + server_name.
  • Khớp location có ưu tiên nghiêm ngặt: chính xác → prefix ^~ → regex (thứ tự file) → prefix thường (dài nhất).
  • root cộng, alias thay.
  • try_files $uri $uri/ /index.html; là phao cứu sinh cho SPA.

Tiếp — Phần 3: reverse proxy & load balancing. Ta sẽ đặt Nginx trước một app backend thật.


Bài tập

  1. Hai site, một Nginx: tạo hai block server (a.local, b.local) ở cổng 80 với root khác nhau; xác minh từng cái bằng curl -H 'Host: ...'.
  2. Đoán kết quả khớp: với các block location ở §3, đoán cái nào xử lý /health, /assets/app.js, /photo.JPG, và /about — rồi test cả bốn.
  3. root vs alias: phục vụ /assets/app.js từ một thư mục bằng root, rồi viết lại bằng alias trỏ tới thư mục tên khác.
  4. Dự phòng SPA: build một SPA bất kỳ (hoặc giả bằng file route rỗng), cấu hình try_files, và xác nhận refresh một route sâu phục vụ index.html thay vì 404.
  5. Làm hỏng MIME: comment dòng include mime.types;, reload, và quan sát CSS về dưới dạng text/plain trong DevTools.
  6. Nâng cao: thêm location = /health { return 200 "ok\n"; } và xác nhận nó phản hồi mà không chạm đĩa.