So sánh Nginx, Caddy và Traefik: Chọn proxy nào?
Nên dùng Nginx, Caddy hay Traefik làm reverse proxy trên VPS? Bài viết so sánh chi tiết về khả năng quản lý chứng chỉ TLS, cấu hình Docker, websockets và hiệu năng thực tế.
Nginx so với Caddy và Traefik: câu trả lời ngắn gọn
Nginx, Caddy và Traefik đều thực hiện cùng một công việc là reverse proxy: lắng nghe trên cổng 443, đọc hostname trong mỗi request và chuyển tiếp đến đúng dịch vụ trên VPS của bạn. Cả ba đều có thể đặt bốn ứng dụng tự host sau một địa chỉ IP công cộng, và tất cả đều đủ nhanh để ứng dụng của bạn mới là thành phần gây chậm hệ thống. Điểm khác biệt nằm ở cách mỗi công cụ lấy chứng chỉ TLS (transport layer security) và chi phí cấu hình cho mỗi ứng dụng bổ sung. Sự khác biệt khác sẽ xuất hiện sau này, vào ngày bạn cần một tính năng mà các hướng dẫn thông thường bỏ qua.
Hãy chọn Caddy nếu bạn muốn HTTPS được xử lý tự động và các dịch vụ của bạn là ứng dụng web thông thường. Hãy chọn Traefik nếu mọi thứ chạy trong Docker Compose và bạn thêm dịch vụ mới mỗi vài tuần. Hãy chọn Nginx nếu bạn đã sử dụng nó, hoặc nếu bạn cần response caching, client certificate, raw TCP forwarding, hoặc một cấu hình lớn hiện có mà bạn không muốn viết lại.
Mỗi công cụ lấy chứng chỉ TLS như thế nào?
Tiêu chí này quyết định lựa chọn của phần lớn người dùng, vì vậy hãy bắt đầu từ đây. Cả ba cuối cùng đều giữ cùng một chứng chỉ do cùng một authority cấp. Nhưng cách bạn thực hiện để lấy chứng chỉ thì khác nhau.
Caddy yêu cầu chứng chỉ vì bạn đã đặt hostname. Khai báo 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 port 80 và tự gia hạn. Bạn không cần thêm tool hoặc timer để kiểm tra. Chứng chỉ nằm trong data directory của user caddy, cụ thể là /var/lib/caddy/.local/share/caddy khi cài bằng package. Vì vậy, hãy thêm path này vào 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 sẽ ký bằng certificate authority cục bộ của chính Caddy. Kết quả tương đương với tạo self-signed certificate trên Ubuntu, nhưng quá trình gia hạn được tự động xử lý.
Nginx cần thêm một ACME client. Tool thường dùng là Certbot. Tool này lấy chứng chỉ, còn plugin --nginx sẽ sửa server block để thêm listener trên port 443 và redirect. Việc gia hạn chạy bằng systemd timer do package cài đặt, nên bạn có hai thành phần cần quản lý 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. Hướng dẫn từng bước có tại Certbot trên Ubuntu 24.04 với Nginx. Cùng tool này cũng xử lý wildcard certificate qua DNS-01 challenge khi bạn có nhiều subdomain hơn số lượng muốn liệt kê.
Từ August 2025, còn có một cách chính thức không dùng Certbot. ngx_http_acme_module là một dynamic module, được cài dưới dạng package nginx-module-acme từ repository riêng của nginx.org thay vì được compile vào nginx. Một block acme_issuer khai báo certificate authority. Một dòng acme_certificate trong server block yêu cầu chứng chỉ, sau đó nginx tự gia hạn chứng chỉ. Tính đến October 2026, module này xử lý được các challenge HTTP-01 và TLS-ALPN-01 nhưng chưa hỗ trợ wildcard name. Vì vậy, wildcard certificate vẫn cần Certbot và DNS-01.
Traefik có ACME client riêng. Bạn cấu hình một certificate resolver trong static configuration, sau đó mọi router đều có thể 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 dùng file này nếu file cho phép bất kỳ user nào ngoài owner đọc được. Traefik sẽ 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 600Mount một directory và để Traefik tự tạo file. Hãy tạo directory trước bằng touch. Directory sẽ kế thừa umask của bạn, đây là lý do phần lớn người dùng gặp dòng cấu hình đó.
Có một điểm giống nhau ở cả ba công cụ. HTTP-01 challenge cần port 80 có thể truy cập từ Internet, vì certificate authority sẽ kết nối ngược vào port này. Nếu chỉ mở port 443, việc cấp chứng chỉ sẽ thất bại với lỗi trông giống lỗi DNS.
Cùng một tác vụ định tuyến 2 ứng dụng trong 3 cấu hình
Tác vụ: app.example.com trỏ đến một service trên 127.0.0.1:8080, còn files.example.com trỏ đế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 tương ứng của từng proxy, để thấy rõ sự khác biệt về độ dài 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 link, kiểm tra, 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 syntax is ok và test is successful là bước 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. Công dụng của 4 header này và lý do dấu slash ở cuối proxy_pass âm thầm thay đổi path mà ứng dụng nhận được được giải thích từng directive trong hướng dẫn về nginx server block này.
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. reverse_proxy tự thiết lập 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 renewal đều được suy ra từ 2 địa chỉ site. 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 định tuyến request. Khi chạy dưới dạng Compose service, với image tag mới nhất tính đến October 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:/letsencryptMỗi ứng dụng sau đó 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, và đó mới là lợi ích chính: chỉ Traefik được publish ra ngoài. Cấu hình đầy đủ, gồm network dùng chung và redirect middleware, nằm trong hướng dẫn định tuyến nhiều ứng dụng bằng Traefik và Docker Compose.
Mỗi ứng dụng bổ sung tốn bao nhiêu cấu hình?
Dữ liệu đằng sau biểu đồ này
[
{
"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
}
]Tính từ các khối bên trên. Một server block của Nginx tốn 11 dòng không trống, và bạn phải viết lại nó cho mỗi hostname. Một site block của Caddy tốn 3 dòng. Traefik yêu cầu 17 dòng cấu hình tĩnh trước khi phục vụ bất kỳ request nào, sau đó là 5 dòng nhãn (label) cho mỗi ứng dụng.
Hãy nhìn vào sự đánh đổi, đừng tìm người thắng cuộc. Traefik tốn nhiều công sức nhất trước khi có ứng dụng đầu tiên và tốn ít nhất cho mỗi ứng dụng sau đó, hai tổng số này gặp nhau ở khoảng site thứ ba. Dưới mức đó, cấu hình tĩnh là chi phí dư thừa bạn không cần. Trên mức đó, các nhãn bắt đầu chiếm ưu thế và duy trì lợi thế, vì định tuyến nằm ngay cạnh dịch vụ mà nó điều hướng. Xóa dịch vụ thì route của nó cũng mất theo, đây là điểm yếu của file cấu hình tập trung: các server block cũ kỹ cho những ứng dụng đã ngừng hoạt động từ nhiều tháng trước.
Số dòng cấu hình cũng làm Nginx trông có vẻ gọn hơn thực tế. Mỗi khối đó cần một symlink, một nginx -t, một lệnh reload và một lần chạy certbot, trong khi chỉnh sửa Caddy chỉ cần một lệnh reload và Traefik không cần chạy bất kỳ lệnh nào. Cả ba đều reload mà không làm ngắt các kết nối đang hoạt động. Sự khác biệt nằm ở số lượng các bước riêng lẻ mà bạn phải nhớ vào lúc một giờ sáng.
Thành phần nào biết về các container của bạn?
Traefik theo dõi Docker socket và tự động tạo các router từ label của container khi chúng khởi động hoặc dừng. Không có công cụ nào khác ở đây làm được điều này. Cả Nginx và Caddy đều cần chỉnh sửa file cấu hình và reload khi có container mới xuất hiện, đồng thời chúng cần một địa chỉ có thể truy cập được: hoặc là một cổng được publish trên loopback, hoặc là một Docker network dùng chung mà proxy được kết nối vào đó.
Tính năng này có cái giá của nó và cần được nêu rõ. Traefik đọc /var/run/docker.sock. Bất kỳ ai có thể giao tiếp với socket đó đều có thể khởi động một container với filesystem của host được mount vào bên trong, điều này tương đương với quyền root trên host. Việc mount ở chế độ read-only sẽ giảm thiểu rủi ro nhưng không loại bỏ hoàn toàn. Nếu điều này quan trọng đối với mô hình đe dọa của bạn, hãy đặt một socket proxy ở giữa để chỉ expose các endpoint liệt kê container mà Traefik cần.
Caddy có thể thực hiện discovery dựa trên label thông qua một plugin cộng đồng, nhưng các plugin của Caddy được biên dịch trực tiếp vào 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, sau đó bạn phải tự quản lý bản build đó và các bản cập nhật của nó. Đối với ba hoặc bốn dịch vụ, việc chỉnh sửa Caddyfile tốn ít công sức hơn.
Websockets và streaming: những lỗi thường gặp và nguyên nhân
Nginx là thành phần cần được cấu hình bổ sung. Một kết nối WebSocket bắt đầu bằng một yêu cầu HTTP chứa Upgrade: websocket, và Nginx sẽ không chuyển tiếp các hop-by-hop header tới upstream trừ khi bạn chỉ định rõ.
# /etc/nginx/conf.d/upgrade-map.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}Sau đó, bên trong block location, bạn phải thêm ba dòng sau:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;Nếu thiếu chúng, console của trình duyệt sẽ báo lỗi WebSocket connection to 'wss://app.example.com/ws' failed trong khi log của backend chỉ hiển thị một yêu cầu GET thông thường. Biến map tồn tại vì nếu dùng Connection: upgrade cố định, nó sẽ được gửi kèm trong mọi yêu cầu, kể cả các yêu cầu HTTP thông thường vốn cần giá trị close.
Hai thiết lập mặc định khác của Nginx cũng gây lỗi. proxy_read_timeout mặc định là 60 giây và nó áp dụng cho tunnel sau khi upgrade, vì vậy một kết nối websocket không có lưu lượng trong một phút sẽ bị proxy đóng lại. Ngoài ra, các server-sent events sẽ đến trễ hoặc bị ngắt quãng cho đến khi bạn thiết lập proxy_buffering off; tại location đó, vì Nginx giữ phản hồi trong bộ đệm trong khi trang web của bạn vẫn đang chờ dữ liệu.
Caddy tự động thực hiện upgrade và chuyển kết nối sang tunnel hai chiều mà không cần thêm chỉ thị nào. Nó cũng đẩy dữ liệu (flush) ngay lập tức khi phản hồi là text/event-stream hoặc không xác định được độ dài, nên streaming hoạt động bình thường. Traefik cho phép các yêu cầu upgrade đi qua và không buffer phản hồi trừ khi bạn tự thêm middleware buffering. Nếu dịch vụ của bạn bao gồm chat, web terminal, log tails hoặc dashboard thời gian thực, đây là sự khác biệt lớn về khối lượng cấu hình bạn phải viết và debug.
Block server Nginx đầy đủ, bao gồm websockets 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 cần nằm trong ngữ cảnh http, không phải bên trong server, vì vậy hãy giữ nó trong file riêng tại /etc/nginx/conf.d/. Chỉ tắt proxy_buffering tại các location có streaming, vì cơ chế buffering giúp Nginx giải phóng backend worker sớm hơn đối với các phản hồi thông thường. Certbot sẽ ghi đè block này khi bạn chạy nó, vì vậy hãy kiểm tra lại file sau khi thực hiện.
Điều gì xảy ra khi bạn cần những cấu hình không phổ biến?
Đây là lúc Nginx thể hiện giá trị qua các dòng cấu hình bổ sung.
- Client certificate, còn gọi là mTLS (mutual TLS), yêu cầu client cũng phải cung cấp chứng chỉ. Nginx cần
ssl_client_certificate /etc/ssl/ca.pem;vàssl_verify_client on;trong server block. Caddy cần một blockclient_authbên trongtls. Traefik labels không thể thực hiện việc này: bạn phải định nghĩa một TLS option trong file provider và trỏ router đến đó bằngtraefik.http.routers.app.tls.options=mtls@file. Mô hình mọi thứ trong labels sẽ có ngoại lệ đầu tiên khi bạn cần tính năng này. - Upload dung lượng lớn. Nginx mặc định giới hạn body của request ở mức 1 MB. Upload lớn hơn sẽ trả về
413 Request Entity Too Large, và error log sẽ ghiclient intended to send too large body. Hãy tăngclient_max_body_size. Caddy và Traefik mặc định không giới hạn body, nên request sẽ đến thẳng ứng dụng của bạn và giới hạn của ứng dụng sẽ quyết định. - Response caching. Nginx có
proxy_cachevà nó rất ổn định. Caddy cần một plugin được biên dịch kèm. Bản open source của Traefik không có HTTP cache, điều này gây bất ngờ cho những người mặc định rằng mọi proxy đều có cache. - Raw TCP hoặc UDP, cho cổng database hoặc game server. Nginx có module
stream. Traefik có các TCP và UDP router trên entrypoint riêng của chúng. Caddy cần một plugin khác, đồng nghĩa với việc phải build lại bản tùy chỉnh. - Web server đã nằm sau proxy. Nếu dịch vụ là một ứng dụng PHP truyền thống, thì LAMP stack trên Ubuntu 24.04 đã bao gồm Apache. Việc đặt thêm một 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 xem thành phần nào sẽ thực hiện TLS termination, sau đó để thành phần còn lại chạy HTTP thuần và bind vào loopback.
Bẫy firewall sau lựa chọn này
Mục đích của reverse proxy là chỉ mở cổng 80 và 443. Docker âm thầm phá vỡ điều này. Việc publish một cổng bằng -p 8080:80 sẽ ghi một rule DNAT vào bảng nat, và rule này được đánh giá trước các rule INPUT mà ufw quản lý, vì vậy ufw deny 8080 không chặn được nó và ứng dụng của bạn nằm trên public internet ngay cạnh proxy mà bạn đã cấu hình kỹ lưỡng. Hãy bind các cổng đã publish vào loopback bằng 127.0.0.1:8080:80, hoặc bỏ hoàn toàn ports: và để proxy kết nối tới container thông qua Docker network, đây chính là cách ví dụ về Traefik ở trên thực hiện. Cơ chế và cách khắc phục nằm tại tại sao các cổng Docker publish lại bypass ufw.
Hãy kiểm tra từ một máy không phải là VPS, vì kiểm tra chạy ngay trên máy đó sẽ luôn thành công:
curl --max-time 5 http://your.server.address:8080Connection refused hoặc một timeout là kết quả bạn muốn. Một phản hồi HTTP nghĩa là ứng dụng đó có thể truy cập được mà không cần đi qua proxy của bạn, và mọi thứ bạn đã cấu hình ở trên chỉ là hình thức.
Bạn nên chọn proxy nào?
Các trang web chủ yếu là tĩnh, kèm theo một hoặc hai ứng dụng: Caddy. Tính năng HTTPS tự động loại bỏ công việc lặp đi lặp lại lớn nhất mà bạn phải làm. Cấu hình vẫn đủ ngắn để đọc trên một màn hình, và một trang web tĩnh chỉ cần một dòng root và một dòng file_server bên trong cùng một khối site. Cái giá phải trả là cộng đồng hỗ trợ nhỏ hơn khi có sự cố lạ xảy ra.
Một homelab dùng docker-compose mà bạn liên tục thêm dịch vụ mới: Traefik. Sau dịch vụ thứ ba, việc dùng labels sẽ đỡ tốn công hơn là chỉnh sửa một file cấu hình trung tâm, và khi xóa một dịch vụ, route của nó cũng tự động bị xóa theo. Hãy dành ra một buổi chiều cho lần thiết lập đầu tiên, vì entrypoints, routers, services và middlewares đều là những khái niệm mới. Một lỗi đánh máy trong label thường hiển thị dưới dạng lỗi 404 từ Traefik thay vì lỗi không khởi động được, vì vậy hãy đọc docker logs traefik để tìm lỗi cú pháp trước khi cho rằng ứng dụng bị hỏng.
Một cấu hình Nginx hiện có, hoặc bất kỳ yêu cầu nào từ danh sách trên: Nginx. Nó đã có sẵn giải pháp cho việc response caching và client certificates, và hầu hết các hướng dẫn từ bên thứ ba đều mặc định dùng nó. Cái giá phải trả là chứng chỉ và hỗ trợ websocket là những thứ bạn phải tự cấu hình thay vì có sẵn.
Có một quy tắc áp dụng cho bất kể bạn chọn công cụ nào. Chỉ duy nhất một tiến trình được phép lắng nghe trên giao diện public, và mọi thứ khác phải lắng nghe trên loopback hoặc trên một Docker network riêng.
FAQ
Reverse proxy nào là tốt nhất cho một vài ứng dụng Docker trên cùng một VPS?
Với ba hoặc bốn dịch vụ mà bạn thỉnh thoảng mới thêm vào, Traefik rất đáng dùng vì mỗi ứng dụng tự mang theo các routing label của riêng nó và không cần chỉnh sửa file cấu hình trung tâm. Nếu các dịch vụ ổn định và bạn chủ yếu muốn giải quyết vấn đề HTTPS, Caddy dễ học và ít lỗi hơn. Hãy chọn Nginx khi bạn đã quen thuộc với nó, hoặc khi bạn cần một tính năng mà hai công cụ kia không có, ví dụ như response caching hoặc một TCP listener thuần túy.
Caddy có thực sự không cần cấu hình chứng chỉ không?
Với trường hợp thông thường, đúng là như vậy. Việc đặt tên một hostname công khai làm địa chỉ trang web là toàn bộ cấu hình: Caddy tự yêu cầu chứng chỉ qua ACME, phục vụ chuyển hướng từ cổng 80 và gia hạn trước khi hết hạn. Có hai điều kiện cần phải đúng. Cổng 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 về VPS, vì cơ quan cấp chứng chỉ sẽ phân giải tên miền và kết nối ngược lại.
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 cổng. Tiến trình nào khởi động sau sẽ không thể bind cổng, Nginx sẽ báo bind() to 0.0.0.0:443 failed (98: Address already in use) trong khi Traefik ghi log lỗi bind tương tự và thoát. Hãy chạy một proxy trên cổng 80 và 443, rồi đặt mọi thứ khác phía sau nó. Nếu bạn đang di chuyển dịch vụ, hãy chuyển từng hostname một: để proxy phía trước chuyển tiếp đến proxy cũ trên một cổng loopback cho đến khi trang web cuối cùng đã được chuyển xong.
Tại sao websockets của tôi bị ngắt sau 60 giây khi chạy sau Nginx?
proxy_read_timeout mặc định là 60 giây và nó áp dụng cho đường truyền ngay khi quá trình upgrade hoàn tất, vì vậy một kết nối không có lưu lượng trong một phút sẽ bị proxy đóng thay vì ứng dụng của bạn. Hãy tăng giá trị này tại location đó bằng proxy_read_timeout 3600s;, hoặc cấu hình ứng dụng gửi một ping frame mỗi 30 giây. Caddy và Traefik không đóng các kết nối đã upgrade bị nhàn rỗi theo bộ đếm thời gian một phút, đó là lý do tại sao cùng một ứng dụng có thể trông ổn định khi chạy sau chúng nhưng lại không ổn định khi chạy sau Nginx.