SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-23

Traefik v3 Docker Compose 反向代理配置5个应用

了解如何用 Traefik v3 和一个 Docker Compose 文件,让5个应用共享一个公网 IP 与 443 端口,按 Host 规则路由并自动申请 Let's Encrypt TLS,同时避开 acme.json 权限错误导致的启动失败。

一个 IP、5 个应用、一个 443 端口

您的 VPS 只有一个公网 IPv4 地址和一个 TCP 端口 443。您希望在同一台服务器上运行 Gitea、应用的预发布副本、内部仪表板、状态页面和 Webhook 接收器,共使用5个主机名。反向代理负责监听 :80 和 :443,读取每个请求中的 Host 标头,并将请求转发到正确的容器。Traefik 可以完成这些工作,并为每个主机名自动申请和续期证书,无需您手动运行 certbot。Nginx 和 Caddy 也可以正常终止这5个主机名的 TLS,因此如果尚未确定方案,建议先比较这3种代理在证书管理和每个应用的配置成本,再将所有服务接入其中一个代理。

Traefik 与 nginx server {} 块的区别在于配置来源。使用 nginx 时,您需要编辑配置文件并重新加载;证书生命周期则是另一项独立工作。这就是您在通过 nginx 上的 certbot 申请 Let's Encrypt 证书时采用的流程,续期计时器完全位于 Web 服务器之外。Traefik 的 Docker provider 会监控 Docker 事件流,并读取容器上的 labels:启动一个带有 Host() 规则标签的容器后,该容器会在1秒内可路由;停止容器后,路由就会消失。这也是其中的陷阱。配置分散在5个位置的 labels 中,错误的 label 不会产生明显提示,容器只是不再被路由,Traefik 也不会报告原因。

四个术语

  • 入口点是监听套接字。您将定义两个入口点:web 监听 :80websecure 监听 :443
  • 路由器匹配请求(Host(...)),并将请求关联到服务。证书按路由器申请,通过 tls.certresolver 完成。
  • 服务是后端,包括一个容器,以及该容器在 Docker 网络内部监听的端口。
  • 中间件位于路由器和服务之间,用于实现基本身份验证、IP 允许列表、请求头重写和重定向。

这四个术语是 Traefik 对原本需要手动编写的配置的命名:路由器相当于一个 server_name,服务相当于一个 proxy_pass 目标,中间件则对应您在逐行构建 nginx 反向代理服务器块时自行设置的请求头和身份验证指令。

静态配置(入口点、providers、ACME)通过 Traefik 的命令行或 traefik.yml 传入。修改静态配置需要重启 Traefik。动态配置(路由器、服务、中间件)来自容器标签,并支持热加载。混淆这两类配置,是出现“我的标志不起作用”问题的常见原因。

Compose 文件

名为 proxy 的共享 Docker 网络是整个架构的基础。只有 Traefik 和容器同时连接到该网络时,Traefik 才能访问此容器。

name: edge

networks:
  proxy:
    name: proxy

services:
  traefik:
    image: traefik:v3.5
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false
      - --providers.docker.network=proxy
      - --entryPoints.web.address=:80
      - --entryPoints.websecure.address=:443
      - --entryPoints.web.http.redirections.entryPoint.to=websecure
      - --entryPoints.web.http.redirections.entryPoint.scheme=https
      - --certificatesresolvers.le.acme.email=you@example.com
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
      # while you iterate, point at staging so a mistake costs nothing:
      # - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
      - --api.dashboard=true
      - --log.level=INFO
      - --accesslog=true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
      - traefik.http.routers.dashboard.entrypoints=websecure
      - traefik.http.routers.dashboard.tls.certresolver=le
      - traefik.http.routers.dashboard.service=api@internal
      - traefik.http.routers.dashboard.middlewares=dashboard-auth
      - traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS

  gitea:
    image: gitea/gitea:1  # major-only pin keeps this demo copy-pasteable; pin an exact release in production
    restart: unless-stopped
    volumes:
      - ./gitea:/data
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.gitea.rule=Host(`git.example.com`)
      - traefik.http.routers.gitea.entrypoints=websecure
      - traefik.http.routers.gitea.tls.certresolver=le
      - traefik.http.services.gitea.loadbalancer.server.port=3000

