本篇文章聊聊如何通过 Docker 容器使用 Traefik,进行稳定的 Traefik 服务的部署。

写在前面

距离 Traefik v2.0.0 的发布,不知不觉快四年了,在过去的四年里,我写过非常多和 Traefik 相关的实践内容,感兴趣的同学可以翻阅这里

上个月官方 Traefik 3.0.0 第三个 beta 版本的发布,3.0 新版本的代码被第二次正式合并进主干分支,距离我们能够正式使用到 3.0 版本,也越来越近了。

相较一个季度前的版本,目前 Traefik 版本变化应该已经接近稳定,为了后面更简单的切换到新版本,或许是时候开始尝试服务迁移了。

正好,尝试详细的写一篇使用 Docker 来使用 Traefik 的内容,帮助还没有入门的同学,或者使用但是还不熟悉的同学查缺补漏。

Traefik 的 Docker 基础容器配置

在展开详细的 Traefik 容器配置和优化调整之前,我们需要先来看看最简的容器配置是什么样的。

Traefik 的 Docker 最简容器配置

最基础的配置不到十行,我们只需要声明 Traefik 服务使用的容器镜像、使用和对外暴露的端口号、以及基础的命令行参数即可。

version: "3"

services:
  traefik:
    image: traefik:v3.0.0-beta3
    ports:
      - 8080:8080
    command: "--api=true --api.dashboard=true --api.insecure=true"

将上面的内容保存为 docker-compose.yml 后,我们使用 docker compose up 启动 Traefik 容器服务,打开浏览器 localhost:8080/dashboard 就能够看到 Traefik 的 Dashboard 啦。

在容器运行的 Traefik 应用

但是,这个容器只能够提供我们查看 Traefik Dashboard 和默认的内部“服务”,不能够提供神奇的“服务发现”和各种“高级自定义”或“服务可观测性”等功能。

所以,我们还需要继续进行配置扩展和调整。

使用 Traefik 进行服务域名绑定

Traefik 最擅长的能力是提供服务发现,即让能够提供对外网络访问能力(HTTP/TCP)的服务使用域名和具体的 URL 地址允许用户访问。

假设我们上文中使用 localhost:8080/dashboard 访问的 Dashboard 是一个正式的服务,有正式的域名,没有“非正式”的端口号,在使用 Traefik 能力的情况下该如何做呢?

比如,我们要使用 traefik.console.lab.io 这个域名来访问上面的服务,那么首先要确保这个域名的指向是 Traefik 服务的网络 IP 地址。你可以通过 DNS 管理工具(包括各种云服务商的网页控制面板)来调整,或者如果你的 Traefik 服务运行在本地,可以修改 /etc/hosts 来完成第一步前置条件。

# 修改 /etc/hosts

127.0.0.1 traefik.console.lab.io

接着,我们对上面的配置进行调整:

version: "3"

