Nginx vs Caddy vs Traefik: Chọn proxy nào?
So sánh Nginx, Caddy và Traefik trên một VPS: TLS, cấu hình mỗi app, WebSocket và routing Docker khác nhau ra sao khi dùng chung một IP public?
Nginx và Caddy và Traefik: câu trả lời ngắn gọn
Nginx, Caddy và Traefik đều làm cùng một việc với vai trò reverse proxy: lắng nghe trên cổng 443, đọc hostname trong từng request rồi chuyển request đến đúng service trên VPS của bạn. Bất kỳ công cụ nào trong ba công cụ này cũng có thể đưa bốn ứng dụng tự host vào phía sau một địa chỉ IP public duy nhất. Cả ba đều đủ nhanh để ứng dụng của bạn mới là phần chậm hơn. Điểm khác nhau nằm ở cách từng công cụ lấy chứng chỉ TLS (transport layer security) và lượng cấu hình cần thêm cho mỗi ứng dụng mới. Khác biệt còn lại chỉ xuất hiện sau này, khi bạn cần một tính năng mà các tutorial phổ biến thường bỏ qua.
Chọn Caddy nếu bạn muốn Caddy tự xử lý HTTPS và các service của bạn là những web app thông thường. Chọn Traefik nếu mọi thứ chạy trong Docker Compose và bạn thêm service mới vài tuần một lần. Chọn Nginx nếu bạn đã chạy Nginx, hoặc nếu bạn cần cache response, chứng chỉ client, chuyển tiếp TCP thô hay một config hiện có lớn mà bạn không muốn viết lại.
Mỗi loại nhận chứng chỉ TLS như thế nào?
Đây là tiêu chí quyết định lựa chọn của hầu hết mọi người, nên hãy bắt đầu từ đây. Cả ba cuối cùng đều lưu cùng một chứng chỉ do cùng một cơ quan cấp. Nhưng các bước để có chứng chỉ không giống nhau.
Caddy yêu cầu chứng chỉ vì bạn đã khai báo hostname. Ghi app.example.com làm địa chỉ site, Caddy sẽ yêu cầu chứng chỉ qua ACME (automatic certificate management environment) từ Let's Encrypt, chuyển sang ZeroSSL nếu yêu cầu thất bại, phục vụ redirect từ HTTP sang HTTPS trên cổng 80 và tự gia hạn. Bạn không cần công cụ thứ hai hay timer để kiểm tra. Chứng chỉ nằm trong thư mục dữ liệu của user caddy, là /var/lib/caddy/.local/share/caddy khi cài bằng package. Vì vậy, hãy thêm đường dẫn này vào bản backup hoặc chấp nhận việc cấp chứng chỉ mới sau khi rebuild. Với hostname không public, tls internal ký bằng certificate authority cục bộ của chính Caddy. Kết quả tương đương với tạo chứng chỉ tự ký trên Ubuntu, nhưng việc gia hạn được tự động xử lý.
Nginx không có ACME client. Certbot lấy chứng chỉ, còn plugin --nginx sẽ sửa server block của bạn để thêm listener trên cổng 443 và redirect. Việc gia hạn chạy bằng systemd timer do package cài đặt, nên có hai thành phần cần hoạt động và hai việc cần kiểm tra: systemctl list-timers | grep certbot cho biết timer có tồn tại, còn sudo certbot renew --dry-run xác nhận quy trình gia hạn vẫn hoạt động. Các bước chi tiết có tại Certbot trên Ubuntu 24.04 với Nginx. Công cụ này cũng xử lý chứng chỉ wildcard qua thử thách DNS-01 khi bạn có nhiều subdomain hơn số lượng muốn liệt kê.
Traefik tích hợp ACME client riêng. Bạn cấu hình một certificate resolver trong static configuration, sau đó mọi router đều có thể sử dụng resolver này. Toàn bộ state, bao gồm account key và chứng chỉ, nằm trong một file acme.json duy nhất. Traefik từ chối sử dụng file này nếu bất kỳ user nào ngoài owner có quyền đọc, đồng thời thông báo lỗi trước khi loại bỏ resolver:
The ACME resolver "le" is skipped from the resolvers list because: unable to get ACME account: permissions 660 for /letsencrypt/acme.json are too open, please use 600Hãy mount một thư mục và để Traefik tự tạo file. Tạo thư mục trước bằng touch để file kế thừa umask của bạn. Đây là cách hầu hết mọi người xử lý yêu cầu đó.
Có một điểm giống nhau ở cả ba loại. Thử thách HTTP-01 yêu cầu cổng 80 có thể truy cập từ Internet, vì certificate authority sẽ kết nối ngược vào cổng này. Nếu chỉ mở cổng 443, việc cấp chứng chỉ sẽ thất bại theo cách dễ khiến bạn nghĩ lỗi nằm ở DNS.
Cùng một bài toán định tuyến 2 ứng dụng trong 3 cấu hình
Bài toán: app.example.com đi đến một service trên 127.0.0.1:8080, còn files.example.com đi đến một service trên 127.0.0.1:8081; cả hai đều dùng HTTPS. Dưới đây là toàn bộ cấu hình cho từng proxy, để thấy rõ mức độ dài ngắn thay vì chỉ khẳng định.
Nginx
# /etc/nginx/sites-available/app.example.com
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}Sau đó tạo liên kết, kiểm tra cấu hình, reload và thêm certificate.
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d app.example.comLệnh nginx -t in ra syntax is ok và test is successful là kiểm tra cần chạy trước mỗi lần reload. Ứng dụng thứ hai dùng cùng block này, chỉ thay hostname và port. Các dòng proxy_set_header không phải để trang trí: khi proxy_pass trỏ đến một địa chỉ, nginx mặc định gửi Host: 127.0.0.1:8080 đến backend. Vì vậy, ứng dụng tạo absolute URL từ Host header sẽ gửi người dùng đến localhost.
Caddy
app.example.com {
reverse_proxy 127.0.0.1:8080
}
files.example.com {
reverse_proxy 127.0.0.1:8081
}sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyĐó là toàn bộ file cấu hình. reverse_proxy tự đặt X-Forwarded-For, X-Forwarded-Proto và X-Forwarded-Host, đồng thời mặc định bỏ qua các giá trị client gửi trong những header đó. Vì vậy, request không thể giả mạo với backend về nguồn gửi request. Certificate, redirect từ port 80 và việc gia hạn đều được suy ra từ hai site address. Không cần thêm cấu hình nào khác trong file.
Traefik
Traefik cần static configuration trước khi có thể định tuyến. Khi chạy dưới dạng Compose service, với image tag hiện hành tính đến tháng 8 năm 2026:
services:
traefik:
image: traefik:v3.7
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.email=you@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencryptSau đó mỗi ứng dụng tự khai báo routing bằng labels trong compose file riêng:
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=le"
- "traefik.http.services.app.loadbalancer.server.port=8080"loadbalancer.server.port là port bên trong container, không phải published port, vì Traefik truy cập container qua Docker network dùng chung. Ứng dụng không cần dòng ports: nào cả. Đây là lợi ích chính: chỉ Traefik được publish ra ngoài. Toàn bộ cấu hình, gồm network dùng chung và redirect middleware, có trong định tuyến nhiều ứng dụng với Traefik và Docker Compose.
Mỗi ứng dụng bổ sung tốn bao nhiêu cấu hình?
The data behind this chart
[
{
"tool": "Nginx",
"proxy_setup_lines": 0,
"lines_per_app": 11
},
{
"tool": "Caddy",
"proxy_setup_lines": 0,
"lines_per_app": 3
},
{
"tool": "Traefik",
"proxy_setup_lines": 17,
"lines_per_app": 5
}
]Dựa trên số dòng trong các block ở trên. Nginx server block có 11 dòng không trống, và bạn phải viết lại block này cho từng hostname. Caddy site block có 3 dòng. Traefik cần 17 dòng cấu hình tĩnh trước khi phục vụ request đầu tiên, sau đó cần 5 label cho mỗi app.
Hãy xem xét sự đánh đổi, không chỉ xem công cụ nào thắng. Traefik tốn nhiều nhất trước khi có app đầu tiên và tốn ít nhất cho mỗi app tiếp theo. Hai tổng số gặp nhau ở khoảng site thứ ba. Nếu có ít hơn số đó, cấu hình tĩnh là phần overhead không cần thiết. Nếu có nhiều hơn, label bắt đầu có lợi thế và lợi thế tiếp tục tăng, vì rule định tuyến nằm cạnh service mà nó định tuyến. Khi xóa service, route của service đó cũng bị xóa theo. Đây là điểm mà file cấu hình tập trung xử lý kém: các server block cũ của những app đã ngừng tồn tại từ nhiều tháng trước.
Số dòng cũng khiến Nginx có vẻ đơn giản hơn thực tế. Mỗi block còn cần một symlink, một nginx -t, một lần reload và một lần chạy certbot. Với Caddy, bạn chỉ cần chỉnh sửa rồi reload một lần. Với Traefik, bạn không cần chạy command nào. Cả ba đều reload mà không ngắt các kết nối đang hoạt động. Khác biệt nằm ở số bước riêng biệt bạn phải nhớ vào lúc 1 giờ sáng.
Ai biết về các container của bạn?
Traefik theo dõi Docker socket và tạo router từ các label của container khi container khởi động hoặc dừng. Không thành phần nào khác trong số này làm được việc đó. Nginx và Caddy đều cần chỉnh sửa cấu hình rồi reload khi có container mới, đồng thời cần một địa chỉ mà chúng có thể truy cập: hoặc một cổng được publish trên loopback, hoặc một Docker network dùng chung có proxy kết nối vào.
Tính năng này có cái giá của nó, và cần nói rõ. Traefik đọc /var/run/docker.sock. Bất kỳ ai có thể truy cập socket đó đều có thể khởi động một container với filesystem của host được mount vào trong container, qua đó có quyền root trên host. Mount ở chế độ chỉ đọc giúp giảm rủi ro nhưng không loại bỏ rủi ro. Nếu điều này quan trọng với threat model của bạn, hãy đặt một socket proxy ở giữa và chỉ expose các endpoint liệt kê container mà Traefik cần.
Caddy có thể thực hiện service discovery dựa trên label thông qua một plugin do cộng đồng phát triển, nhưng plugin của Caddy được build cố định trong binary. Vì vậy, bạn phải build một binary tùy chỉnh hoặc một image tùy chỉnh với xcaddy, rồi tự chịu trách nhiệm quản lý bản build đó và các bản cập nhật của nó. Với ba hoặc bốn service, chỉnh sửa Caddyfile sẽ ít tốn công hơn.
WebSocket và streaming: lỗi gì xảy ra và vì sao
Nginx là thành phần cần cấu hình thêm. Một kết nối WebSocket bắt đầu bằng một HTTP request có chứa Upgrade: websocket, nhưng nginx không chuyển tiếp các hop-by-hop header lên upstream nếu bạn không chỉ định rõ.
# /etc/nginx/conf.d/upgrade-map.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}Sau đó, bên trong block location, phải có đủ 3 dòng sau:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;Nếu thiếu các dòng này, console của trình duyệt sẽ hiển thị WebSocket connection to 'wss://app.example.com/ws' failed, còn log của backend chỉ ghi nhận một GET thông thường. map tồn tại vì nếu hardcode Connection: upgrade thì header này sẽ được gửi trong mọi request, kể cả những request thông thường vốn phải ghi là close.
Hai mặc định khác của Nginx cũng có thể gây lỗi. proxy_read_timeout là 60 giây và áp dụng cho tunnel sau khi upgrade, nên một WebSocket không có traffic trong 1 phút sẽ bị proxy đóng. Server-sent events cũng đến trễ hoặc dồn thành từng đợt cho đến khi bạn đặt proxy_buffering off; trong location đó, vì nginx giữ response trong buffer trong khi trang đang chờ dữ liệu.
Caddy tự thực hiện upgrade và chuyển kết nối thành tunnel hai chiều mà không cần thêm directive nào. Caddy cũng flush ngay khi response là text/event-stream hoặc không có độ dài xác định, nên streaming hoạt động mà không cần cấu hình thêm. Traefik chuyển tiếp các kết nối đã upgrade và không buffer response, trừ khi bạn tự thêm middleware buffering. Nếu các service của bạn có chat, terminal web, log tail hoặc dashboard realtime, khác biệt này ảnh hưởng trực tiếp đến lượng cấu hình bạn phải viết và debug.
Block server Nginx đầy đủ, gồm cả WebSocket và SSE
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name app.example.com;
client_max_body_size 64m;
location / {
proxy_pass http://127.0.0.1:8080;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_buffering off;
}
}map phải được đặt trong context http, không phải bên trong server, nên hãy để directive này trong một file riêng dưới /etc/nginx/conf.d/. Chỉ tắt proxy_buffering ở những location có streaming, vì buffering giúp nginx giải phóng worker backend sớm đối với các response thông thường. Certbot sẽ ghi lại block này khi bạn chạy lệnh, vì vậy hãy đọc lại file sau đó.
Điều gì xảy ra khi bạn cần một tính năng đặc biệt?
Đây là lúc Nginx cho thấy số dòng cấu hình nhiều hơn là có lý do.
- Chứng chỉ client, còn gọi là mTLS (mutual TLS), trong đó client cũng phải cung cấp một chứng chỉ. Nginx yêu cầu
ssl_client_certificate /etc/ssl/ca.pem;vàssl_verify_client on;trong server block. Caddy yêu cầu một blockclient_authbên trongtls. Traefik không thể biểu diễn cấu hình này bằng labels: bạn phải định nghĩa một TLS option trong file provider rồi trỏ router đến option đó bằngtraefik.http.routers.app.tls.options=mtls@file. Mô hình mọi thứ đều khai báo bằng labels sẽ có ngoại lệ ngay lần đầu bạn cần tính năng này. - Upload lớn. Theo mặc định, Nginx giới hạn request body ở mức 1 MB. Upload lớn hơn sẽ trả về
413 Request Entity Too Large, còn error log ghiclient intended to send too large body. Tăng giá trịclient_max_body_size. Caddy và Traefik không đặt giới hạn body theo mặc định, nên request được chuyển đến app và giới hạn của chính app sẽ quyết định. - Cache response. Nginx có
proxy_cachevà tính năng này đã ổn định. Caddy cần plugin được build sẵn. Bản build open source của Traefik hoàn toàn không có HTTP cache. Điều này thường gây bất ngờ cho những người cho rằng proxy nào cũng cache. - TCP hoặc UDP thô, dùng cho cổng database hoặc game server. Nginx có module
stream. Traefik có TCP router và UDP router trên các entrypoint riêng. Caddy cần thêm một plugin, tức là cần thêm một custom build. - Một web server đã nằm phía sau proxy. Nếu service là một PHP application kiểu cổ điển, thì LAMP stack trên Ubuntu 24.04 đã bao gồm Apache. Đặt thêm proxy phía trước sẽ tạo ra hai nơi thiết lập header và hai nơi có thể rewrite URL. Hãy quyết định thành phần nào terminate TLS, sau đó giữ thành phần còn lại dùng HTTP thuần và bind vào loopback.
Bẫy firewall phát sinh từ lựa chọn này
Mục tiêu của reverse proxy là chỉ mở cổng 80 và 443. Docker âm thầm phá vỡ điều đó. Việc publish một cổng bằng -p 8080:80 sẽ ghi một rule DNAT vào bảng nat. Rule này được xử lý trước các rule INPUT do ufw quản lý, nên ufw deny 8080 không chặn được. Khi đó app của bạn nằm trên public Internet cùng với proxy mà bạn đã cấu hình cẩn thận. Hãy bind các cổng đã publish vào loopback bằng 127.0.0.1:8080:80, hoặc bỏ hẳn ports: và để proxy truy cập container qua Docker network, như ví dụ Traefik ở trên. Cơ chế và cách khắc phục được giải thích trong vì sao cổng Docker đã publish bỏ qua ufw.
Hãy kiểm tra từ một máy không phải VPS, vì kiểm tra ngay trên chính máy đó luôn thành công:
curl --max-time 5 http://your.server.address:8080Kết quả mong muốn là Connection refused hoặc timeout. Nếu nhận được HTTP response, app đó có thể được truy cập mà không đi qua proxy. Khi đó mọi cấu hình ở trên đều không có tác dụng.
Bạn nên chọn proxy nào?
Chủ yếu là static site, kèm một hoặc hai app: Caddy. HTTPS tự động loại bỏ công việc định kỳ lớn nhất. Cấu hình đủ ngắn để đọc trên một màn hình, và một static site chỉ cần một dòng root và một dòng file_server trong cùng một site block. Đổi lại, khi có lỗi bất thường, bạn sẽ có ít hướng dẫn copy-paste hơn để tham khảo.
Homelab dùng docker-compose và thường xuyên thêm service: Traefik. Khi quá 3 service, dùng label sẽ ít việc hơn sửa một file cấu hình trung tâm, và khi xóa một service thì route của service đó cũng bị xóa theo. Hãy dành một buổi chiều cho lần thiết lập đầu tiên, vì entrypoint, router, service và middleware đều là các thuật ngữ mới. Lỗi chính tả trong label thường xuất hiện dưới dạng lỗi 404 từ Traefik thay vì khiến Traefik không khởi động, vì vậy hãy đọc docker logs traefik để tìm lỗi parse trước khi cho rằng app bị hỏng.
Đã có cấu hình Nginx hoặc có bất kỳ yêu cầu nào trong danh sách trên: Nginx. Nginx đã có sẵn giải pháp cho response caching và client certificate, đồng thời hầu hết hướng dẫn của bên thứ ba đều giả định bạn dùng Nginx. Đổi lại, bạn phải tự cấu hình certificate và hỗ trợ websocket thay vì được cung cấp sẵn.
Một quy tắc áp dụng bất kể bạn chọn proxy nào. Chỉ một process được listen trên public interface. Tất cả process còn lại phải listen trên loopback hoặc private Docker network.
FAQ
Reverse proxy nào phù hợp nhất cho một vài Docker app trên cùng một VPS?
Với ba hoặc bốn service chỉ thỉnh thoảng mới thêm, Traefik thường đáng dùng vì mỗi app có sẵn routing label riêng và không cần sửa file cấu hình trung tâm. Nếu các service đã ổn định và bạn chủ yếu muốn HTTPS không còn là việc phải lo, Caddy dễ học hơn và ít khả năng cấu hình sai hơn. Chọn Nginx nếu bạn đã quen dùng nó, hoặc nếu cần một tính năng mà hai lựa chọn kia không có, chẳng hạn response caching hoặc TCP listener thuần.
Caddy có thật sự không cần cấu hình certificate không?
Trong trường hợp thông thường thì đúng. Chỉ cần dùng public hostname làm địa chỉ site là đủ: Caddy request certificate qua ACME, phục vụ redirect từ port 80 và tự gia hạn trước khi certificate hết hạn. Tuy vậy, vẫn phải đáp ứng hai điều kiện. Port 80 phải truy cập được từ Internet để thực hiện HTTP-01 challenge, và bản ghi DNS A hoặc AAAA của hostname phải trỏ sẵn đến VPS, vì certificate authority sẽ resolve hostname rồi kết nối ngược đến đó.
Tôi có thể chạy Nginx và Traefik trên cùng một VPS không?
Không thể chạy trên cùng các port. Proxy khởi động sau sẽ bind thất bại, nginx báo bind() to 0.0.0.0:443 failed (98: Address already in use) còn Traefik ghi một bind error tương tự vào log rồi thoát. Chạy một proxy trên port 80 và 443, rồi đặt mọi thứ khác phía sau nó. Nếu đang migration, hãy chuyển từng hostname một: để front proxy forward đến proxy cũ qua một loopback port cho đến khi site cuối cùng được chuyển xong.
Tại sao websocket của tôi bị ngắt sau 60 giây phía sau Nginx?
proxy_read_timeout mặc định là 60 giây và áp dụng cho tunnel sau khi upgrade hoàn tất. Vì vậy, connection không có traffic trong một phút sẽ bị proxy đóng thay vì app của bạn. Tăng giá trị này tại location đó bằng proxy_read_timeout 3600s;, hoặc để application gửi ping frame mỗi 30 giây. Caddy và Traefik không đóng các connection đã upgrade khi idle theo bộ đếm một phút. Vì vậy, cùng một app có thể hoạt động ổn định phía sau chúng nhưng không ổn định phía sau Nginx.