先配置 docker compose up -d,再配置 docker compose logs -f traefik。每个新增应用都复制 gitea 块,并为其设置独立的路由器名称、Host() 和内部端口。在 Docker 中运行并配置 TLS 和备份的 Nextcloud 安装也采用相同方式:删除其发布端口,将其连接到 proxy,再由路由器标签处理主机名和证书。

其中有 5 个细节需要注意。

exposedByDefault=false 会让容器在带有 traefik.enable=true 之前对 Traefik 不可见。如果省略它,之后启动的每个容器都会自动生成路由,包括你为检查某项功能而临时运行的 postgres

providers.docker.network=proxy 告诉 Traefik 在容器连接多个网络时应使用哪个网络。省略它后,Traefik 可能会选择错误的容器 IP,最终表现为 502,看起来像是应用故障。

loadbalancer.server.port=3000 是容器内部的端口;Gitea 在该端口上监听 3000。注意,应用容器都没有发布端口,只有 Traefik 发布端口。

web entrypoint 上的重定向会将明文请求转换为指向 HTTPS 的 308。80 端口仍需保持开放:ACME HTTP challenge 需要使用该端口,直接输入主机名的用户也需要使用该端口。

basic-auth 哈希中的双 $$ 是 Compose 的转义格式,并非拼写错误。使用 htpasswd -nbB admin 'your-password'(软件包 apache2-utils)生成哈希,然后将每个 $ 加倍。

证书与 acme.json 陷阱

tlschallenge=true选择 TLS-ALPN-01:Let's Encrypt 会通过 443 连接到您的 VPS,Traefik 在 TLS 握手期间响应验证挑战。另一种方式是使用 HTTP-01,通过 80 端口验证。将 Traefik 的 command: 列表中的 tlschallenge 行替换为以下两行:

      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web

两种方式都可以使用。但两者都要求主机名的公网 DNS 已指向您的 VPS,并且证书颁发机构能够解析该名称并从外部连接。先创建 A 记录(以及 AAAA 记录),使用 dig +short git.example.com 确认,然后启动 Traefik。

下面是一个可能让您排查一晚的问题。Traefik 会将 ACME 帐户密钥和所有已签发的证书保存在一个 acme.json 中。如果该文件对组用户或所有用户可读,Traefik 会输出与以下内容非常接近的日志,然后停止:

error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600

最稳妥的修复方法就是上面所述的方法:绑定挂载目录,让 Traefik 自行创建文件,并设置正确的权限。如果您使用 touch 创建了 acme.json,umask 会将其权限设为 644。请在主机上修复:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

请将该目录与应用卷一起备份。丢失它不会造成不可恢复的问题,证书可以重新签发;但同时为 5 个主机名重新签发证书,可能会触发速率限制。

迭代配置时请使用 staging CA。取消注释 caserver 行,确认所有路由都能正常工作,然后将其重新注释,并删除 acme.json,以便重新申请生产环境证书。生产环境的 Let's Encrypt 对完全相同的主机名集合每周允许签发 5 张重复证书,并会限制针对同一名称反复验证失败的请求。staging 环境签发的是不受信任的证书,浏览器会显示警告;该警告说明验证已成功,并且 staging 环境的限制宽松得多。

仪表板是控制界面,不是演示页面

大多数快速入门配置都会设置 --api.insecure=true,使仪表板在 8080 端口上提供服务,且不启用身份验证。在具有公网 IP 的服务器上,这会将您的路由拓扑、主机名、中间件名称和后端端口暴露给任何进行扫描的人。

上文 traefik 服务的标签提供了另一种方案:像其他应用一样,通过真实主机名和 TLS 路由仪表板,并置于 basicauth 后面。service=api@internal 负责将路由器连接到 Traefik 的内置 API。您还可以按从左到右的顺序串联 IP 允许列表,进一步收紧访问控制。如果办公室地址是动态的,请将范围设置为 在同一 VPS 上自行托管的 WireGuard VPN 分配的子网,并且仅通过该隧道访问仪表板:

- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth

当 5 个应用需要各自的账户时,共用一个 basicauth 密码就不再合理。同一个中间件位置也可以接收 forwardauth,将身份验证决策交给 自托管的单点登录服务器 Authentik。这样,仪表板及其旁边的每条路由都由同一个登录入口保护,并且可以在一个位置撤销访问权限。

Docker socket 具有 root 权限

/var/run/docker.sock 是一个 API,可创建挂载主机 / 的容器。访问该 API 等同于获得该计算机上的 root 权限,而 Traefik 需要通过它读取标签。

保留挂载上的 :ro,但必须明确它的作用:它只会将 socket 文件 设为只读。它不会阻止通过该 socket 发送 POST 请求到 Docker API。真正的缓解措施是永远不要将 socket 交给 Traefik,而是在两者之间放置一个过滤代理:

  dockerproxy:
    image: tecnativa/docker-socket-proxy   # pin the current tag
    restart: unless-stopped
    environment:
      CONTAINERS: 1
      NETWORKS: 1
      POST: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy

从 Traefik 中删除 socket 卷,并将 provider 指向该代理:

--providers.docker.endpoint=tcp://dockerproxy:2375

Traefik 仍可读取容器和网络信息,但无法创建任何对象。

防火墙、端口,以及每个人都会弄错的规则

开放两个端口,另外开放 SSH:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Docker 发布的端口会绕过 ufw。 Docker 会插入自己的 iptables 规则,这些规则的处理顺序早于 ufw 链。因此,使用 ports: ["3000:3000"] 启动的容器,即使对应位置已有 ufw deny 规则,仍可从互联网访问。防护措施应采用结构化设计,而不是依赖防火墙配置:只允许 Traefik 发布端口,其他每个容器都只配置 networks: [proxy],不配置任何其他内容。如果确实需要让某个服务访问主机,请将其绑定到回环地址,即 "127.0.0.1:3000:3000"

故障排查:您实际会看到的错误

404 page not found,由 Traefik 返回。没有任何路由匹配。按可能性从高到低排列:容器缺少 traefik.enable=true(且已设置 exposedByDefault=false);Host() 规则与您输入的名称不匹配;某个标签中的路由名称与另一个标签中的路由名称不同(routers.gitea.rulerouters.gitea.entrypoints 必须是同一个名称);或者您使用引号包裹了主机名,而不是反引号。Traefik v3 要求在匹配器中使用反引号。

502 Bad Gateway。 某个路由已匹配,但无法访问后端。几乎总是因为容器未连接到 proxy 网络,请检查 docker inspect -f '{{json .NetworkSettings.Networks}}' gitea。另一个可能原因是 loadbalancer.server.port 错误:您填写了已发布的端口,或者应用监听在其他端口。日志会记录该连接尝试:dial tcp 172.18.0.5:8080: connect: connection refused

浏览器发出警告,且证书签发给 TRAEFIK DEFAULT CERT 该主机名没有对应证书,Traefik 返回了自签名占位证书。查看 ACME 日志:

unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.com

DNS 尚未指向此服务器。修正记录,等待 TTL 过期,然后重启 Traefik。

HTTP challenge 出现 Invalid response from http://git.example.com/.well-known/acme-challenge/...:从外部访问时,端口 80 的流量未到达 Traefik。通常是 VPS 前面的服务商级防火墙导致,而不是 ufw。

证书始终无法签发,且 DNS 使用 Cloudflare 并启用了橙色云朵。 Cloudflare 会在其边缘节点终止 TLS,TLS-ALPN-01 无法通过 Cloudflare 完成验证。签发证书期间,将记录设置为仅 DNS,或改用带 API token 的 DNS-01 challenge。DNS-01 也是唯一可以签发通配符证书的 challenge。

重定向循环。 Traefik 前面已有组件终止 TLS,并将明文流量转发到 :80;入口点重定向又将其发回 HTTPS。删除其中一个重定向。

