SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

Nginx、Caddy 与 Traefik 怎么选?反向代理对比

三种反向代理共用一个 VPS 和公网 IP:比较 HTTPS 证书、每个应用的配置成本、WebSocket 与 Docker 路由,帮您按场景选择 Nginx、Caddy 或 Traefik。

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 上使用 Certbot 配置 Nginx。如果子域名较多,不想逐一列出,还可以使用同一工具通过DNS-01 challenge 申请通配符证书

Traefik 自带 ACME 客户端。 在静态配置中配置一个证书解析器,之后每个 router 都可以使用它。所有状态信息,包括账户密钥和证书,都保存在一个 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 oktest is successful 的命令。第二个应用使用相同的配置块,只需修改主机名和端口。proxy_set_header 行并非可有可无:当 proxy_pass 指向某个地址时,nginx 默认会向上游发送 Host: 127.0.0.1:8080,因此,如果应用根据 Host 请求头生成绝对 URL,就会把用户重定向到 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

整个配置文件就这些内容。reverse_proxy 会自动设置 X-Forwarded-ForX-Forwarded-ProtoX-Forwarded-Host。默认情况下,它会忽略客户端在这些请求头中发送的内容,因此客户端无法向后端伪造请求来源。证书、端口 80 重定向和续期都由这两个站点地址自动处理。配置文件中不需要额外声明这些功能。

Traefik

Traefik 在执行任何路由之前,需要先配置静态配置。下面是作为 Compose 服务运行的配置,镜像标签截至 2026 年 8 月为当前版本:

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 为多个应用配置路由

每增加一个应用,需要多少配置?

ChartNon-blank config lines for the same two-app routing job
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 块中必须同时存在以下三行:

        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 秒,并且作用于升级后的隧道,因此 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 的标签完全无法表达此配置:您需要在 file provider 中定义 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 在独立的 entrypoint 上提供 TCP 和 UDP 路由器。Caddy 需要另一个插件,因此还需要再次自定义构建。
  • 代理后面已有 Web 服务器。 如果服务是经典 PHP 应用,那么 Ubuntu 24.04 上的 LAMP 堆栈 已包含 Apache。在前面再加一层代理后,就会有两个位置设置响应头,也会有两个位置可以重写 URL。请确定由哪一层终止 TLS,然后让另一层仅通过回环地址监听纯 HTTP。

选择之后出现的防火墙陷阱

反向代理的作用是只开放 80 和 443 端口。Docker 会在后台绕过这一点。使用 -p 8080:80 发布端口时,Docker 会向 nat 表写入 DNAT 规则。该规则会在 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 家庭实验室:Traefik。 服务超过 3 个后,修改标签通常比编辑集中式配置文件更省事,而且删除服务时,其路由也会一并删除。首次配置应预留一个下午,因为 entrypoints、routers、services 和 middlewares 都是新的术语。标签中的拼写错误通常会导致 Traefik 返回 404,而不是导致服务启动失败。因此,在判断应用损坏前,先查看 docker logs traefik 中的解析错误。

已有 Nginx 配置,或有上述列表中的任何需求:Nginx。 Nginx 已经提供响应缓存和客户端证书的配置方案,而且几乎所有第三方指南都以它为前提。代价是,证书和 WebSocket 支持需要手动配置,不能开箱即用。

无论选择哪种方案,都应遵循同一条规则。只有一个进程监听公网接口,其他所有进程都监听 loopback 或私有 Docker 网络。

FAQ

少量 Docker 应用部署在同一台 VPS 上时,哪个反向代理最好?

如果你偶尔添加 3 或 4 个服务,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 端口,再将其他所有服务置于其后。如果正在迁移,请一次迁移一个主机名:在最后一个站点迁移完成前,让前端代理通过 loopback 端口转发到旧代理。

为什么我的 WebSocket 经 Nginx 代理后会在 60 秒后断开?

proxy_read_timeout 的默认值为 60 秒。升级完成后,该设置会应用于隧道,因此连接如果持续 1 分钟没有流量,就会由代理关闭,而不是由应用关闭。在对应的 location 中使用 proxy_read_timeout 3600s; 增大该值,或者让应用每 30 秒发送一个 ping frame。Caddy 和 Traefik 不会使用 1 分钟计时器关闭空闲的升级连接,因此同一个应用在它们后面看起来稳定,在 Nginx 后面却不稳定。