services:
  traefik:
    image: traefik:v3.0.0-beta3
    ports:
      - 8080:8080
      - 80:80
    command: "--api=true --api.dashboard=true --api.insecure=true --entrypoints.http.address=:80 --providers.docker=true --providers.docker.endpoint=unix:///var/run/docker.sock"
    labels:
      - "traefik.http.routers.traefik-dashboard.entrypoints=http"
      - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.console.lab.io`)"
      - "traefik.http.routers.traefik-dashboard.service=dashboard@internal"
      - "traefik.http.routers.traefik-dashboard-api.entrypoints=http"
      - "traefik.http.routers.traefik-dashboard-api.rule=Host(`traefik.console.lab.io`) && PathPrefix(`/api`)"
      - "traefik.http.routers.traefik-dashboard-api.service=api@internal"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

使用上面的内容,更新之前保存的 docker-compose.yml 文件,再次使用 docker compose up 启动 Traefik 容器服务,我们除了还能够使用浏览器访问 localhost:8080/dashboard 来访问 Dashboard 之外,与此同时,也能够使用 traefik.console.lab.io 这个域名来访问服务啦。

使用域名来访问服务

在上面的配置中,我们首先增加了容器暴露的端口 80:80,并在 Traefik 启动参数中添加了 --entrypoints.http.address=:80 参数,创建了一个名为 http 的网络入口。

接着,我们在 Docker Volumes 中将本地的 docker.sock 和容器中的 sock 文件进行了映射 /var/run/docker.sock:/var/run/docker.sock:ro,允许 Traefik 订阅 Docker 服务事件,来动态的添加或删除要对用户暴露的网络服务,在启动参数中,也添加了对应的内容 --providers.docker=true --providers.docker.endpoint=unix:///var/run/docker.sock

最后,通过在 Docker Labels 中添加了声明式的路由,分别将 Dashboard 的网页(路由名称 traefik-dashboard)和 API (路由名称 traefik-dashboard-api)注册在了我们创建的 http 网络入口上,用户就可以通过我们设置的域名来访问服务了。

这里的 service=dashboard@internalservice=api@internal 是 Traefik 的内部服务别名,在日常使用过程中,我们可以使用 Docker Compose 中的 Service NameContainer Name、具体的 IP:端口号 来进行替换。

通过类似上面的方式,我们能够实现通过不同的域名,而非端口号来访问我们的网络服务,只需要根据实际需求,创建不同的路由名称和地址规则即可。

减少对外暴露的端口

在上面的配置中,我们能够通过两种方式来访问相同的服务。

实际使用过程中,除非我们需要进行调试,否则只通过 Traefik 提供服务注册的域名来进行服务访问,显然是更好的模式。并且,在这个过程中,我们能够在我们声明的路由中添加各种各样的额外操作:添加认证、修改请求头、修改响应内容、进行重定向、进行限流、进行访问限制等等。

所以,我们可以将上面暴露的端口从下面的内容:

ports:
  - 8080:8080
  - 80:80

调整为:

ports:
  - 80:80

优化 Traefik 命令行配置写法

在上面的配置中,我们操作 Traefik 的命令行的书写方式是这样的:

command: "--api=true --api.dashboard=true --api.insecure=true --entrypoints.http.address=:80 --providers.docker=true --providers.docker.endpoint=unix:///var/run/docker.sock"

实际使用过程中,我们会使用二十余个参数,如果使用上面的写法进行书写,那么将是一大坨文本混在一起,非常不利于后续调试和调整,不过我们可以使用一个 小技巧,来进行改善:

command:
  - "--api=true"
  - "--api.dashboard=true"
  - "--api.insecure=true"
  - "--entrypoints.http.address=:80"
  - "--providers.docker=true"
  - "--providers.docker.endpoint=unix:///var/run/docker.sock"

添加 Traefik 服务的健康检查

和其他所有的想要稳定运行的网络服务一样,为了服务运行稳定省心,我们需要进行定期的 Health Check,并在服务不健康的时候,重新启动服务,来保证服务能力。

Traefik 本身内置了服务检查的接口,我们可以通过在 command 参数中添加 --ping=true 来启用 /ping 路由接口。

接着在 docker-compose.yml 中添加下面的内容,让 Traefik 能够每 3 秒进行一次服务自检,当连续十次检查失败之后(30秒),告诉 Docker 服务状态是异常的:

healthcheck:
  test: ["CMD-SHELL", "wget -q --spider --proxy off localhost:8080/ping || exit 1"]
  interval: 3s
  retries: 10

为了让服务能够遇到问题自动重启,我们可以添加下面的内容到配置中:

restart: always

完整的配置如下:

version: "3"

services:
  traefik:
    image: traefik:v3.0.0-beta3
    restart: always
    ports:
      - 80:80
    command:
      - "--api=true"
      - "--api.dashboard=true"
      - "--api.insecure=true"
      - "--ping=true"
      - "--entrypoints.http.address=:80"
      - "--providers.docker=true"
      - "--providers.docker.endpoint=unix:///var/run/docker.sock"
    labels:
      - "traefik.http.routers.traefik-dashboard.entrypoints=http"
      - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.console.lab.io`)"
      - "traefik.http.routers.traefik-dashboard.service=dashboard@internal"
      - "traefik.http.routers.traefik-dashboard-api.entrypoints=http"
      - "traefik.http.routers.traefik-dashboard-api.rule=Host(`traefik.console.lab.io`) && PathPrefix(`/api`)"
      - "traefik.http.routers.traefik-dashboard-api.service=api@internal"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy off localhost:8080/ping || exit 1"]
      interval: 3s
      retries: 10

再次使用 docker compose up 启动服务之后,我们新开一个命令行窗口,执行 docker compose ps 就能够看到类似下面的日志输出了:

NAME                            IMAGE                  COMMAND                  SERVICE             CREATED             STATUS                    PORTS
traefikconsolelabio-traefik-1   traefik:v3.0.0-beta3   "/entrypoint.sh --ap…"   traefik             18 seconds ago      Up 17 seconds (healthy)   0.0.0.0:80->80/tcp

日志中的 (healthy) 说明服务是运行在健康状态的。

当然,在添加 healthcheck 声明之后,如果服务状态还不为健康状态时,Traefik 是不会将服务进行对外注册和暴露的(不可访问)。

