Traefik 反代与自动 HTTPS 统一入口实战:一个 443 端口托管全家容器应用


当你用 Docker 在 VPS 上跑起七八个自建应用(Navidrome、FreshRSS、Calibre-Web、Grafana……),第一个冒出来的问题是:每个容器都暴露一个端口,端口号全靠脑子记,明文 HTTP 到处裸奔,证书还要手动续期。Traefik 解决的不只是"少记几个端口",它是把"HTTPS 证书签发、域名路由、负载均衡、访问控制"这一整套网关能力全部自动化到容器编排里的边缘路由器。下面把它的原理和完整落地拆开讲。

一、EntryPoints、Routers、Services 三件事先想清楚

Traefik 把一条请求的生命周期分成三段,理解这三段,后面所有配置都能顺藤摸瓜:EntryPoints 是流量进网关的端口(如 80、443),Routers 决定"这个请求该交给谁",靠规则匹配(域名、路径等),Services 是真正的后端容器与负载均衡目标。中间件(Middlewares)则挂在 Router 上,在转发前对请求做认证、限流、改头、重定向。三者顺序是 EntryPoint 收到请求 → Router 按规则选择 → 经过 Middleware → 转发给 Service。

这样的解耦带来了两个很实在的好处:一是服务从哪个端口进来完全由 Traefik 决定,业务容器不再需要发布端口;二是证书、路由、中间件都能热更新,改配置不用重启网关。

二、第一版:用 Docker Provider 自动发现服务

Traefik 最常用的是 Docker Provider:它订阅 Docker 的容器事件,凡是打了 Traefik 标签的容器会自动成为可路由的服务。先写网关本身的 compose:

# /opt/traefik/docker-compose.yml
services:
  traefik:
    image: traefik:v3.2
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - ./acme.json:/acme.json

把 /var/run/docker.sock 以只读挂进容器,Traefik 才能感知容器生灭。这一步是整个方案里权限最大的挂载,后面会讲怎么把它收紧。静态配置 traefik.yml 里声明入口点和证书存储:

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"
certificatesResolvers:
  letsencrypt:
    acme:
      email: you@example.com
      storage: /acme.json
      httpChallenge:
        entryPoint: web

exposedByDefault: false 很关键:它要求每个容器显式声明"我愿意被路由",避免把误打标签的容器意外暴露到公网。HTTP 端口统一 301 跳到 HTTPS,证书由 ACME HTTP-01 挑战自动签发与续期,整个生命周期你不用再碰证书文件。

三、给业务容器打标签:一条规则挂上中间件

业务容器只需在标签里告诉 Traefik"我叫什么、用什么域名、要走什么中间件"。以 FreshRSS 为例:

services:
  freshrss:
    image: freshrss/freshrss:latest
    networks: [ frontend ]
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.freshrss.rule=Host(`rss.example.com`)"
      - "traefik.http.routers.freshrss.entrypoints=websecure"
      - "traefik.http.routers.freshrss.tls.certresolver=letsencrypt"
      - "traefik.http.routers.freshrss.middlewares=auth@file"
      - "traefik.http.services.freshrss.loadbalancer.server.port=80"
networks:
  frontend:
    external: true

业务容器统一加入同一个外部网络 frontend,与 Traefik 连通,但自身完全不发布端口——流量只会通过网关进来,这就是"一个 443 入口托管全家"的结构基础。规则里的反引号是 Traefik 的函数式规则语法,`Host()` 之外还能用 `PathPrefix()`、`HostHeader()` 组合,多条用 `&&` 或 `||` 组合。

四、中间件:把认证、限流、安全头从应用里剥离

中间件是 Traefik 最值钱的地方,因为同一个能力(比如基础认证)可以只写一次、给任何应用复用。用文件 Provider 定义共享中间件:

# /opt/traefik/dynamic.yml  (在静态配置中 providers.file.filename 引入)
http:
  middlewares:
    auth:
      basicAuth:
        users:
          - "admin:$2y$05$8O...hash..."          # htpasswd 生成
    ratelimit:
      rateLimit:
        average: 100
        burst: 50
        period: 1s
    secure:
      headers:
        stsSeconds: 31536000
        contentSecurityPolicy: "default-src 'self'"
        customFrameOptionsValue: "SAMEORIGIN"
    redirect:
      redirectScheme:
        scheme: https
        permanent: true

密码是 bcrypt 散列,用 htpasswd 生成后放进来。除了 BasicAuth,Traefik 还内置了 ForwardAuth(对接 OAuth/OIDC 网关)、IPAllowList(白名单)、Headers、Gzip、CircuitBreaker 等一整套中间件,绝大多数"想给应用加个壳"的需求都能在网关层解决,应用代码一行不用改。

五、Multi-service 的坑:同主机多副本与负载均衡

当同一个服务跑了两三个副本时,Traefik 会按容器数自动负载均衡。更精细的用法是通过 addPrefix 或 stripPrefix 处理路径前缀。注意一个高频踩坑点:如果一个 compose 里有多个容器都给同一 Service 补端口,Traefik 会把它们当成一个服务里的多个后端做轮询,可能把请求发给不相干的容器。解决方法是给每个容器独立的 Router/Service 名称,或用 traefik.http.services.{name}.loadbalancer.server.port 显式指定端口,并在路由规则上再做 Host 与 Path 的双重区分。

六、Dashboard 也要锁起来

Dashboard 默认全开,必须叠一个 BaseAuth 或限定内网来源,并且不要单独给它一条对外路由:

labels:
  - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
  - "traefik.http.routers.dashboard.service=api@internal"
  - "traefik.http.routers.dashboard.middlewares=auth"

api@internal 是 Traefik 内置控制面服务,用它配合认证中间件,Dashboard 才对公网安全可见;否则任何能访问你域名的人都能看到每个容器的内部网络拓扑,这是一类非常普遍的暴露源。

七、收紧 Docker Socket 这块"总开关"

把完整 docker.sock 给 Traefik 相当于给了它整台主机的 Root。更稳的做法是只给 Traefik 需要的最小权限:要么用 Taekn/Traefik socket-proxy 这类只读代理过滤 API,要么给容器只挂它真正需要的、控制特定目录的权限,并把 Traefik 所在容器设置为 read_only 根文件系统 + cap_drop: [ALL]。原理上,Provider 只要有"读取容器事件 + 读 metadata 的路由授权"就够了,没必要放开完整的 Docker API。能少一个能力,就少一条被攻破后横向移动的路径。

八、排障方法论

请求"通了又没通"时,先看诊断优先级,不要乱改配置:第一步访问 /api/http/routers 看路由是否已由 provider 生成;第二步看 /metrics 或日志里的 level=debug,确认请求命中了哪条规则;第三步检查中间件链是否把非预期来源挡掉。大多数"502"都出在读不到 Service 端口(容器没入 frontend 网络)而不是规则写错,先 ping 一下容器在 gateway 网络里是否可达,往往比改十条规则更快定位。

结语

把 Traefik 架成全家容器应用的唯一入口,换来的是三个长期收益:端口不再由记忆管理,域名路由与 HTTPS 证书全自动,认证限流这类"改应用太重的需求"下沉到网关批量解决。投入一天把网关搭扎实,后面每加一个新应用只需要贴一段标签,边际成本趋近于零。越过"一台机器跑一个服务"那一步之后,这才是自托管应有的规模感。


手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

Docker 数据与升级之道:持久化、备份与零停机滚动更新

自建 RSS 阅读器:用 FreshRSS 把信息流完全握在自己手里

评 论
更换验证码