本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2023年07月18日 统计字数: 22553字 阅读时间: 46分钟阅读 本文链接: https://soulteary.com/2023/07/18/traefik-v3-docker-comprehensive-user-guide-basics.html ----- # Traefik v3.0 Docker 全面使用指南:基础篇 本篇文章聊聊如何通过 Docker 容器使用 Traefik,进行稳定的 Traefik 服务的部署。 ## 写在前面 距离 [Traefik v2.0.0 的发布](https://github.com/traefik/traefik/releases/tag/v2.0.0),不知不觉快四年了,在过去的四年里,我写过非常多和 Traefik 相关的实践内容,感兴趣的同学可以[翻阅这里](https://soulteary.com/tags/traefik.html)。 上个月官方 [Traefik 3.0.0 第三个 beta 版本的发布](https://github.com/traefik/traefik/releases/tag/v3.0.0-beta3),3.0 新版本的代码被第二次正式合并进主干分支,距离我们能够正式使用到 3.0 版本,也越来越近了。 相较一个季度前的版本,目前 Traefik 版本变化应该已经接近稳定,为了后面更简单的切换到新版本,或许是时候开始尝试服务迁移了。 正好,尝试详细的写一篇使用 Docker 来使用 Traefik 的内容,帮助还没有入门的同学,或者使用但是还不熟悉的同学查缺补漏。 ## Traefik 的 Docker 基础容器配置 在展开详细的 Traefik 容器配置和优化调整之前,我们需要先来看看最简的容器配置是什么样的。 ### Traefik 的 Docker 最简容器配置 最基础的配置不到十行,我们只需要声明 Traefik 服务使用的容器镜像、使用和对外暴露的端口号、以及基础的命令行参数即可。 ```yaml 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 应用](https://attachment.soulteary.com/2023/07/18/traefik-basic.jpg) 但是,这个容器只能够提供我们查看 Traefik Dashboard 和默认的内部“服务”,不能够提供神奇的“服务发现”和各种“高级自定义”或“服务可观测性”等功能。 所以,我们还需要继续进行配置扩展和调整。 ### 使用 Traefik 进行服务域名绑定 Traefik 最擅长的能力是提供服务发现,即让能够提供对外网络访问能力(HTTP/TCP)的服务使用域名和具体的 URL 地址允许用户访问。 假设我们上文中使用 `localhost:8080/dashboard` 访问的 Dashboard 是一个正式的服务,有正式的域名,没有“非正式”的端口号,在使用 Traefik 能力的情况下该如何做呢? 比如,我们要使用 `traefik.console.lab.io` 这个域名来访问上面的服务,**那么首先要确保这个域名的指向是 Traefik 服务的网络 IP 地址**。你可以通过 DNS 管理工具(包括各种云服务商的网页控制面板)来调整,或者如果你的 Traefik 服务运行在本地,可以修改 `/etc/hosts` 来完成第一步前置条件。 ```bash # 修改 /etc/hosts 127.0.0.1 traefik.console.lab.io ``` 接着,我们对上面的配置进行调整: ```yaml 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` 这个域名来访问服务啦。 ![使用域名来访问服务](https://attachment.soulteary.com/2023/07/18/traefik-with-domain.jpg) 在上面的配置中,我们首先增加了容器暴露的端口 `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@internal` 和 `service=api@internal` 是 Traefik 的内部服务别名,在日常使用过程中,我们可以使用 Docker Compose 中的 `Service Name`、`Container Name`、具体的 `IP:端口号` 来进行替换。 **通过类似上面的方式,我们能够实现通过不同的域名,而非端口号来访问我们的网络服务,只需要根据实际需求,创建不同的路由名称和地址规则即可。** ### 减少对外暴露的端口 在上面的配置中,我们能够通过两种方式来访问相同的服务。 实际使用过程中,除非我们需要进行调试,否则只通过 Traefik 提供服务注册的域名来进行服务访问,显然是更好的模式。并且,在这个过程中,我们能够在我们声明的路由中添加各种各样的额外操作:添加认证、修改请求头、修改响应内容、进行重定向、进行限流、进行访问限制等等。 所以,我们可以将上面暴露的端口从下面的内容: ```yaml ports: - 8080:8080 - 80:80 ``` 调整为: ```yaml ports: - 80:80 ``` ### 优化 Traefik 命令行配置写法 在上面的配置中,我们操作 Traefik 的命令行的书写方式是这样的: ```yaml command: "--api=true --api.dashboard=true --api.insecure=true --entrypoints.http.address=:80 --providers.docker=true --providers.docker.endpoint=unix:///var/run/docker.sock" ``` 实际使用过程中,我们会使用二十余个参数,如果使用上面的写法进行书写,那么将是一大坨文本混在一起,非常不利于后续调试和调整,不过我们可以使用一个 小技巧,来进行改善: ```yaml 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 服务状态是异常的: ```yaml healthcheck: test: ["CMD-SHELL", "wget -q --spider --proxy off localhost:8080/ping || exit 1"] interval: 3s retries: 10 ``` 为了让服务能够遇到问题自动重启,我们可以添加下面的内容到配置中: ```yaml restart: always ``` 完整的配置如下: ```yaml 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` 就能够看到类似下面的日志输出了: ```bash 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` 的中间件: ```yaml labels: - "traefik.http.middlewares.gzip.compress=true" ``` 定义完中间件之后,我们就可以在之前定义好的路由中添加这个服务了: ```yaml labels: - "traefik.http.routers.traefik-dashboard.middlewares=gzip@docker" - "traefik.http.routers.traefik-dashboard-api.middlewares=gzip@docker" ``` 因为我们的 `gzip` 服务是写在 Docker 的配置文件中的,为了使用的严谨,这里在调用中间件的时候,推荐加上 `@docker` 后缀,要求服务从 “Docker 中”定义的中间件里查找能够使用的中间件。(因为我们也可以在文件中定义中间件) ![压缩后的请求内容](https://attachment.soulteary.com/2023/07/18/traefik-gzip.jpg) 再次访问服务,能够发现我们的页面已经被压缩过了,网页访问速度也得到了提升。 ## 搭配外部软件提供 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,所以我们可以在配置的端口中增加提供外部访问的容器中端口: ```yaml ports: - 443:443 ``` 在上面的内容中,我们定义了 `80` 端口,举一反三,我们可以定义一个名为 `https` 的 `443` 端口: ```yaml command: - "--entrypoints.https.address=:443" ``` 然后,我们可以增加或修改原来的服务路由使用的 `entrypoints` 接口: ```yaml labels: - "traefik.http.routers.traefik-dashboard.entrypoints=http" - "traefik.http.routers.traefik-dashboard-api.entrypoints=http" ``` 从 `http` 调整为 `https`: ```yaml labels: - "traefik.http.routers.traefik-dashboard.entrypoints=https" - "traefik.http.routers.traefik-dashboard-api.entrypoints=https" ``` 当然,除了简单的改名字之外,我们还需要额外增加一个配置声明 `tls=true`: ```yaml 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 修改域名记录的服务,更多的域名服务商的相关内容,有必要单独写一篇文章来讲。 我们首先在配置中添加下面三个环境变量: ```yaml 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` 字段。 ```yaml 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 的服务的网络路由中进行下面的配置: ```yaml 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 能够记住我们花费时间申请好的证书,我们需要将容器的文件做持久化存储: ```yaml volumes: - ./certs/:/certs/:ro ``` 在上面的一通操作之后,我们的 Traefik 就能够自动的申请和使用 HTTPS 证书了,并且会在证书快过期之前,自动更新证书。 完整的配置文件,你可以参考 GitHub 中早些时候提交的[配置例子](https://github.com/soulteary/Home-Network-Note/blob/2191fdeeab261722638bb2e324be6cb24fc8ed36/minimal/console/traefik/docker-compose.yml)。 ### 使用 Traefik 和现成证书提供服务 通过云服务商购买或免费申请 HTTPS 证书也好、通过类似上面的 let's encrypt 注册证书工具进行证书注册和保存,或者进行自签名证书生成也罢,我们都能够得到提高服务所需要的证书文件。 **这里,我们使用最简单和自由度最高的方案来进行接下来的配置的讲解:自签名证书。** 几年前,我写过一个简单的,只有 4MB 大小的容器工具:[soulteary/certs-maker](https://github.com/soulteary/certs-maker),使用它可以快速的生成任意域名的 HTTPS 证书,搭配一些 DNS 设置,比如公司内的 DNSMASQ 等私有 DNS 服务器设置或修改 `/etc/hosts`,我们可以让 Traefik 支持任意服务的任意域名的 HTTPS 访问,比如你可以提供一个页面上有一个苹果的服务,通过 `https://www.apple.com` 来访问它。 虽然使用 Docker 命令行可以看起来更短小精悍的生成配置,但考虑到清晰可读,我们还是创建一个 `docker-compose.certs.yml` 的文件,来帮助我们生成 HTTPS 证书吧。 ```yaml 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`,在执行命令的目录中,就能够看到新鲜的被生成出来的证书文件和配置了。 ```bash ├── lab.io.conf ├── lab.io.crt └── lab.io.key ``` 想要让我们的 Traefik 正确的使用证书,提供浏览器支持的 TLS 传输能力,我们需要先创建一个 tls.toml 配置文件: ```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 )。 然后,我们将目录映射到容器环境中: ```yaml volumes: - ./certs/:/certs/:ro - ./config/:/etc/traefik/config/:ro ``` 并在 `command` 命令中添加能够读取目录中 `tls.toml` 等配置的参数: ```yaml command: - "--providers.file.directory=/etc/traefik/config" ``` 和上文中一样在 `labels` 中调整之前的路由的 `entrypoints` 和增加 `tls=true` 配置: ```yaml - "traefik.http.routers.traefik-dashboard.entrypoints=https" - "traefik.http.routers.traefik-dashboard.tls=true" ``` 我们将上文中提供 HTTP 服务的配置进行调整,完成配置如下: ```yaml 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` 来访问服务了。 ![使用的自签名证书的详细信息](https://attachment.soulteary.com/2023/07/18/traefik-certs.jpg) 在浏览器的证书信息选项卡里,我们能够看到这张自签名证书的详细信息,如果你想进行信息的定制化,可以参考 [certs-maker](https://github.com/soulteary/certs-maker) 项目文档调整生成证书使用的参数。 ## 同时提供 HTTP 和 HTTPS 服务 在上面的文章中,我们为了节约代码篇幅,和减少不必要的理解成本,分别实现了几种 HTTP 和 HTTPS 服务的实现方式,下面我们来将配置组合在一起,完成一个完整的服务配置。 首先是定义 `command` 和 `ports`,让 Traefik 能够同时提供 80 端口的 HTTP 服务,和 443 端口的 HTTPS 服务。 ```yaml 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 拥有了提供服务的基础能力,但是没有服务内容,所以接下来我们创建能够提供服务内容的路由配置: ```yaml 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=true` 和 `entrypoints` 的设置。 重新启动服务,我们就能够使用 `http://traefik.console.lab.io` 或 `https://traefik.console.lab.io` 访问服务了。 ### 使用中间件来改进服务访问体验 在上面的配置中,我们的冗余内容其实挺多的,虽然 Traefik 1.x 之后没辙,就得这样。不过,如果我们服务只需要 HTTPS 访问,当用户访问 HTTP 协议的时候(直接在某些浏览器中敲域名,回车后),HTTP 协议自动跳转 HTTPS ,上面的配置就能够精简很多了。 我们先定义一个能够将服务协议从 HTTP 自动切换为 HTTPS 的 Traefik 中间件规则: ```yaml - "traefik.http.middlewares.redir-https.redirectscheme.scheme=https" - "traefik.http.middlewares.redir-https.redirectscheme.permanent=false" ``` 然后,在 HTTP 网页服务的路由上添加这个中间件规则: ```yaml - "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 的路由删除掉: ```yaml 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 内部的一个“魔术变量”来进行服务替换,将真实服务替换为一个空的服务。 ```yaml 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 支持的所有的静态配置文件,可以参考[这个在线文档来使用和调整](https://doc.traefik.io/traefik/v3.0/reference/static-configuration/cli/)。 虽然有很多参数默认是 `false` 、“空”等我们不设置也没问题的数值,但是为了避免 Traefik 程序版本升级,调整默认行为,对我们造成服务行为预期不符的问题,建议将所有的使用到的相关配置都进行显式的声明: ```yaml 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` 虚拟网络: ```bash docker network create traefik ``` 接着,在需要 Traefik 提供服务发现的应用中添加下面的字段,让应用在网络中: ```yaml networks: - traefik ``` 然后,在 `docker-compose.yml` 配置的末尾,声明这个 `traefik` 网络是容器外的独立的网络,而不是根据当前的服务配置文件,创建一个只在这个配置文件作用范围的虚拟网络。 ```yaml networks: traefik: external: true ``` 因为 Docker 容器中时常会有多个虚拟网络,所以我们需要在 `command` 中指定要使用的网络名称: ```yaml command: - "--providers.docker.network=traefik" ``` 为了避免 Traefik 智能的自动解析和将所有在 Traefik 网络的服务都尝试进行公开服务,我们可以在命令中添加下面的命令,让 Traefik 只对我们在 `labels` 中声明了要进行服务注册的应用提供服务: ```yaml "--providers.docker.exposedbydefault=false" ``` 在 `labels` 中,我们的定义写法是这样的: ```yaml labels: - "traefik.enable=true" ``` 这里还有一些小技巧,比如对计划提供发现服务的进行进一步的筛选,或者针对每一个服务,调整要使用 Traefik 进行服务的虚拟网络,我们未来的文章里再聊。 ### 调整容器服务端口 在上面的文章中,我们为了行文简单,使用了端口暴露的简写模式,为了能够让 Traefik 在容器中也能够取到正确的访问客户端的 IP 地址,我们需要将 `ports` 调整为下面的写法: ```yaml ports: - target: 80 published: 80 protocol: tcp mode: host - target: 443 published: 443 protocol: tcp mode: host ``` ### 避免 Traefik 进行数据上报 想要避免 Traefik 进行数据上报,我们可以通过设置下面两个 `command` 参数实现: ```yaml command: - "--global.sendanonymoususage=false" - "--global.checknewversion=false" ``` 如果你还不放心,可以继续设置下面的配置,让容器访问不到下面的 API 地址: ```yaml 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 配置参数,来自动丢弃过大的日志输出: ```yaml logging: driver: "json-file" options: max-size: "1m" ``` 比如上面的配置中,将自动丢弃超过 1MB 的日志输出。 ## 最终的容器配置文件 好了,我们将上面的所有内容进行合理组合,不难得到最终的配置(相关代码已上传至 [soulteary/Home-Network-Note/tree/master/example/traefik-v3.0.0](https://github.com/soulteary/Home-Network-Note/tree/master/example/traefik-v3.0.0)): ```yaml 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