所以,如果我们注册了一个服务到 Traefik,并要求使用具体域名和路径提供对外服务,但是始终访问不到服务,除了 Typo 别字问题外,最大的可能就是服务健康状态不是 “healthy” 的。

使用 Traefik 内置中间件:压缩网页内容

前面提到了,我们在注册服务路由上“叠buff”,下面我们来使用 Traefik 内置中间件能力来对网页内容进行压缩,只需要在配置中先添加一行,定义一个名为 gzip 的中间件:

labels:
  - "traefik.http.middlewares.gzip.compress=true"

定义完中间件之后,我们就可以在之前定义好的路由中添加这个服务了:

labels:
  - "traefik.http.routers.traefik-dashboard.middlewares=gzip@docker"
  - "traefik.http.routers.traefik-dashboard-api.middlewares=gzip@docker"

因为我们的 gzip 服务是写在 Docker 的配置文件中的,为了使用的严谨,这里在调用中间件的时候,推荐加上 @docker 后缀,要求服务从 “Docker 中”定义的中间件里查找能够使用的中间件。(因为我们也可以在文件中定义中间件)

压缩后的请求内容

再次访问服务,能够发现我们的页面已经被压缩过了,网页访问速度也得到了提升。

搭配外部软件提供 HTTPS 服务

上面的配置和实验中,我们能够使用传统的 HTTP 的方式来访问不同的网络服务,但是,在很多程序的场景需要中,不论是因为数据安全需要,还是因为包括 Server Push 等功能,我们是需要启用 HTTPS 的。

想要让 Traefik 提供 HTTPS 服务,本质上我们需要让 Traefik 或者 Traefik “身前”的其他服务,正确挂载 HTTPS 证书。

在详细展开 HTTPS 配置之前,我们先聊两种简单,用于生产环境有极高性能的玩法。

搭配云服务商的负载均衡软件使用

先从最简单的方式聊起。

包括阿里云、腾讯云等服务商的负载均衡服务,都支持挂载 HTTPS 证书,我们只需要在挂载好 HTTPS 证书的云服务后,设置上游端口为我们的 Traefik 服务器地址的 80 端口即可。

这里我们不需要使用自己的服务器来处理 HTTPS 握手、证书解析等等计算,所有的计算机算力资源都能够用在服务上,所以效率最高。

搭配 Nginx 和现成的证书提供 HTTPS 访问

有一些同学之前会注册或购买 HTTPS 证书,然后搭配 Nginx 进行使用。和上面使用云服务商类似,我们的 Nginx 充当了 “负载均衡网关”。我们只需要“配置 Nginx HTTPS 证书” 和 “Nginx 反向代理地址为 Traefik 服务地址:80 端口”即可。

由于 Nginx 默认不像 Traefik 一样支持动态注册服务,并且是使用 C 针对 WEB 服务场景进行了大量特别优化的软件,所以性能相比单纯使用 Traefik 会高非常多,如果我们的访问服务域名固定,这个方法不失为一个非常好的性能优化技巧。

使用 Traefik 提供 HTTPS 访问服务

接下来,我们来聊聊如何让 Traefik 直接提供 HTTPS 服务,不需要借助外部软件,虽然性能会下降一些,但是胜在维护成本低、代码配置更内聚,方便迁移和管理。

为 Traefik 创建 HTTPS 服务接口

想要提供 HTTPS 服务,创建 Traefik HTTPS 接口和准备证书缺一不可。先来看看如何在 Traefik 中创建 HTTPS 服务接口。

因为默认的 HTTPS 服务端口为 443,所以我们可以在配置的端口中增加提供外部访问的容器中端口:

ports:
  - 443:443

在上面的内容中,我们定义了 80 端口,举一反三,我们可以定义一个名为 https443 端口:

command:
  - "--entrypoints.https.address=:443"

然后,我们可以增加或修改原来的服务路由使用的 entrypoints 接口:

labels:
  - "traefik.http.routers.traefik-dashboard.entrypoints=http"
  - "traefik.http.routers.traefik-dashboard-api.entrypoints=http"

http 调整为 https

labels:
  - "traefik.http.routers.traefik-dashboard.entrypoints=https"
  - "traefik.http.routers.traefik-dashboard-api.entrypoints=https"

当然,除了简单的改名字之外,我们还需要额外增加一个配置声明 tls=true

labels:
  - "traefik.http.routers.traefik-dashboard.entrypoints=https"
  - "traefik.http.routers.traefik-dashboard.tls=true"
  - "traefik.http.routers.traefik-dashboard-api.entrypoints=https"
  - "traefik.http.routers.traefik-dashboard-api.tls=true"

