为什么个人站长该认真看看 Traefik
如果你手上有三五台 VPS,跑着十几个容器化的站点和服务,那你多半经历过这样的场景:每加一个服务,就要手动去 Nginx 里补一段 server 块、写一段 proxy_pass、再去 certbot 申请一张证书、reload 一次。服务一多,配置文件就成了没人敢碰的意大利面,谁写的、哪个域名对应哪个上游,全靠注释和记忆。
Traefik 就是来解决这个问题的。它是一个用 Go 写的现代反向代理,核心卖点只有一句话:服务的路由规则和证书,跟着容器自己走,而不是跟着代理的配置文件走。你在 docker-compose 里给容器贴几个 label,Traefik 就自动知道「这个服务的域名是什么、走哪个端口、要不要 HTTPS」,然后自己去申请证书、自己挂载路由。容器起,路由生;容器停,路由灭。
这篇教程我会用一台干净的 VPS,从零搭起一套 Traefik,把 HTTP 自动跳转 HTTPS、Let's Encrypt 自动签证书、多服务按域名分流、以及几个很容易踩的坑,一次讲透。全程 Docker,不污染宿主机。
Traefik 和 Nginx 到底差在哪
先把定位讲清楚,免得你选错工具。Nginx 是「配置驱动」的:路由规则写在 nginx.conf 里,是静态的、需要人工维护的。Traefik 是「发现驱动」的:它连到 Docker socket(或者 K8s、Consul、etcd),实时监听容器和服务的增删,配置是动态生成的。
这不是说 Traefik 比 Nginx 强,而是两者的适用场景不同。如果你只有一两个静态站,Nginx 更简单直接、性能调优空间也更大。但如果你是「容器多、变动频繁、域名多」的运维场景,Traefik 的自动化优势会被放大到肉眼可见:
- 免 reload:新增服务不需要重载代理进程,路由表秒级生效,用户零感知。
- 自动证书:内置 ACME 客户端,域名一贴,Let's Encrypt 证书自动签发、自动续期,过期前自动换。
- Label 即配置:服务的路由规则写在自己身上,做到「谁的服务谁负责」,模板可以复制粘贴。
- 中间件体系:限流、鉴权、重定向、加头,都是可复用的中间件,串在路由上。
反过来说,Traefik 的短板也要心里有数:它的日志和调优不如 Nginx 成熟,超大规模下的极致性能不如手写 Nginx 配置,而且 v2 到 v3 的配置格式改过,网上很多老教程是 v2 语法,照抄会报错。本文用当前主流的 v3 写法。
第一步:准备环境与 Docker 网络
假设你有一台 Ubuntu/Debian 的 VPS,已经装了 Docker 和 docker compose 插件。我们要做的第一件事是建一个公共的 Docker 网络,让 Traefik 和所有被代理的容器都在同一张网里,这样 Traefik 才能通过容器名直接连到上游。
docker network create traefik-net然后准备目录结构。个人站长没必要搞得太复杂,一个目录放 compose 文件和数据卷就够了:
mkdir -p /opt/traefik/data
cd /opt/traefik
# 证书等重要数据会落在 data 目录,稍后里面会生成 acme.json
touch data/acme.json
chmod 600 data/acme.json注意 acme.json 的权限必须是 600。这是 Traefik 的硬性要求——证书私钥文件如果权限过松,Traefik 会直接拒绝启动并在日志里报错,很多人第一次就卡在这里。文件先建好、权限先设好,后面就不会踩。
第二步:一份最小可用的 compose 文件
下面是我推荐的 Traefik v3 起步 compose。它同时开了 80/443 两个入口,把 443 作为安全入口并作为默认入口,这样所有服务默认走 HTTPS:
services:
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./data/acme.json:/acme.json
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--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=/acme.json"
- "--certificatesresolvers.le.acme.tlschallenge=true"
networks:
- traefik-net
networks:
traefik-net:
external: true这里有几个关键点必须讲清楚。第一,exposedbydefault=false 是我强烈建议打开的。默认情况下,任何连接到这张网、暴露了端口的容器都会被 Traefik 自动接上公网——包括你本来只想内网访问的数据库、Adminer、phpMyAdmin。关掉它之后,只有显式打了 traefik.enable=true 标签的服务才会被代理,安全性天差地别。
第二,docker.sock 挂载我加了 :ro 只读。这是最低限度的防护。更严格的做法是用 socket 代理把只读接口再包一层,只暴露 Traefik 需要的那几个 endpoints,避免容器被攻破后拿到宿主机 root。对个人站来说,只读挂载 + 不跑来路不明的镜像,已经挡住了绝大多数风险。
第三,这里用的是 tlschallenge(TLS-ALPN-01 挑战),它直接在 443 端口上完成验证,不需要额外的 80 端口回调,配置最省事。代价是它只能签单域名证书,泛域名要走 DNS challenge。个人站单域名居多,tlschallenge 够用。
第三步:起一个服务试试自动路由
现在起一个最简单的 whoami 服务,验证整条链路。注意它本身没有映射任何端口到宿主机,唯一的入口是 Traefik:
services:
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=le"
networks:
- traefik-net
networks:
traefik-net:
external: true把 whoami.example.com 换成你自己的域名,DNS 的 A 记录先指向这台 VPS,然后 docker compose up -d。几十秒内,Traefik 会:发现这个容器 → 读取 label → 生成一条路由 → 向 Let's Encrypt 申请 whoami.example.com 的证书 → 挂上 TLS。浏览器打开就是一把绿锁。
这里有个高频坑:label 里的反引号。Traefik 的 Host 规则语法是 Host(`域名`),用的是反引号而不是单引号。在 docker-compose 的 YAML 里,如果写成单引号包裹整个字符串,里面的反引号没问题;但如果你的文本编辑器自动把反引号转成了中文标点或单引号,规则就解析失败,Traefik 会静默跳过,表现为「容器起来了但域名打不开,日志里啥也没报」。遇到访问不了先看 label 原文。
第四步:给服务加上常用中间件
Traefik 的中间件(middleware)是可以复用的,定义一次,挂到任意路由上。下面这几个是个人站最常用的。定义方式是在 Traefik 自己的 label 里声明,然后各服务引用:
# 在 traefik 服务的 labels 里添加:
- "traefik.http.middlewares.sec-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.sec-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.sec-headers.headers.frameDeny=true"
- "traefik.http.middlewares.gzip.compress=true"
- "traefik.http.middlewares.rl.ratelimit.average=100"
- "traefik.http.middlewares.rl.ratelimit.burst=50"然后在需要它的服务上串起来。关键:一个路由可以挂多个中间件,多个中间件名字之间用逗号分隔,顺序就是执行顺序:
- "traefik.http.routers.whoami.middlewares=sec-headers,gzip,rl"sec-headers 加了 HSTS 和防嗅探、防嵌套框架;gzip 开压缩;rl 是限流,平均 100 请求/秒、突发 50。中间件一旦定义错了,整个路由会 404,而且 Traefik 不一定报明显错误,所以加的时候一个服务一个服务地加、逐个验证最稳。
一次真实的排查经历
讲个我上个月亲身踩的坑,比干讲配置有用得多。我有一台 VPS 上跑了 Traefik + 五六个服务,某天开始,新加的一个服务怎么都拿不到证书,浏览器报「SSL handshake failed」,但其他老服务一切正常。
排查动作分三步。第一步看 Traefik 日志里 ACME 相关行,发现反复出现 challenge 失败的记录,但错误信息很含糊,只说验证超时。第二步我怀疑是 80 端口,于是从外网 curl -v http://新域名/.well-known/acme-challenge/test,发现请求根本没到 Traefik——被前面那层 CDN 拦截了。原来这个新域名我顺手接入了 CDN 做加速,CDN 把 80 端口的 HTTP 校验请求在边缘就吃掉了,压根没回源,Let's Encrypt 的 HTTP-01 挑战自然通不过。
第三步验证猜想:我把证书解析方式从 HTTP challenge 换成 tlschallenge(在 443 上校验),重新拉起容器,证书当场签发。前因后果清楚了:只要站点前面套了 CDN,就绝不能用 HTTP-01 挑战去签证书,因为校验请求到不了源站。要么用 TLS-ALPN(只要 CDN 不回源 443 就行,多数 CDN 默认回源),要么干脆用 DNS-01 挑战绕开一切网络层限制。这个坑我在文档里没见过明确说明,是靠 curl 层定位出来的。数据对比很直观:加 CDN 前证书秒签,加 CDN 后 HTTP 挑战 100% 失败,换 TLS 挑战后立刻恢复。
方法论提炼:证书签不下来时,先别改 Traefik 配置,先用 curl 从外网确认「挑战请求能不能到达源站的 80/443 端口」。到达不了,问题在网络链路(CDN、防火墙、云安全组)而不在代理本身。这个判断顺序能帮你省掉一半的瞎折腾。
第五步:用 file provider 接非容器服务
Traefik 不只代理容器。如果你有跑在宿主机上、不在 Docker 里的服务(比如某个用 systemd 跑的老应用),可以用 file provider 用 YAML 声明路由,让 Traefik 一起管。配置里加上这个 provider:
- "--providers.file.directory=/etc/traefik/dynamic"
- "--providers.file.watch=true"然后放一个动态配置文件,语法接近但更结构化:
http:
routers:
legacy-app:
rule: "Host(`legacy.example.com`)"
service: legacy-svc
entryPoints: [websecure]
tls:
certResolver: le
services:
legacy-svc:
loadBalancer:
servers:
- url: "http://172.17.0.1:8080"这里 172.17.0.1 是 Docker 默认网桥的宿主地址,容器内的 Traefik 通过它访问宿主机服务。注意这个地址在不同 Docker 网络配置下可能是 172.18.0.1 之类,用 ip addr show docker0 或 docker network inspect 确认。用远程磁盘、自定义网桥的场景下最容易搞错。
file provider 的一个重要特性是 watch=true:你改完 YAML 文件,Traefik 自动重载,不用 restart。这让它变成了一个「容器 + 静态文件」双通道的统一网关。
第六步:监控与排错清单
Traefik 默认的日志级别是 ERROR,排查问题时太安静。日常建议开 INFO,出问题临时开 DEBUG:
- "--log.level=INFO"
- "--accesslog=true"
- "--accesslog.filepath=/var/log/traefik/access.log"access log 会记录每个请求的路由匹配结果。当某个域名 404 时,翻 access log 看它有没有匹配到 router、匹配到哪个 service,几秒钟就能定位是 label 没生效还是上游挂了。
我把最常见的故障整理成一张对照表,遇到问题照着查:
| 现象 | 最可能原因 | 快速验证 |
|---|---|---|
| 域名打不开,容器正常 | label 没生效或反引号写错 | 看 access log 有无 router 匹配 |
| 证书签不下来 | CDN/防火墙吞了挑战请求 | 外网 curl 挑战 URL |
| 502 Bad Gateway | 上游端口写错/不同网络 | 进 Traefik 容器 curl 上游 |
| 启动即退出 | acme.json 权限不是 600 | ls -l 看权限 |
| 只想内网的服务暴露了 | 忘了 exposedbydefault=false | 检查是否被自动代理 |
这张表能覆盖九成的日常问题。尤其是「只想内网的服务暴露了」这一条,是安全事件的高发区,建议每次加服务后都从公网扫一遍自己暴露了哪些域名和端口。
常见问题
Q:Traefik 能取代 Nginx 吗?能代理,但两者可以共存。很多人的做法是 Traefik 做最外层网关(管证书、路由、限流),后端服务内部再各自跑 Nginx 处理静态文件和 rewrite。各司其职,比硬替换省事。
Q:证书续期会断线吗?不会。Traefik 在后台自动续期,新证书就绪后才热加载,期间旧证书继续服务,用户无感。这也是它比手写 certbot + reload 脚本省心的地方。
Q:docker.sock 挂进去安全吗?只读挂载是底线,但要知道只读不等于安全——能读 socket 仍可能通过 API 获取信息。生产环境建议用 socket 代理或 rootless Docker 进一步隔离。
Q:为什么我的路由一直 404?九成是 label 里域名用了单引号而非反引号,或者 traefik.enable=true 忘了写。先看 label 原文,再看 access log。
Q:能挂多个域名吗?可以,rule 里用 || 连接多个 Host:Host(`a.com`) || Host(`b.com`)。证书会自动为每个域名各签一张。
总结
Traefik 把「加一个服务要改配置、申证书、reload」这套繁琐流程,压缩成了「贴三个 label、起容器」这一件事。对容器化程度高、服务变动频繁的个人站长来说,它省下的不是几分钟,而是「配置文件会不会写错、证书会不会过期」这类长期心智负担。
上手路径建议是:先用最小 compose 把 Traefik 跑起来,接一个 whoami 验证自动路由和证书,确认无误后再把真实服务一个个迁移过来。迁移过程中最有价值的习惯是开 access log——路由不生效时,日志比任何猜测都快。等你把限流、压缩、安全头这些中间件也配齐,这套网关就基本能抗住个人站的全部流量,而且以后加服务只需要复制粘贴几行 label,再也不用担心动一个站影响另一个。