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.

In Part 1 you installed Nginx and served a page {Ở Phần 1 bạn đã cài Nginx và phục vụ một trang}. Now we learn the language of its config — because 90% of “Nginx problems” are really “I didn’t understand the config model” problems {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”}.

Master this part and you can read any Nginx config on the internet {Nắm vững phần này thì bạn đọc được mọi config Nginx trên mạng}.


1. Directives and contexts {Directive và context}

An Nginx config is made of two things {Một config Nginx gồm hai thứ}:

  • Directives — instructions, ending with ; {Directive — câu lệnh, kết thúc bằng ;}. Example: listen 80; {Ví dụ: listen 80;}.
  • Contexts (blocks) — { } sections that group directives and define scope {Context (block) — các phần { } nhóm directive lại và định nghĩa phạm vi}.

Contexts nest like Russian dolls {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

The key rule {Quy tắc then chốt}: child contexts inherit directives from their parent, and can override them {context con kế thừa directive từ cha, và có thể ghi đè}. Set gzip on; in http and every server inherits it, unless a specific server says otherwise {Đặt gzip on; trong http thì mọi server kế thừa, trừ khi một server cụ thể nói khác}.

A minimal but complete config looks like this {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 is roughly your max concurrent connections {worker_processes × worker_connections xấp xỉ số kết nối đồng thời tối đa}. auto workers × 1024 is plenty for local work {auto worker × 1024 là quá đủ cho việc local}.


2. server blocks — virtual hosts {Block server — virtual host}

One Nginx can host many sites {Một Nginx có thể host nhiều site}. Each server block is a virtual host {Mỗi block server là một virtual host}. Nginx picks which server handles a request using two things {Nginx chọn server nào xử lý request bằng hai thứ}: the listen port/address, and the server_name matched against the request’s Host header {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;
    }
}

Both listen on port 80, but the Host header decides the winner {Cả hai nghe ở cổng 80, nhưng Host header quyết định ai thắng}. Test it locally by faking the header {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

If no server_name matches, Nginx uses the default server for that port (the first one, or the block marked listen 80 default_server;) {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 — the heart of routing {location — trái tim của routing}

Inside a server, location blocks decide what to do with different URL paths {Bên trong server, block location quyết định làm gì với các đường dẫn URL khác nhau}. This is where most of your config logic lives {Đây là nơi phần lớn logic config của bạn nằm}.

There are several match types, and they have a strict priority order — not top-to-bottom like you’d expect {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)
}

The matching algorithm Nginx actually runs {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.

The two facts that surprise everyone {Hai sự thật làm ai cũng bất ngờ}:

  • Exact (=) beats everything — great for /health or /favicon.ico (fastest possible match) {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 is checked in file order, but plain prefixes by longest match {Regex được xét theo thứ tự trong file, còn prefix thường thì theo khớp dài nhất}. So reordering plain prefixes doesn’t matter, but reordering regex does {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ó}.

Quick test {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 — the classic trap {root vs alias — cái bẫy kinh điển}

Both map a URL to disk, but they combine paths differently {Cả hai ánh xạ URL tới đĩa, nhưng chúng ghép đường dẫn khác nhau}. This is the single most common static-file bug {Đâ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
}

Mnemonic {Mẹo nhớ}: root adds, alias replaces {root cộng thêm, alias thay thế}. With alias, always end both the location and the alias with / to avoid surprises {Với alias, luôn kết thúc cả location lẫn alias bằng / để tránh bất ngờ}.


5. try_files — serve files, fall back gracefully {try_files — phục vụ file, dự phòng mượt mà}

try_files tries a list of paths in order and uses the first one that exists {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}. It’s essential for two everyday cases {Nó thiết yếu cho hai tình huống hằng ngày}.

Static site with a custom 404 {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 (React/Vue/Astro SPA) — every unknown route must fall back to index.html so the client router can handle it {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
}

Without that last line, refreshing /dashboard in a SPA gives a 404 — because there’s no dashboard file on disk {Thiếu dòng cuối đó, refresh /dashboard trong SPA sẽ 404 — vì không có file dashboard trên đĩa}. try_files rewrites it to index.html, and the JS router takes over {try_files viết lại nó thành index.html, và router JS tiếp quản}.


6. MIME types & default_type {MIME type & default_type}

Browsers decide how to handle a response by its Content-Type header {Trình duyệt quyết định xử lý response thế nào qua Content-Type header}. Nginx sets it from the file extension using the mime.types map {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
}

Forget include mime.types; and your CSS arrives as text/plain — the browser refuses to apply it, and your “broken styling” bug has nothing to do with the CSS itself {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. The safe edit-and-reload workflow {Quy trình sửa-và-reload an toàn}

Repeat this loop for every change in the rest of the series {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

Why reload and not restart? {Tại sao reload mà không restart?} reload keeps existing connections alive while swapping config; restart drops everything for a moment {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}. Use reload by default {Mặc định dùng reload}.


8. Recap {Tóm tắt}

  • Config = directives (;) inside contexts ({ }); children inherit from parents {Config = directive (;) bên trong context ({ }); con kế thừa từ cha}.
  • A server block is a virtual host, chosen by listen + server_name {Block servervirtual host, chọn bằng listen + server_name}.
  • location matching has a strict priority: exact → ^~ prefix → regex (file order) → plain prefix (longest) {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 adds, alias replaces {root cộng, alias thay}.
  • try_files $uri $uri/ /index.html; is the SPA lifesaver {try_files $uri $uri/ /index.html; là phao cứu sinh cho SPA}.

Next — Part 3: reverse proxy & load balancing {Tiếp — Phần 3: reverse proxy & load balancing}. We’ll put Nginx in front of a real backend app {Ta sẽ đặt Nginx trước một app backend thật}.


Exercises {Bài tập}

  1. Two sites, one Nginx {Hai site, một Nginx}: create two server blocks (a.local, b.local) on port 80 with different roots; verify each with curl -H 'Host: ...' {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. Predict the match {Đoán kết quả khớp}: given the location blocks in §3, predict which one handles /health, /assets/app.js, /photo.JPG, and /about — then test all four {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 {root vs alias}: serve /assets/app.js from a folder using root, then rewrite it with alias to point at a differently-named folder {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. SPA fallback {Dự phòng SPA}: build any SPA (or fake it with empty route files), configure try_files, and confirm that refreshing a deep route serves index.html instead of 404 {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. Break MIME {Làm hỏng MIME}: comment out include mime.types;, reload, and observe your CSS arrive as text/plain in DevTools {comment dòng include mime.types;, reload, và quan sát CSS về dưới dạng text/plain trong DevTools}.
  6. Stretch {Nâng cao}: add location = /health { return 200 "ok\n"; } and confirm it responds without touching the disk {thêm location = /health { return 200 "ok\n"; } và xác nhận nó phản hồi mà không chạm đĩa}.