如果我们不设置 tls=true,那么 Traefik 其实是不会在我们的端口上启用 TLS 来对内容进行响应的,换言之,没有这个标记的网络接口,就是普通的 HTTP 响应。(例如我们可以在 443 端口提供的 HTTP 服务)。

好了,了解了该怎么调整配置之后,我们来解决证书,让 HTTPS 服务更完善。

使用 Traefik 和 DNS 服务自动完成 HTTPS 证书申请和服务

和绝大多数的现代的 HTTP 服务器一样,Traefik 也支持我们直接注册和使用 let’s encrypt 免费的证书,来提供 HTTPS 服务。

关于这部分,本篇文章就只展开如何使用能够通过 Cloudflare 修改域名记录的服务,更多的域名服务商的相关内容,有必要单独写一篇文章来讲。

我们首先在配置中添加下面三个环境变量:

environment:
  - CF_API_EMAIL=${CF_DNS_EMAIL}
  - CLOUDFLARE_DNS_API_TOKEN=${CF_API_TOKEN}
  - CLOUDFLARE_ZONE_API_TOKEN=${CF_API_TOKEN}

CF_API_EMAIL 是我们的 Cloudflare 账号邮箱,剩下的两个 *_API_TOKEN 则可以从 Cloudflare 控制面板中创建。

接着,我们需要在 Traefik 命令行中添加命令,让 Traefik 能够按照我们的要求,向指定的域名 DNS 服务商 Cloudflare 申请证书,并将证书保存在我们想要的目录中,这里同样别忘记修改 email 字段。

command:
  - "--certificatesresolvers.le.acme.email=${CF_DNS_EMAIL}"
  - "--certificatesresolvers.le.acme.storage=/certs/acme.json"
  - "--certificatesresolvers.le.acme.dnsChallenge.resolvers=1.1.1.1:53,8.8.8.8:53"
  - "--certificatesresolvers.le.acme.dnsChallenge.provider=cloudflare"
  - "--certificatesresolvers.le.acme.dnsChallenge.delayBeforeCheck=30"

最后,在我们要提供 HTTPS 的服务的网络路由中进行下面的配置:

labels:
  - "traefik.http.routers.traefik-dashboard- secure.tls.certresolver=le"
  - "traefik.http.routers.traefik-dashboard- secure.tls.domains[0].main=${CF_DNS_MAIN}"
  - "traefik.http.routers.traefik-dashboard- secure.tls.domains[0].sans=${CF_DNS_LIST}"

上面变量中的 CF_DNS_MAIN 需要替换为你想申请(你拥有的域名)的根域名(example.com)或二级域名(console.example.com),CF_DNS_LIST 则可以写一串域名,比如 *.console.example.com,*.data.example.com,*.exmaple.com,在完成配置更新之后,我们使用 docker compose up 启动服务,在稍等片刻之后,Traefik 会自动注册好我们所需要的域名的证书,让我们能够通过上面域名列表内的域名访问我们的服务。

为了让 Traefik 能够记住我们花费时间申请好的证书,我们需要将容器的文件做持久化存储:

volumes:
  - ./certs/:/certs/:ro

在上面的一通操作之后,我们的 Traefik 就能够自动的申请和使用 HTTPS 证书了,并且会在证书快过期之前,自动更新证书。

完整的配置文件,你可以参考 GitHub 中早些时候提交的配置例子

使用 Traefik 和现成证书提供服务

通过云服务商购买或免费申请 HTTPS 证书也好、通过类似上面的 let’s encrypt 注册证书工具进行证书注册和保存,或者进行自签名证书生成也罢,我们都能够得到提高服务所需要的证书文件。

这里,我们使用最简单和自由度最高的方案来进行接下来的配置的讲解:自签名证书。

几年前,我写过一个简单的,只有 4MB 大小的容器工具:soulteary/certs-maker,使用它可以快速的生成任意域名的 HTTPS 证书,搭配一些 DNS 设置,比如公司内的 DNSMASQ 等私有 DNS 服务器设置或修改 /etc/hosts,我们可以让 Traefik 支持任意服务的任意域名的 HTTPS 访问,比如你可以提供一个页面上有一个苹果的服务,通过 https://www.apple.com 来访问它。

虽然使用 Docker 命令行可以看起来更短小精悍的生成配置,但考虑到清晰可读,我们还是创建一个 docker-compose.certs.yml 的文件,来帮助我们生成 HTTPS 证书吧。

