1. 为什么 Docker 一跑容器,UFW 就像没开一样
如果你在 Ubuntu 上用过 Docker + UFW,大概率遇到过这个场景:明明ufw status里只放行了 22、80、443,结果docker run -p 8080:80 nginx一执行,外网直接就能访问 8080。防火墙规则看起来完全没拦住它。
这不是 UFW 坏了,而是 Docker 在启动容器时,会往 iptables 的nat表和filter表里插入自己的转发规则,这些规则的优先级在 UFW 的ufw-user-input链之前。数据包走的是 Docker 自己创建的DOCKER链,压根没经过 UFW 的过滤路径。所以你在 UFW 里 deny 什么,对-p发布的端口基本无效。
我试过最直接的验证方式:先ufw default deny incoming,再ufw allow 22/tcp,然后跑一个docker run --rm -p 80:80 nginx:alpine,浏览器访问公网 IP,nginx 欢迎页照样出来。这时候ufw status verbose显示 80 是 deny 的,但服务就是暴露着。
这个问题的核心矛盾在于:Docker 需要 iptables 做端口映射和容器互通,而 UFW 也依赖 iptables 做主机防火墙,两者对链的插入顺序和规则归属没有协调机制。网上常见的“关闭 Docker iptables”方案({"iptables": false})确实能让 UFW 重新管住端口,但代价是容器访问外网、容器间通信、客户端 IP 获取都会出问题,后面还得手动补一堆 iptables 规则,维护成本反而更高。
所以更合理的思路不是“关掉 Docker 的 iptables”,而是“让容器不直接发布端口到公网”。具体做法是:容器只用expose在内部网络暴露端口,由 Traefik 作为唯一入口监听 80/443,UFW 只需要放行 Traefik 的端口。这样 Docker 的 iptables 转发规则只存在于内部网卡之间,公网入口收敛到 Traefik 这一个进程上,UFW 的规则就能真正生效。
这个方案还有一个附带好处:客户端真实 IP 能通过 Traefik 的X-Forwarded-For传递到后端容器,fail2ban 之类的工具可以继续用,日志里也不会全是虚拟网卡地址。
2. TaoToken 统一 Key 接入:让 Traefik 后面的 AI 服务共用一套凭证
在讲 Traefik 配置之前,先说一下为什么这篇会提到 TaoToken。场景是这样的:Traefik 后面往往不止一个 Web 服务,可能还有几个调用大模型 API 的内部工具或 Agent 服务。如果每个服务各自维护一套 API Key,轮换、限额、审计都会很麻烦。TaoToken 提供的是统一 Key 接入层,你可以在它的控制台里生成一个 Key,然后让 Traefik 后面的多个服务共用这个 Key 去调用模型接口,省去每个服务单独配置的重复工作。
TaoToken 的官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。如果你只是想让 Traefik 后面的某个服务快速验证模型调用,可以直接用模型对话页面;如果是长期跑编码类 Agent,建议看 Coding Plan;接入细节和 Key 管理在接入文档和 API Keys 页面里都有说明。
这里要强调的是:TaoToken 在这个架构里扮演的是“统一凭证出口”的角色,不是替代 Traefik 的反向代理功能。Traefik 负责流量入口和路由,TaoToken 负责模型调用的 Key 收敛。两者职责不冲突。
3. 可复制的 Traefik 静态配置与 Docker Compose 骨架
下面给出一份可以直接改域名就用的配置。思路是 Traefik 以容器方式运行,但只发布 80 和 443 到宿主机,其他后端容器一律不写ports,只用expose。
先建目录结构:
mkdir -p /data/traefik/{config,logs,ssl} touch /data/traefik/config/traefik.yml touch /data/traefik/config/dynamic.yml静态配置traefik.yml:
global: sendAnonymousUsage: false log: level: INFO filePath: /logs/traefik.log accessLog: filePath: /logs/access.log api: dashboard: true insecure: false entryPoints: web: address: ":80" http: redirections: entryPoint: to: websecure scheme: https websecure: address: ":443" providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false watch: true file: filename: /config/dynamic.yml watch: true certificatesResolvers: le: acme: email: your@email.com storage: /ssl/acme.json httpChallenge: entryPoint: web动态配置dynamic.yml,这里放中间件和 dashboard 路由:
http: middlewares: security-headers: headers: stsSeconds: 31536000 contentTypeNosniff: true browserXssFilter: true rate-limit: rateLimit: average: 100 burst: 50 routers: dashboard: rule: "Host(`traefik.your.com`)" entryPoints: - websecure service: api@internal middlewares: - security-headers tls: certResolver: leDocker Compose 文件docker-compose.yml:
version: "3.8" services: traefik: image: traefik:v2.11 container_name: traefik restart: unless-stopped ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./config/traefik.yml:/etc/traefik/traefik.yml:ro - ./config/dynamic.yml:/config/dynamic.yml:ro - ./logs:/logs - ./ssl:/ssl networks: - proxy labels: - "traefik.enable=true" whoami: image: traefik/whoami container_name: whoami restart: unless-stopped expose: - "80" networks: - proxy labels: - "traefik.enable=true" - "traefik.http.routers.whoami.rule=Host(`who.your.com`)" - "traefik.http.routers.whoami.entrypoints=websecure" - "traefik.http.routers.whoami.tls.certResolver=le" - "traefik.http.services.whoami.loadbalancer.server.port=80" networks: proxy: name: proxy注意 whoami 服务没有ports字段,只有expose。这意味着它只在proxy这个 Docker 网络内部可达,公网无法直接访问它的 80 端口。Traefik 通过 Docker socket 发现它,并根据 label 自动生成路由。
启动:
cd /data/traefik docker compose up -d4. UFW 放行验证与 iptables 链检查
Traefik 起来之后,UFW 只需要放行 80 和 443。先确认当前规则:
ufw status verbose如果之前没配过,按下面这套来:
ufw default deny incoming ufw default allow outgoing ufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcp ufw enable启用后再次检查:
ufw status numbered应该看到 22、80、443 是 ALLOW,其余默认 DENY。
接下来验证 Docker 的 iptables 规则有没有绕过 UFW。查看 nat 表的 DOCKER 链:
iptables -t nat -L DOCKER -n --line-numbers正常情况你会看到只有 Traefik 容器的 80/443 映射规则,以及 Docker 内部网络的 MASQUERADE 规则。如果看到 whoami 或其他后端容器的端口映射,说明 compose 里写了ports,需要改成expose。
再看 filter 表的转发链:
iptables -L DOCKER-USER -n --line-numbersDOCKER-USER链是 Docker 留给用户自定义规则的位置,优先级在 Docker 自动规则之前。如果你想额外限制某些源 IP 访问 Traefik,可以在这里加规则,比如只允许特定网段访问 443:
iptables -I DOCKER-USER -p tcp --dport 443 ! -s 10.0.0.0/8 -j DROP不过更推荐的做法是保持DOCKER-USER干净,把访问控制交给 Traefik 的中间件(比如 IPWhiteList)或 UFW 的上游规则。
验证端口暴露情况,从外部机器执行:
curl -I http://who.your.com curl -I https://who.your.com如果 who.your.com 解析到这台服务器,应该看到 301 跳转到 https,然后 https 返回 200。再直接试后端容器的内部端口,比如 whoami 的 80:
curl -I http://your-server-ip:80这个请求应该被 Traefik 处理(返回 404 或跳转),而不是直接命中 whoami。如果你用nmap扫一下服务器,除了 22、80、443,不应该看到其他开放端口。
5. nginx 共存与常见报错排查
5.1 nginx 已经占了 80/443 怎么办
如果这台机器上已经有 nginx 在跑,Traefik 再绑 80/443 会报bind: address already in use。两种处理方式:
第一种,把 nginx 改成只监听 127.0.0.1:8080,让 Traefik 反代它。nginx 配置里改:
server { listen 127.0.0.1:8080; server_name legacy.your.com; location / { proxy_pass http://127.0.0.1:3000; } }然后在 Traefik 的 dynamic.yml 里加一个 file provider 的路由:
http: routers: legacy: rule: "Host(`legacy.your.com`)" entryPoints: - websecure service: legacy-svc tls: certResolver: le services: legacy-svc: loadBalancer: servers: - url: "http://127.0.0.1:8080"第二种,如果 nginx 只是做静态文件,直接把它也容器化,用expose接入 Traefik 网络,彻底去掉宿主机上的 nginx 进程。
5.2 常见报错对照
| 报错信息 | 原因 | 处理 |
|---|---|---|
port is already allocated | 80/443 被其他进程占用 | ss -tlnp | grep :80找到进程,停掉或改端口 |
Unable to obtain ACME certificate | 80 端口没放行或域名解析不对 | 检查 UFW 80 规则、DNS A 记录 |
404 page not found | 容器 label 没写对或 exposedByDefault 为 true 但没启用 | 确认traefik.enable=true和 rule 匹配 |
Bad Gateway | Traefik 找不到后端端口 | 检查loadbalancer.server.port是否和容器 expose 一致 |
permission denied /var/run/docker.sock | Traefik 容器没权限读 socket | 挂载时加:ro,或把容器加入 docker 组 |
5.3 验证请求是否真的经过 Traefik
用 whoami 镜像最直观。访问https://who.your.com,返回内容里应该包含:
X-Forwarded-For: 你的公网IP X-Forwarded-Proto: https X-Real-Ip: 你的公网IP如果X-Forwarded-For显示的是172.x.x.x这种内网地址,说明请求没经过 Traefik,而是直接打到了容器。这时候回去检查 compose 里是不是误写了ports。
另外,Traefik 的 access log 在/data/traefik/logs/access.log,每一条请求都会记录来源 IP、状态码、后端服务。你可以用这个日志配合 fail2ban 做扫描器拦截,规则和 nginx 的日志格式类似,改一下正则就能复用。
6. 把入口收敛之后,Key 和流量都各归其位
这套方案跑下来,最明显的变化是ufw status终于和实际暴露的端口一致了。公网只能看到 22、80、443,后端容器全部藏在 Docker 网络里,Traefik 根据 Host 和 Path 做路由。客户端 IP 通过X-Forwarded-For透传,日志里不再是清一色的172.20.0.x。
如果你后面要接入模型调用类的服务,建议把 Key 统一收到 TaoToken 这边管理。API Keys 页面可以生成和轮换 Key,接入文档里有不同语言的调用示例。Traefik 后面的服务只需要配置一个环境变量指向https://taotoken.net/api,不用每个服务单独维护凭证。长期跑编码 Agent 的话,Coding Plan 的额度模型比按次调用更划算,具体可以在模型对话页面先试一下响应格式再决定。
最后提醒一个容易忽略的点:Traefik 的 dashboard 默认走api@internal,如果你在 dynamic.yml 里配了Host(traefik.your.com),记得给这个路由加上认证中间件,比如 basicauth,否则 dashboard 会直接暴露在公网上。basicauth 的密码用htpasswd -nb admin yourpassword生成,填到 middlewares 里即可。