Nginx、Caddy 与 Traefik 怎么选?反向代理对比
一台 VPS、一个公网 IP 部署四个应用时,比较 Nginx、Caddy 和 Traefik 的 TLS 证书、每个应用的配置成本、WebSocket 与 Docker 路由差异,帮您按场景选型。
Nginx、Caddy 与 Traefik:简短结论
Nginx、Caddy 和 Traefik 都执行反向代理的相同工作:监听 443 端口,读取每个请求中的主机名,然后将请求转发到 VPS 上正确的服务。三者都可以让四个自托管应用共用一个公网 IP 地址,而且性能都足够高,真正的瓶颈通常会是应用本身。它们的区别在于各自如何获取 TLS(传输层安全)证书,以及每增加一个应用需要多少配置工作。另一个区别会在之后显现:当你需要处理常见教程没有涉及的情况时。
如果希望由代理自动处理 HTTPS,并且服务都是常规 Web 应用,请选择 Caddy。如果所有服务都运行在 Docker Compose 中,而且每隔几周就会新增一个服务,请选择 Traefik。如果你已经在使用 Nginx,或者需要响应缓存、客户端证书、原始 TCP 转发,或不想重写一份规模较大的现有配置,请选择 Nginx。
每种方案如何获取 TLS 证书?
对大多数人来说,这个维度决定了最终选择,因此从这里开始。三种方案最终都会从同一个机构获取并持有同一张证书,但获取过程并不相同。
Caddy 会因为你指定了主机名而申请证书。 将 app.example.com 写成站点地址后,Caddy 会通过 ACME(自动证书管理环境)向 Let's Encrypt 请求证书;如果失败,则回退到 ZeroSSL。它会在 80 端口提供从 HTTP 到 HTTPS 的重定向,并自动续期。无需额外工具,也无需设置定时器进行检查。证书存储在 caddy 用户的数据目录中;通过软件包安装时,路径为 /var/lib/caddy/.local/share/caddy。请将该路径加入备份,或者接受重建后重新签发证书。对于不公开的主机名,tls internal 会使用 Caddy 自己的本地证书颁发机构进行签名。其效果与 在 Ubuntu 上创建自签名证书相同,只是续期由 Caddy 自动处理。
Nginx 不包含 ACME 客户端。 Certbot 负责获取证书,其 --nginx 插件会重写您的 server block,添加 443 监听和重定向。续期由软件包安装的 systemd 定时器执行,因此需要验证两个组件和两项内容:systemctl list-timers | grep certbot 可确认定时器存在,sudo certbot renew --dry-run 可确认续期流程仍能正常运行。分步说明请参阅 在 Ubuntu 24.04 上使用 Nginx 配置 Certbot。如果子域名数量较多,不想逐一列出,也可以使用同一工具通过 DNS-01 challenge 获取通配符证书。
Traefik 自带 ACME 客户端。 您只需在静态配置中配置一个证书解析器,之后每个路由器都可以使用它。所有状态信息,包括账户密钥和证书,都存储在一个 acme.json 文件中。如果该文件对所有者以外的用户可读,Traefik 将拒绝使用它,并在丢弃该解析器前显示相应提示:
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 600挂载一个目录,让 Traefik 自行创建该文件。先使用 touch 创建文件,这样文件会继承您的 umask;大多数人遇到该提示就是因为这一点。
三种方案都有一个共同点。HTTP-01 challenge 要求 80 端口可从互联网访问,因为证书颁发机构需要回连该端口。只开放 443 端口会导致证书签发失败,而且错误信息看起来像是 DNS 故障。
三种配置中的同一双应用路由任务
任务如下:app.example.com 转发到 127.0.0.1:8080 上的服务,files.example.com 转发到 127.0.0.1:8081 上的服务,二者都使用 HTTPS。下面分别给出各代理中的完整配置,以便直观看到详细程度的差异,而不是仅作说明。
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;
}
}然后创建链接、测试配置、重新加载服务,再添加证书。
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.com在每次重新加载前,都应运行 nginx -t,检查 syntax is ok 和 test is successful。第二个应用使用相同的配置块,只需修改主机名和端口。proxy_set_header 这些行并非装饰:当 proxy_pass 指向某个地址时,nginx 默认会将 Host: 127.0.0.1:8080 发送到上游服务。因此,如果应用根据 Host 请求头生成绝对 URL,就会把用户重定向到 localhost。这 4 个请求头分别有什么作用,以及在 proxy_pass 末尾添加斜杠为何会悄然改变应用收到的路径,都在这个 nginx server 块详解中按指令逐项说明。
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整个文件只有这些内容。reverse_proxy 会自动设置 X-Forwarded-For、X-Forwarded-Proto 和 X-Forwarded-Host。默认情况下,它会忽略客户端在这些请求头中发送的值,因此客户端无法向后端伪造请求来源。证书、从 80 端口进行的重定向以及续期,都由这两个站点地址自动处理。文件中无需额外配置。
Traefik
Traefik 在进行路由之前需要静态配置。以下是作为 Compose 服务运行的配置,镜像标签截至 August 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:/letsencrypt随后,每个应用在自己的 compose 文件中通过 labels 配置各自的路由:
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 是容器内部的端口,而不是发布到主机的端口,因为 Traefik 通过共享 Docker 网络访问容器。应用完全不需要 ports: 行,这正是这种方式的实际优势:只有 Traefik 发布端口。包含共享网络和重定向中间件的完整构建配置,见使用 Traefik 和 Docker Compose 为多个应用配置路由。
每增加一个应用,需要多少配置?
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
}
]根据上面的配置块统计。Nginx server block 有 11 行非空内容,而且每个主机名都要重复编写。Caddy site block 有 3 行。Traefik 在处理第一个请求前,需要先配置 17 行静态配置,之后每个应用还需要 5 个标签。
不要只看谁的总行数最少,还要看这种取舍。Traefik 在添加第一个应用前的成本最高,但之后每个应用的成本最低;两者的总量大约在第三个站点时持平。站点数少于这个数量时,静态配置就是不必要的额外开销。超过这个数量后,标签方案会逐渐领先,而且优势会继续扩大,因为路由配置与对应的服务放在一起。删除服务时,其路由也会一并删除。这正是集中式配置文件难以处理的问题:应用数月前已经不存在,但过时的 server block 仍然保留。
行数统计也让 Nginx 看起来更简单。每个配置块都需要创建符号链接、执行 nginx -t、重新加载配置并运行 certbot,而修改 Caddy 配置只需要重新加载一次,修改 Traefik 配置则完全不需要执行命令。三种方案重新加载时都不会断开现有连接。区别在于凌晨一点时,你需要记住多少个独立步骤。
谁能发现您的容器?
Traefik 监视 Docker socket,并在容器启动和停止时根据容器标签构建路由器。这里的其他工具都不会这样做。新容器出现时,Nginx 和 Caddy 都需要编辑配置并重新加载,还需要一个它们能够访问的地址:可以是发布到 loopback 的端口,也可以是代理已连接的共享 Docker 网络。
此功能需要付出代价,这一点应明确说明。Traefik 会读取 /var/run/docker.sock。任何能够与该 socket 通信的用户,都可以启动一个将主机文件系统挂载到容器中的容器,而这等同于获得主机上的 root 权限。以只读方式挂载可以降低风险,但无法消除风险。如果这不符合您的威胁模型,请在中间加入 socket proxy,仅暴露 Traefik 所需的容器列表端点。
Caddy 可以通过社区插件执行基于标签的服务发现,但 Caddy 插件会编译进程序,因此您需要使用 xcaddy 构建自定义二进制文件或自定义镜像,之后还要负责维护该构建及其更新。对于三四个服务,直接编辑 Caddyfile 工作量更小。
WebSocket 和流式传输:哪些配置会出问题,以及原因
需要额外配置的是 Nginx。WebSocket 连接首先以携带 Upgrade: websocket 的 HTTP 请求开始;除非明确配置,否则 nginx 不会将逐跳标头转发到上游。
# /etc/nginx/conf.d/upgrade-map.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}然后,在 location 块中加入以下 3 行,缺一不可:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;如果省略这些配置,浏览器控制台会显示 WebSocket connection to 'wss://app.example.com/ws' failed,而后端日志中会记录普通的 GET 请求。之所以使用 map,是因为如果硬编码 Connection: upgrade,它会随每个请求发送,包括本应使用 close 的普通请求。
Nginx 还有两个默认值会导致问题。proxy_read_timeout 的默认值是 60 秒,并且它作用于升级后的隧道。因此,1 分钟内没有流量的 WebSocket 连接会被代理关闭。服务器发送事件在设置该位置的 proxy_buffering off; 之前,会延迟到达或成批到达,因为 nginx 会将响应暂存在缓冲区中,而页面正在等待数据。
Caddy 无需任何指令即可执行升级,并将连接切换为双向隧道。如果响应为 text/event-stream 或长度未知,它还会立即刷新响应,因此无需额外配置即可支持流式传输。Traefik 会直接转发升级请求;除非手动添加其 buffering 中间件,否则不会缓冲响应。如果服务包含聊天、Web 终端、日志实时跟踪或实时仪表盘,这会直接影响需要编写和调试的配置量。
完整的 Nginx server 块,包括 WebSocket 和 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 应放在 http 上下文中,而不是 server 内部,因此应将其单独放在 /etc/nginx/conf.d/ 下的文件中。仅在需要流式传输的位置关闭 proxy_buffering,因为在普通响应中,缓冲可以让 nginx 提前释放后端工作进程。运行 Certbot 时,它会重写此块,因此运行完成后请重新检查该文件。
需要处理特殊需求时怎么办?
这正是 Nginx 多出配置项的价值所在。
- 客户端证书,也称为 mTLS(双向 TLS),要求客户端也提供证书。Nginx 要求在 server 块中配置
ssl_client_certificate /etc/ssl/ca.pem;和ssl_verify_client on;。Caddy 要求在tls内配置一个client_auth块。Traefik 的标签完全无法表达此配置:您需要在文件提供程序中定义 TLS 选项,再通过traefik.http.routers.app.tls.options=mtls@file将路由器指向该选项。第一次遇到这类需求时,完全依赖标签的配置方式就需要例外处理。 - 大文件上传。 Nginx 默认将请求正文限制为 1 MB。上传更大的文件会返回
413 Request Entity Too Large,错误日志中会记录client intended to send too large body。请增大client_max_body_size。Caddy 和 Traefik 默认不限制正文大小,因此请求会到达应用,最终由应用自身的限制决定。 - 响应缓存。 Nginx 提供
proxy_cache,功能也很成熟。Caddy 需要编译包含相应插件的版本。Traefik 的开源版本完全没有 HTTP 缓存功能,这会让认为所有代理都支持缓存的用户感到意外。 - 原始 TCP 或 UDP,例如数据库端口或游戏服务器。Nginx 提供
stream模块。Traefik 在各自的入口点上提供 TCP 和 UDP 路由器。Caddy 需要另一个插件,因此还需要执行一次自定义构建。 - 代理后面已经有 Web 服务器。 如果服务是传统 PHP 应用,那么 Ubuntu 24.04 上的 LAMP 堆栈已经包含 Apache。再在前面叠加代理,会产生两个设置响应头的位置,也会产生两个可以重写 URL 的位置。请确定由哪一层终止 TLS,然后让另一层使用绑定到 loopback 的纯 HTTP。
此选择带来的防火墙陷阱
反向代理的作用是只开放 80 和 443 端口。Docker 会在后台绕过这一点。使用 -p 8080:80 发布端口时,Docker 会将 DNAT 规则写入 nat 表。该规则会在 ufw 管理的 INPUT 规则之前执行,因此 ufw deny 8080 无法阻止它。这样,应用就会和精心配置的代理一起暴露在公网中。使用 127.0.0.1:8080:80 将发布的端口绑定到 loopback,或者完全删除 ports:,让代理通过 Docker 网络访问容器。上面的 Traefik 示例采用的就是后一种方式。具体机制和修复方法请参阅为什么 Docker 发布的端口会绕过 ufw。
请从 VPS 以外的计算机执行测试。因为在服务器本机运行的检查总会成功:
curl --max-time 5 http://your.server.address:8080返回 Connection refused 或超时才是预期结果。若收到 HTTP 响应,说明该应用无需经过代理即可访问,那么上面的所有配置都形同虚设。
应选择哪种代理?
主要是静态网站,另外运行一两个应用:Caddy。 自动 HTTPS 消除了最繁琐的定期维护工作。配置足够简短,可以在一个屏幕内读完;静态网站只需在同一个站点块中添加一行 root 和一行 file_server。代价是出现异常时,可直接复制使用的解决方案较少。
持续向其中添加服务的 docker-compose homelab:Traefik。 服务超过3个后,修改标签比编辑集中式配置文件更省事,而且删除服务时,其路由也会一并删除。首次配置应预留一个下午,因为 entrypoints、routers、services 和 middlewares 都是新的术语。标签中的拼写错误通常会表现为 Traefik 返回 404,而不是服务启动失败,因此在判断应用损坏之前,先阅读 docker logs traefik,查找解析错误。
已有 Nginx 配置,或有上述列表中的任何要求:Nginx。 Nginx 已经提供响应缓存和客户端证书的配置方案,而且几乎所有第三方指南都以它为前提。代价是,证书和 websocket 支持需要手动配置,不能直接获得。
无论选择哪一种,都应遵循一条规则。只有一个进程监听公网接口,其他所有进程都监听 loopback 或私有 Docker 网络。
FAQ
少量 Docker 应用运行在一台 VPS 上时,哪个反向代理更好?
如果只有三四个服务,并且会不时添加新服务,Traefik 更合适,因为每个应用都带有自己的路由标签,无需编辑中心配置文件。如果服务比较稳定,而您主要是不想再处理 HTTPS,Caddy 更容易学习,也更不容易配置出错。如果您已经熟悉 Nginx,或者需要另外两者不具备的功能(例如响应缓存或纯 TCP 监听器),请选择 Nginx。
Caddy 真的不需要配置证书吗?
对于常规场景,是的。将公网主机名指定为站点地址后,就完成了全部配置:Caddy 通过 ACME 请求证书,在 80 端口提供重定向,并在证书到期前续期。不过仍需满足两个条件。对于 HTTP-01 challenge,80 端口必须可从互联网访问;主机名的 DNS A 或 AAAA 记录也必须已经指向该 VPS,因为证书颁发机构会解析该名称并连接回该主机。
我可以在同一台 VPS 上运行 Nginx 和 Traefik 吗?
不能监听相同的端口。后启动的那个服务会绑定失败,nginx 会显示 bind() to 0.0.0.0:443 failed (98: Address already in use),而 Traefik 会记录类似的绑定错误并退出。让一个代理监听 80 和 443 端口,再将其他所有服务放在它后面。如果您正在迁移,请逐个迁移主机名:在最后一个站点迁移完成前,让前端代理通过回环端口转发到旧代理。
为什么我的 WebSocket 在 Nginx 后面运行 60 秒后会断开?
proxy_read_timeout 默认值为 60 秒,并且升级完成后该设置会应用于隧道。因此,如果连接持续一分钟没有流量,关闭连接的是代理,而不是您的应用。在该 location 中使用 proxy_read_timeout 3600s; 增大超时时间,或者让应用每 30 秒发送一个 ping 帧。Caddy 和 Traefik 不会使用一分钟计时器关闭空闲的已升级连接,因此同一个应用在它们后面运行时可能看起来很稳定,而在 Nginx 后面运行时却不稳定。