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_connectionsis roughly your max concurrent connections {worker_processes × worker_connectionsxấp xỉ số kết nối đồng thời tối đa}.autoworkers × 1024 is plenty for local work {autoworker × 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/healthor/favicon.ico(fastest possible match) {Khớp chính xác (=) thắng tất cả — tuyệt cho/healthhoặ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
serverblock is a virtual host, chosen bylisten+server_name{Blockserverlà virtual host, chọn bằnglisten+server_name}. locationmatching has a strict priority: exact →^~prefix → regex (file order) → plain prefix (longest) {Khớplocationcó ưu tiên nghiêm ngặt: chính xác → prefix^~→ regex (thứ tự file) → prefix thường (dài nhất)}.rootadds,aliasreplaces {rootcộng,aliasthay}.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}
- Two sites, one Nginx {Hai site, một Nginx}: create two
serverblocks (a.local,b.local) on port 80 with different roots; verify each withcurl -H 'Host: ...'{tạo hai blockserver(a.local,b.local) ở cổng 80 với root khác nhau; xác minh từng cái bằngcurl -H 'Host: ...'}. - Predict the match {Đoán kết quả khớp}: given the
locationblocks in §3, predict which one handles/health,/assets/app.js,/photo.JPG, and/about— then test all four {với các blocklocationở §3, đoán cái nào xử lý/health,/assets/app.js,/photo.JPG, và/about— rồi test cả bốn}. - root vs alias {root vs alias}: serve
/assets/app.jsfrom a folder usingroot, then rewrite it withaliasto point at a differently-named folder {phục vụ/assets/app.jstừ một thư mục bằngroot, rồi viết lại bằngaliastrỏ tới thư mục tên khác}. - 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 servesindex.htmlinstead of 404 {build một SPA bất kỳ (hoặc giả bằng file route rỗng), cấu hìnhtry_files, và xác nhận refresh một route sâu phục vụindex.htmlthay vì 404}. - Break MIME {Làm hỏng MIME}: comment out
include mime.types;, reload, and observe your CSS arrive astext/plainin DevTools {comment dònginclude mime.types;, reload, và quan sát CSS về dưới dạngtext/plaintrong DevTools}. - Stretch {Nâng cao}: add
location = /health { return 200 "ok\n"; }and confirm it responds without touching the disk {thêmlocation = /health { return 200 "ok\n"; }và xác nhận nó phản hồi mà không chạm đĩa}.