Traefik Docker Compose 反向代理配置5个应用
用 Traefik v3 和 Docker Compose 让5个应用共享一个公网 IP:通过 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监听:80,websecure监听:443。 - 路由器匹配请求(
Host(...)),并将请求关联到服务。证书按路由器申请,通过tls.certresolver完成。 - 服务是后端,包括一个容器,以及该容器在 Docker 网络内部监听的端口。
- 中间件位于路由器和服务之间,用于实现基本身份验证、IP 允许列表、请求头重写和重定向。
这四个术语是 Traefik 对原本需要手动编写的配置的命名:路由器相当于一个 server_name,服务相当于一个 proxy_pass 目标,中间件则对应您在逐行构建 nginx 反向代理服务器块时自行设置的请求头和身份验证指令。
静态配置(入口点、providers、ACME)通过 Traefik 的命令行或 traefik.yml 传入。修改静态配置需要重启 Traefik。动态配置(路由器、服务、中间件)来自容器标签,并支持热加载。混淆这两类配置,是出现“我的标志不起作用”问题的常见原因。
Compose 文件
一个名为 proxy 的共享 Docker 网络是整个配置的基础。Traefik 只有在容器与自身都连接到该网络时,才能访问该容器。
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.7
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 上的重定向默认是永久重定向,因此明文 GET 请求(浏览器发送的请求)会收到指向 HTTPS 的 301;对于 POST 等其他方法,Traefik 只返回 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 端口连接到您的服务器,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 会记录一条与以下内容非常接近的日志,并丢弃该解析器。Traefik 仍会运行并转发流量,但不会请求任何证书,因此所有主机名都会提供 TRAEFIK DEFAULT CERT:
ERR The ACME resolve is skipped from the resolvers list error="unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600" resolver=le正确的修复方法就是上面所述的方法:绑定挂载该目录,让 Traefik 自行创建文件,并使用正确的权限模式。如果您使用 touch 创建了 acme.json,umask 会将其权限设置为 644。请在主机上修复:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefik将该目录与应用卷一起备份。丢失一次通常可以恢复,证书会重新签发;但每次重新部署都删除该目录,会触发每 7 天对同一组主机名最多签发 5 张证书的限制。
迭代配置时使用 staging CA。 取消注释 caserver 行,让所有路由正常工作,然后重新注释该行并删除 acme.json,以便重新请求生产证书。生产环境中的 Let's Encrypt 对完全相同的一组主机名每周最多允许 5 张重复证书,并会限制同一名称的重复验证失败。Staging CA 签发不受信任的证书,浏览器会显示警告;该警告说明配置已生效,而且 staging CA 的限制宽松得多。
仪表板是控制界面,不是演示页面
大多数快速入门配置都会设置 --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:2375Traefik 仍可读取容器和网络信息,但无法创建任何对象。
防火墙、端口,以及每个人都会弄错的规则
开放两个端口,另外开放 SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableDocker 发布的端口会绕过 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.rule 和 routers.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.comDNS 尚未指向此服务器。修正记录,等待 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.7,不要使用 latest),并固定到仍在发布版本的分支。截至 2026-10-12,Traefik 将 v3.7 列为唯一受支持的 v3 分支。v3.5 的最后一个版本 v3.5.6 于 7 November 2025 发布,其 Docker provider 将 API 版本硬编码为 1.24。Docker Engine 29 及更高版本会因 client version 1.24 is too old 拒绝该版本,因此不会有容器获得路由。Traefik 3.6.1 增加了 Docker API 版本协商,因此上面的 compose 文件使用 v3.7。从 v2 升级到 v3 后,规则语法和 provider 名称发生了变化;无人值守的 latest 会直接重新加载它已无法理解的配置。请按计划升级:阅读迁移说明、提升标签版本、docker compose up -d traefik,并监控日志。如果仍在使用 v2 标签,请参阅 Traefik v2 到 v3 迁移指南。该指南介绍所有名称变更、兼容模式,以及保留证书的回滚方法。
备份 ./letsencrypt 和每个应用的数据卷。除了可从 compose 文件重建的数据外,Traefik 不保存其他状态。
规模化时会出现的问题
第一个瓶颈不是吞吐量,而是单台服务器:一台 VPS 上运行的一个 Traefik 是 5 个应用的单点故障,而 acme.json 是平面文件存储,两个 Traefik 实例同时写入会损坏该文件。要横向扩展,就必须将证书存储移出文件,或在其他位置终止 TLS。
第二个问题是长连接。服务器发送事件、大文件上传和慢速客户端可能触发入口点的响应超时;--entryPoints.websecure.transport.respondingTimeouts.readTimeout 及其 writeTimeout 和 idleTimeout 同类参数用于调整这些超时。WebSocket 无需额外配置即可通过。
第三个问题是磁盘。--accesslog=true 将日志写入标准输出,而 Docker 的 json-file 驱动会永久保留这些日志,除非设置上限。在 Traefik 服务上设置 logging.options.max-size,或者将访问日志写入文件并进行轮换。
这些问题都不需要编排器。服务器必须由您控制,具有真实 IP,并向公网开放 80 和 443 端口;一台小型 VPS 就是全部依赖。
FAQ
运行 Traefik 后,我还需要 certbot 吗?
不需要。Traefik 的 ACME resolver 会为它路由的每个主机名申请并续期证书,并将所有证书存储在 acme.json 中。当由 nginx 或其他服务器自行终止 TLS 时,Certbot 仍然是合适的工具;让两者为同一组主机名同时申请证书,只会消耗 Let's Encrypt 的速率限制额度。
为什么我的容器通过 Traefik 返回 404?
Traefik 返回 404 表示没有任何 router 匹配该请求。确认容器包含 traefik.enable=true(设置 exposedByDefault=false 后此项为必需),确认 Host() 的值与您输入的名称一致,并确认该应用的所有标签使用完全相同的 router 名称。Traefik v3 还要求 matcher 内使用反引号或转义的双引号;单引号会被拒绝。
这里的 404 和 502 有什么区别?
404 表示请求未进行路由;502 表示某个 router 已匹配,但后端拒绝了连接。常见的 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。