version: "2"
services:
  certs-maker:
    image: soulteary/certs-maker:v3.2.0
    environment:
      - CERT_DNS=lab.io,*.lab.io,*.console.lab.io
    volumes:
      - ./ssl:/ssl

使用 docker compose -f docker-compose.certs.yml up,在执行命令的目录中,就能够看到新鲜的被生成出来的证书文件和配置了。

├── lab.io.conf
├── lab.io.crt
└── lab.io.key

想要让我们的 Traefik 正确的使用证书,提供浏览器支持的 TLS 传输能力,我们需要先创建一个 tls.toml 配置文件:

[tls]
  [tls.options.default]
  minVersion = "VersionTLS12"
  sniStrict = true
  cipherSuites = [
    # TLS 1.3
    "TLS_AES_128_GCM_SHA256",
    "TLS_AES_256_GCM_SHA384",
    "TLS_CHACHA20_POLY1305_SHA256",
    # TLS 1.2
    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
  ]

  [tls.stores.default.defaultCertificate]
  certFile = "/certs/lab.io.crt"
  keyFile = "/certs/lab.io.key"

  [[tls.certificates]]
  certFile = "/certs/lab.com.crt"
  keyFile = "/certs/lab.com.key"

  [[tls.certificates]]
  certFile = "/certs/lab.io.crt"
  keyFile = "/certs/lab.io.key"

在上面的配置中,我们定义了 TLS 服务最低版本,以及 1.2 和 1.3 版本 TLS 使用的相对安全的算法,以及设置了 Traefik 的证书路径。(你可以参考这个例子增加更多的不同域名的证书)

接着,我们来调整文件目录,将 tls.toml 配置文件,放在 config/tls.toml ,将刚刚生成在 ssl 目录中的证书们,移动到 certs 目录中。如果你希望你的设备能够像使用付费证书、或者申请的公网证书一样,直接访问服务,而不是在浏览器中允许访问不受信任的证书,那么需要在系统或浏览器中信任自签名证书(非常简单,可以搜索一下 :D )。

然后,我们将目录映射到容器环境中:

volumes:
  - ./certs/:/certs/:ro
  - ./config/:/etc/traefik/config/:ro

并在 command 命令中添加能够读取目录中 tls.toml 等配置的参数:

command:
  - "--providers.file.directory=/etc/traefik/config"

和上文中一样在 labels 中调整之前的路由的 entrypoints 和增加 tls=true 配置:

- "traefik.http.routers.traefik-dashboard.entrypoints=https"
- "traefik.http.routers.traefik-dashboard.tls=true"

我们将上文中提供 HTTP 服务的配置进行调整,完成配置如下:

version: "3"

