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?
Trục này quyết định lựa chọn của hầu hết mọi người, vì vậy hãy bắt đầu từ đây. Cả ba đều nhận được cùng một chứng chỉ từ cùng một cơ quan cấp phát. Công việc bạn cần làm để đạt được điều đó thì không giống nhau.
Caddy tự yêu cầu chứng chỉ vì bạn đã đặt tên hostname. Hãy viết app.example.com làm địa chỉ trang web, Caddy sẽ yêu cầu chứng chỉ qua ACME (automatic certificate management environment) từ Let's Encrypt, tự động chuyển sang ZeroSSL nếu thất bại, phục vụ chuyển hướng HTTP sang HTTPS trên cổng 80 và tự gia hạn. Không có công cụ thứ hai hay bộ đếm thời gian nào cần kiểm tra. Chứng chỉ nằm trong thư mục dữ liệu của người dùng caddy, /var/lib/caddy/.local/share/caddy khi cài đặt qua gói, vì vậy hãy thêm đường dẫn đó vào bản sao lưu của bạn hoặc chấp nhận việc cấp mới sau khi rebuild. Đối với hostname không công khai, tls internal sẽ ký bằng cơ quan cấp chứng chỉ cục bộ của riêng Caddy. Điều này mang lại kết quả tương tự như tạo chứng chỉ tự ký trên Ubuntu, với việc gia hạn được xử lý thay cho bạn.
Nginx không có client ACME. Certbot sẽ lấy chứng chỉ, và plugin --nginx của nó sẽ viết lại server block của bạn để thêm listener 443 và chuyển hướng. Việc gia hạn chạy từ một systemd timer mà gói cài đặt, vì vậy có hai thành phần chuyển động và hai thứ cần xác minh: sudo certbot renew --dry-run chứng minh đường dẫn gia hạn vẫn hoạt động, còn systemctl list-timers | grep certbot hiển thị timer đó có tồn tại. Các bước chi tiết nằm trong Certbot trên Ubuntu 24.04 với Nginx, và cùng công cụ đó hỗ trợ chứng chỉ wildcard thông qua DNS-01 challenge khi bạn có nhiều subdomain hơn mức muốn liệt kê.
Traefik mang theo client ACME của riêng nó. Bạn cấu hình một certificate resolver trong cấu hình tĩnh, và mọi router sau đó đều có thể sử dụng nó. Toàn bộ trạng thái, bao gồm account key và chứng chỉ, nằm trong một file acme.json duy nhất. Traefik sẽ từ chối sử dụng file đó nếu bất kỳ ai ngoài chủ sở hữu có quyền đọc, và nó sẽ thông báo cho bạn trước khi hủy 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. Nếu bạn tạo nó trước bằng touch, nó sẽ kế thừa umask của bạn, đây là cách mà hầu hết mọi người gặp phải lỗi quyền truy cập đó.
Một điều đúng với cả ba: HTTP-01 challenge yêu cầu cổng 80 phải truy cập được từ internet, vì cơ quan cấp chứng chỉ sẽ kết nối ngược lại cổng này. Nếu chỉ mở cổng 443, việc cấp chứng chỉ sẽ thất bại với thông báo lỗi giống như lỗi DNS.
Cùng một tác vụ định tuyến hai ứng dụng trong ba cấu hình
Tác vụ: app.example.com trỏ đến một dịch vụ trên 127.0.0.1:8080, files.example.com trỏ đến một dịch vụ trên 127.0.0.1:8081, cả hai đều qua HTTPS. Dưới đây là toàn bộ cấu hình cho mỗi proxy để thấy rõ sự khác biệt về độ dài thay vì chỉ nói suông.
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 chứng chỉ.
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.comnginx -t chạy syntax is ok và test is successful là bước kiểm tra cần thực hiện trước mỗi lần reload. Ứng dụng thứ hai là một block tương tự với hostname và cổng đã thay đổi. Các dòng proxy_set_header không phải là trang trí: khi proxy_pass định nghĩa một địa chỉ, nginx mặc định gửi Host: 127.0.0.1:8080 đến upstream, vì vậy một ứng dụng tạo URL tuyệt đối từ header Host sẽ điều hướng người dùng của bạn về localhost. Mục đích của bốn header đó là gì, và tại sao dấu gạch chéo ở cuối proxy_pass lại âm thầm thay đổi đường dẫn mà ứng dụng của bạn nhận được, tất cả được giải thích chi tiết từng chỉ thị trong hướng dẫn chi tiết về server block của nginx 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, và theo mặc định, nó bỏ qua bất kỳ header nào client gửi đến, vì vậy một request không thể đánh lừa backend của bạn về nguồn gốc của nó. Chứng chỉ, việc redirect cổng 80 và gia hạn đều tự động dựa trên hai địa chỉ trang web. Không cần thêm bất kỳ dòng nào khác trong file để thực hiện các tác vụ này.
Traefik
Traefik cần cấu hình tĩnh trước khi định tuyến bất cứ thứ gì. Là một service trong Compose, với tag image cập nhật đế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:/letsencryptMỗi ứng dụng sau đó tự mang cấu hình định tuyến riêng thông qua các label trong file compose của chính nó:
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à cổng bên trong container, không phải cổng được publish, vì Traefik kết nối tới container thông qua một Docker network dùng chung. Ứng dụng không cần dòng ports: nào cả, và đó chính là lợi ích thực sự: chỉ có Traefik được publish ra ngoài. Toàn bộ quá trình build, bao gồm network dùng chung và middleware redirect, nằm 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
}
]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.