保持服务持续运行

必须将 Docker 单元设置为开机启用(systemctl is-enabled docker),这样 restart: unless-stopped 才能在重启后恢复整个服务栈。如需明确控制,可创建一个运行 docker compose -f /srv/edge/compose.yml up -d 的小型 systemd 单元,并配合 RemainAfterExit=yes,从而获得 systemctl status edge 和启动顺序控制。

固定 Traefik 标签(traefik:v3.5,不要使用 latest)。从 v2 升级到 v3 后,规则语法和 provider 名称发生了变化;无人值守的 latest 会直接重新加载它已无法解析的配置。请有计划地升级:阅读迁移说明,更新标签,执行 docker compose up -d traefik,并监控日志。如果仍在使用 v2 标签,请参阅 Traefik v2 到 v3 迁移指南。该指南涵盖所有名称变更、兼容模式,以及可保留证书的回滚方法。

备份 ./letsencrypt 以及每个应用的数据卷。Traefik 不保存其他无法通过 compose 文件重建的状态。

规模化时会出现的问题

第一个瓶颈不是吞吐量,而是单台服务器:一台 VPS 上运行的一个 Traefik 是 5 个应用的单点故障,而 acme.json 是平面文件存储,两个 Traefik 实例同时写入会损坏该文件。要横向扩展,就必须将证书存储移出文件,或在其他位置终止 TLS。

第二个问题是长连接。服务器发送事件、大文件上传和慢速客户端可能触发入口点的响应超时;--entryPoints.websecure.transport.respondingTimeouts.readTimeout 及其 writeTimeoutidleTimeout 同类参数用于调整这些超时。WebSocket 无需额外配置即可通过。

第三个问题是磁盘。--accesslog=true 将日志写入标准输出,而 Docker 的 json-file 驱动会永久保留这些日志,除非设置上限。在 Traefik 服务上设置 logging.options.max-size,或者将访问日志写入文件并进行轮换。

这些问题都不需要编排器。服务器必须由您控制,具有真实 IP,并向公网开放 80 和 443 端口;一台小型 VPS 就是全部依赖。

FAQ

我运行 Traefik 后还需要 certbot 吗?

不需要。Traefik 的 ACME 解析器会为它路由的每个主机名申请并续期证书,并将证书统一存储在 acme.json 中。当由 nginx 或其他服务器自行终止 TLS 时,certbot 仍然是合适的工具;让两者同时处理相同的主机名只会消耗 Let's Encrypt 的速率限制额度。

为什么我的容器通过 Traefik 返回 404?

Traefik 返回 404,表示没有任何路由器匹配该请求。请确认容器包含 traefik.enable=true(设置 exposedByDefault=false 后此项为必需),Host() 的值与您输入的名称一致,并确认该应用所有标签中的路由器名称完全相同。Traefik v3 还要求在匹配器中使用反引号,而不是引号。

这里的 404 和 502 有什么区别?

404 表示请求没有进入路由;502 表示路由器已匹配,但后端拒绝了连接。常见的 502 原因包括:容器未连接到 proxy 网络,以及 loadbalancer.server.port 指向了已发布的端口,而不是应用在容器内监听的端口。访问日志会记录 Traefik 实际连接的确切地址。

以只读方式挂载 Docker socket 是否足够?

:ro 标志只会使 socket 文件变为只读,不会限制其背后的 API;POST 请求仍会通过该 socket 传输,而 Docker API 访问权限等同于主机上的 root 权限。更安全的方案是上文所示的 docker-socket-proxy 容器。它只向 Traefik 提供容器和网络的读取权限,并完全阻止写操作。

Traefik 可以签发通配符证书吗?

可以,但只能通过 DNS-01 challenge,并且需要 DNS 提供商的 API token。TLS-ALPN-01 和 HTTP-01 都只能验证单个主机名,无法生成通配符证书。当 Cloudflare 等 CDN 在 VPS 前端终止 TLS,另外两种 challenge 始终无法完成时,也应使用 DNS-01。