services:
  traefik:
    image: traefik:v3.0.0-beta3
    restart: always
    ports:
      - 443:443
    command:
      - "--api=true"
      - "--api.dashboard=true"
      - "--api.insecure=true"
      - "--ping=true"
      - "--entrypoints.https.address=:443"
      - "--providers.docker=true"
      - "--providers.docker.endpoint=unix:///var/run/docker.sock"
      - "--providers.file.directory=/etc/traefik/config"
    labels:
      - "traefik.http.middlewares.gzip.compress=true"
      - "traefik.http.routers.traefik-dashboard.middlewares=gzip@docker"
      - "traefik.http.routers.traefik-dashboard-api.middlewares=gzip@docker"
      - "traefik.http.routers.traefik-dashboard.entrypoints=https"
      - "traefik.http.routers.traefik-dashboard.tls=true"
      - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.console.lab.io`)"
      - "traefik.http.routers.traefik-dashboard.service=dashboard@internal"
      - "traefik.http.routers.traefik-dashboard-api.entrypoints=https"
      - "traefik.http.routers.traefik-dashboard-api.tls=true"
      - "traefik.http.routers.traefik-dashboard-api.rule=Host(`traefik.console.lab.io`) && PathPrefix(`/api`)"
      - "traefik.http.routers.traefik-dashboard-api.service=api@internal"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./certs/:/certs/:ro
      - ./config/:/etc/traefik/config/:ro
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy off localhost:8080/ping || exit 1"]
      interval: 3s
      retries: 10

使用 docker compose up 启动服务之后,我们就能够通过 https://traefik.console.lab.io 来访问服务了。

使用的自签名证书的详细信息

在浏览器的证书信息选项卡里,我们能够看到这张自签名证书的详细信息,如果你想进行信息的定制化,可以参考 certs-maker 项目文档调整生成证书使用的参数。

同时提供 HTTP 和 HTTPS 服务

在上面的文章中,我们为了节约代码篇幅,和减少不必要的理解成本,分别实现了几种 HTTP 和 HTTPS 服务的实现方式,下面我们来将配置组合在一起,完成一个完整的服务配置。

首先是定义 commandports,让 Traefik 能够同时提供 80 端口的 HTTP 服务,和 443 端口的 HTTPS 服务。

ports:
  - 80:80
  - 443:443
command:
  - "--api=true"
  - "--api.dashboard=true"
  - "--api.insecure=true"
  - "--ping=true"
  - "--entrypoints.http.address=:80"
  - "--entrypoints.https.address=:443"
  - "--providers.docker=true"
  - "--providers.docker.endpoint=unix:///var/run/docker.sock"
  - "--providers.file.directory=/etc/traefik/config"

上面的配置让 Traefik 拥有了提供服务的基础能力,但是没有服务内容,所以接下来我们创建能够提供服务内容的路由配置:

labels:
  - "traefik.http.routers.traefik-dashboard.entrypoints=http"
  - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.console.lab.io`)"
  - "traefik.http.routers.traefik-dashboard.service=dashboard@internal"

  - "traefik.http.routers.traefik-dashboard-api.entrypoints=http"
  - "traefik.http.routers.traefik-dashboard-api.rule=Host(`traefik.console.lab.io`) && PathPrefix(`/api`)"
  - "traefik.http.routers.traefik-dashboard-api.service=api@internal"

  - "traefik.http.routers.traefik-dashboard-secure.entrypoints=https"
  - "traefik.http.routers.traefik-dashboard-secure.tls=true"
  - "traefik.http.routers.traefik-dashboard-secure.rule=Host(`traefik.console.lab.io`)"
  - "traefik.http.routers.traefik-dashboard-secure.service=dashboard@internal"

  - "traefik.http.routers.traefik-dashboard-api-secure.entrypoints=https"
  - "traefik.http.routers.traefik-dashboard-api-secure.tls=true"
  - "traefik.http.routers.traefik-dashboard-api-secure.rule=Host(`traefik.console.lab.io`) && PathPrefix(`/api`)"
  - "traefik.http.routers.traefik-dashboard-api-secure.service=api@internal"

因为我们要同时满足网页服务和接口服务都能够支持 HTTP 和 HTTPS,所以这里配的内容看起来重复率比较高,但其实细节上还是有差异的,首先是每个路由的名称是不同的,其次是前文中提到的 tls=trueentrypoints 的设置。

重新启动服务,我们就能够使用 http://traefik.console.lab.iohttps://traefik.console.lab.io 访问服务了。

使用中间件来改进服务访问体验

在上面的配置中,我们的冗余内容其实挺多的,虽然 Traefik 1.x 之后没辙,就得这样。不过,如果我们服务只需要 HTTPS 访问,当用户访问 HTTP 协议的时候(直接在某些浏览器中敲域名,回车后),HTTP 协议自动跳转 HTTPS ,上面的配置就能够精简很多了。

我们先定义一个能够将服务协议从 HTTP 自动切换为 HTTPS 的 Traefik 中间件规则:

- "traefik.http.middlewares.redir-https.redirectscheme.scheme=https"
- "traefik.http.middlewares.redir-https.redirectscheme.permanent=false"

然后,在 HTTP 网页服务的路由上添加这个中间件规则:

- "traefik.http.routers.traefik-dashboard.middlewares=redir-https@docker"

重启服务,当我们访问 http://traefik.console.lab.io 时,就会自动的跳转到 https://traefik.console.lab.io 啦。

使用 HTTP 协议自动跳转的小技巧

在本文的例子中,我们的服务同时提供 HTTP 和 HTTPS 访问,分别有两个路由:网页和 API。

如果用户直接访问 HTTP,浏览器会收到 HTTP 302 响应,不会提供服务内容,不会访问到 HTTP 协议的 API 路由,所以,我们可以将需要跳转 HTTPS 的网页服务会调用的 HTTP API 的路由删除掉:

labels:
  - "traefik.http.middlewares.redir-https.redirectscheme.scheme=https"
  - "traefik.http.middlewares.redir-https.redirectscheme.permanent=false"

  - "traefik.http.routers.traefik-dashboard.middlewares=redir-https@docker"

  - "traefik.http.routers.traefik-dashboard.entrypoints=http"
  - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.console.lab.io`)"
  - "traefik.http.routers.traefik-dashboard.service=dashboard@internal"

  - "traefik.http.routers.traefik-dashboard-secure.entrypoints=https"
  - "traefik.http.routers.traefik-dashboard-secure.tls=true"
  - "traefik.http.routers.traefik-dashboard-secure.rule=Host(`traefik.console.lab.io`)"
  - "traefik.http.routers.traefik-dashboard-secure.service=dashboard@internal"

  - "traefik.http.routers.traefik-dashboard-api-secure.entrypoints=https"
  - "traefik.http.routers.traefik-dashboard-api-secure.tls=true"
  - "traefik.http.routers.traefik-dashboard-api-secure.rule=Host(`traefik.console.lab.io`) && PathPrefix(`/api`)"
  - "traefik.http.routers.traefik-dashboard-api-secure.service=api@internal"

因为我们的网页服务其实也并不会调用到背后真实的程序进行计算,所以这里定义真实的服务多多少少会涉及到 Traefik 寻找和匹配真实服务网络地址的计算,我们可以使用 Traefik 内部的一个“魔术变量”来进行服务替换,将真实服务替换为一个空的服务。

labels:
  - "traefik.http.routers.traefik-dashboard.entrypoints=http"
  - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.console.lab.io`)"
  - "traefik.http.routers.traefik-dashboard.service=noop@internal"

进阶:完善 Traefik 细节配置

搞定 HTTP 和 HTTPS 服务后,我们来了解进阶的配置优化方法。

显式声明所有静态配置参数

有很多文章会使用 Traefik 配置文件来管理服务行为和能力,就我个人的使用经验和观点来看,Traefik 支持的动态配置,我们可以通过文件来管理,而静态配置,使用本文中提到的参数化的方式来管理,更为合理:

  1. 动态化参数是可选的,不影响服务核心能力。
  2. 动态化参数可以通过文件下发,来完成服务行为更新。
  3. 静态化参数和服务配置在一起,可以避免把静态化参数写在配置中,服务启动后,静态配置调整更新,服务重启前,配置和服务行为不一致的问题。

3.0 版本的 Traefik 支持的所有的静态配置文件,可以参考这个在线文档来使用和调整

虽然有很多参数默认是 false 、“空”等我们不设置也没问题的数值,但是为了避免 Traefik 程序版本升级,调整默认行为,对我们造成服务行为预期不符的问题,建议将所有的使用到的相关配置都进行显式的声明:

command:
  - "--global.sendanonymoususage=false"
  - "--global.checknewversion=false"
  - "--entrypoints.http.address=:80"
  - "--entrypoints.https.address=:443"
  - "--api=true"
  - "--api.insecure=true"
...

创建一个专用于 Traefik 网络服务发现的虚拟网络

Traefik 默认会使用当前 Traefik 应用服务的网络,来进行服务发现,简单来说,我们得将各种要提供公开服务的软件都写在和 Traefik 服务所在的 docker-compose.yml 中,然后通过 docker compose up 命令,来管理服务。

这样的模式“不科学”,一来是可能影响到整体服务,比如错误调整和修改了不需要变更的配置,比如 Traefik 的内容;二来,服务想生效,总归要重启服务,可能会造成服务的短暂中断;三来,多种不同的服务配置代码都写一块。代码配置长不说,也不便于管理以及 CI/CD 集成使用。

为了解决这个问题,我们可以使用 Traefik 虚拟网络来解决问题,首先是通过命令行创建一个 Traefik 用来服务发现使用的traefik 虚拟网络:

docker network create traefik

接着,在需要 Traefik 提供服务发现的应用中添加下面的字段,让应用在网络中:

networks:
  - traefik

然后,在 docker-compose.yml 配置的末尾,声明这个 traefik 网络是容器外的独立的网络,而不是根据当前的服务配置文件,创建一个只在这个配置文件作用范围的虚拟网络。

networks:
  traefik:
    external: true

因为 Docker 容器中时常会有多个虚拟网络,所以我们需要在 command 中指定要使用的网络名称:

command:
  - "--providers.docker.network=traefik"

为了避免 Traefik 智能的自动解析和将所有在 Traefik 网络的服务都尝试进行公开服务,我们可以在命令中添加下面的命令,让 Traefik 只对我们在 labels 中声明了要进行服务注册的应用提供服务:

"--providers.docker.exposedbydefault=false"

labels 中,我们的定义写法是这样的:

labels:
  - "traefik.enable=true"

这里还有一些小技巧,比如对计划提供发现服务的进行进一步的筛选,或者针对每一个服务,调整要使用 Traefik 进行服务的虚拟网络,我们未来的文章里再聊。

调整容器服务端口

在上面的文章中,我们为了行文简单,使用了端口暴露的简写模式,为了能够让 Traefik 在容器中也能够取到正确的访问客户端的 IP 地址,我们需要将 ports 调整为下面的写法:

ports:
  - target: 80
    published: 80
    protocol: tcp
    mode: host
  - target: 443
    published: 443
    protocol: tcp
    mode: host

避免 Traefik 进行数据上报

想要避免 Traefik 进行数据上报,我们可以通过设置下面两个 command 参数实现:

command:
  - "--global.sendanonymoususage=false"
  - "--global.checknewversion=false"

如果你还不放心,可以继续设置下面的配置,让容器访问不到下面的 API 地址:

extra_hosts:
  # https://github.com/traefik/traefik/blob/master/pkg/version/version.go#L64
      - "update.traefik.io:127.0.0.1"
  # https://github.com/containous/traefik/blob/master/pkg/collector/collector.go#L20
  - "collect.traefik.io:127.0.0.1"
  - "stats.g.doubleclick.net:127.0.0.1"

避免容器服务日志体积过大

类似 Traefik 这类 HTTP 服务,在长时间运行后,都会积累比较多的日志内容,哪怕我们关闭了日志文件保存功能,只让服务在 stdout 中进行日志打印,Docker 会将输出内容都进行完整保存。

为了缓解这个问题,我们可以通过在将日志单纯对外保存之后,使用 Docker 的 Log 配置参数,来自动丢弃过大的日志输出:

logging:
  driver: "json-file"
  options:
    max-size: "1m"

比如上面的配置中,将自动丢弃超过 1MB 的日志输出。

最终的容器配置文件

好了,我们将上面的所有内容进行合理组合,不难得到最终的配置(相关代码已上传至 soulteary/Home-Network-Note/tree/master/example/traefik-v3.0.0):

version: "3"

services:
  traefik:
    image: traefik:v3.0.0-beta3
    restart: always
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
    command:
      - "--global.sendanonymoususage=false"
      - "--global.checknewversion=false"
      - "--api=true"
      - "--api.dashboard=true"
      - "--api.insecure=true"
      - "--api.debug=false"
      - "--ping=true"
      - "--log.level=INFO"
      - "--log.format=common"
      - "--accesslog=false"
      - "--entrypoints.http.address=:80"
      - "--entrypoints.https.address=:443"
      - "--providers.docker=true"
      - "--providers.docker.watch=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.endpoint=unix:///var/run/docker.sock"
      - "--providers.docker.useBindPortIP=false"
      - "--providers.docker.network=traefik"
      - "--providers.file=true"
      - "--providers.file.watch=true"
      - "--providers.file.directory=/etc/traefik/config"
      - "--providers.file.debugloggeneratedtemplate=true"
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"

      - "traefik.http.middlewares.gzip.compress=true"
      - "traefik.http.middlewares.redir-https.redirectscheme.scheme=https"
      - "traefik.http.middlewares.redir-https.redirectscheme.permanent=false"

      - "traefik.http.routers.traefik-dashboard.middlewares=redir-https@docker"
      - "traefik.http.routers.traefik-dashboard-secure.middlewares=gzip@docker"
      - "traefik.http.routers.traefik-dashboard-api-secure.middlewares=gzip@docker"

      - "traefik.http.routers.traefik-dashboard.entrypoints=http"
      - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.console.lab.io`)"
      - "traefik.http.routers.traefik-dashboard.service=noop@internal"

      - "traefik.http.routers.traefik-dashboard-secure.entrypoints=https"
      - "traefik.http.routers.traefik-dashboard-secure.tls=true"
      - "traefik.http.routers.traefik-dashboard-secure.rule=Host(`traefik.console.lab.io`)"
      - "traefik.http.routers.traefik-dashboard-secure.service=dashboard@internal"

      - "traefik.http.routers.traefik-dashboard-api-secure.entrypoints=https"
      - "traefik.http.routers.traefik-dashboard-api-secure.tls=true"
      - "traefik.http.routers.traefik-dashboard-api-secure.rule=Host(`traefik.console.lab.io`) && PathPrefix(`/api`)"
      - "traefik.http.routers.traefik-dashboard-api-secure.service=api@internal"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./certs/:/certs/:ro
      - ./config/:/etc/traefik/config/:ro
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy off localhost:8080/ping || exit 1"]
      interval: 3s
      retries: 10
    logging:
      driver: "json-file"
      options:
        max-size: "1m"

networks:
  traefik:
    external: true

最后

本篇文章就先写到这里啦,不出意外,应该是你能够在网上找到的最简单和最详尽的关于 Traefik 的教程啦。

接下来的这个系列,我想逐步分享过去几年中使用 Traefik 的一些小经验,包括服务鉴权、用户登陆、自动扩缩容、服务监控等。

–EOF