ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Docker环境下的Traefik实战:自动服务发现与反向代理替代Nginx

Docker环境下的Traefik实战:自动服务发现与反向代理替代Nginx 1. 为什么我在Docker环境里放弃了手写Nginx配置先说一下我自己的背景。前几年我维护的服务器上跑着十几个Docker容器有博客、有API服务、有内部工具还有给客户演示的临时环境。那时候我的做法很传统装一个Nginx手动写配置文件为每个服务配置一条server块指向上游容器的IP和端口。这套方案初期还算能用但越到后面越难受。最典型的一个场景是某天凌晨容器重启了IP变了很多初学Docker的人习惯用默认的bridge网络容器IP每次重建都会变第二天早上发现服务全部打不开因为Nginx的proxy_pass还指向旧IP。另一个场景是每次新增服务我都要登录服务器、编辑配置、测试语法、重载Nginx这套流程一天折腾几十次实在让人崩溃。后来我换成了Traefik这个问题被彻底解决。Traefik是一个专门为容器环境设计但不限于容器的反向代理和负载均衡器它最核心的能力是自动服务发现只要容器启动时声明好自己的标签LabelsTraefik就能自动把对应域名的流量转发到新容器上不需要手动改任何配置也不需要重载进程。它的工作方式通俗点讲就是Traefik就像一个前台接待员每个新入驻的租户容器入住时在前台登记一下自己的房号和名字Labels前台就自动记住了以后访客HTTP请求报出名字前台就知道该往哪个房间引。整个过程不需要人再去翻纸质台账改配置文件了。这篇内容适合谁如果你正在用Docker部署多个Web服务、API、微服务或者想找一个比手动维护Nginx配置更省心的方案这篇文章应该对你有用。我会从环境准备、核心机制、实际案例、踩坑经验几个层面完整讲一遍每一步都给出可以直接复制运行的配置和命令。2. 环境准备Docker网络与端口规划里的几个容易翻车的地方在正式部署Traefik之前先把环境梳理清楚。很多人刚开始配置Traefik失败一半以上的问题其实出在Docker网络和端口规划上而不是Traefik本身。2.1 网络规划为什么要用两个不同作用的网络我在生产环境里习惯创建两个Docker网络traefik-public专门用于Traefik和对外提供服务的容器接入这些容器需要接收来自外部的流量。app-internal用于后端服务之间的内部通信不直接暴露给外部。为什么要拆开因为Traefik本身运行在Docker里它要和目标容器通信就必须和目标容器处于同一个网络。如果让所有容器都在同一个大网络里后端数据库、缓存这类敏感服务也跟对外服务的容器混在一起虽然Docker的bridge网络默认能隔离外网访问但内部容器之间的互通范围会变大不符合最小权限原则。举个例子我用Traefik代理一个前端应用和一个后端API前端需要访问API两者都接入traefik-public而API需要连数据库数据库只接入app-internal不接入traefik-public。这样数据库不会出现在Traefik的服务发现列表里即便有人往公网域名上试也不可能把流量路由到数据库容器上。提示创建网络的命令很简单docker network create traefik-publicdocker network create app-internal2.2 端口规划80和443必须留给TraefikTraefik作为入口网关它要监听HTTP80和HTTPS443两个端口。如果服务器上还有其他程序占用了这两个端口比如你自己手动装了Nginx也占着80端口那Traefik根本启动不了或者启动后端口冲突。我遇到过不止一次这样的情况服务器上之前装过Web面板面板里的Nginx占用了80和443我一启动Traefik容器就报port is already allocated。排查的时候还要先把旧服务停掉非常麻烦。建议在部署前先检查端口占用sudo netstat -tlnp | grep -E :80|:443有输出就说明端口被占用了需要先处理。另外Traefik的控制面板Dashboard端口我用8080这个端口我通常只绑定在服务器内网地址或者通过防火墙限制来源IP不会直接暴露到公网。Dashboard在暴露自己Traefik配置的服务列表和健康状态的同时如果被人打开了也等于把整个内网服务拓扑公开了所以要注意保护。3. Traefik的核心机制静态配置与动态配置为什么是分开的刚接触Traefik的人常常会被它的配置方式搞晕。它有两套配置静态配置启动时加载定义了Traefik自己怎么运行比如监听哪些端口、从哪发现服务、Dashboard是否开启。动态配置运行中热更新定义了流量怎么路由到具体服务比如某个域名的请求转发到哪个容器。这种分离的设计是有意为之的。静态配置相当于Traefik的制度不常变动动态配置则相当于业务台账每次新增容器都可能会变。把两者分开Traefik才能实现新增服务零重启的能力——它通过监听Docker的事件实时感知容器的启动和停止动态调整路由规则。3.1 静态配置文件的完整示例我习惯用traefik.yml作为静态配置文件挂在容器里。一个最简但完整的示例# traefik.yml entryPoints: web: address: :80 websecure: address: :443 providers: docker: endpoint: unix:///var/run/docker.sock exposedByDefault: false network: traefik-public api: dashboard: true insecure: true certificatesResolvers: letsencrypt: acme: email: adminexample.com storage: /letsencrypt/acme.json httpChallenge: entryPoint: web先逐个解释一下各项的作用很多人照抄配置后出问题往往就是不理解这些选项entryPoints.web和entryPoints.websecure定义了Traefik监听的两个入口一个走HTTP80端口一个走HTTPS443端口。providers.docker.endpointTraefik通过这个socket去与Docker守护进程通信获取容器列表和Labels。这个路径在Linux下是默认的如果你用Docker DesktopmacOS/Windows需要确认该路径是否可用或者改用Docker Desktop提供的socket路径。providers.docker.exposedByDefault: false非常关键。默认情况下Traefik会为所有接入Docker的容器自动创建路由但这很容易把那些不希望暴露的服务意外暴露出去。设成false后只有明确写了traefik.enabletrue标签的容器才会被代理。providers.docker.network: traefik-public告诉Traefik优先使用这个网络来找到目标容器IP。如果你的容器同时接了多个网络这个设置能避免Traefik拿到错误的IP。api.dashboard: true并配合api.insecure: true允许通过HTTP直接访问Dashboard。这只是一个方便本地验证的方案生产环境建议关掉insecure给Dashboard单独加认证这个我们后面会讲到。certificatesResolvers.letsencrypt定义了一个证书申请器名字叫letsencrypt以后凡是路由规则里声明要用这个解析器的Traefik就自动为它申请Lets Encrypt证书。3.2 Docker Compose部署Traefik然后用Docker Compose把Traefik跑起来version: 3.8 services: traefik: image: traefik:v3.1 container_name: traefik restart: unless-stopped ports: - 80:80 - 443:443 - 8080:8080 volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./traefik.yml:/traefik.yml:ro - ./letsencrypt:/letsencrypt networks: - traefik-public networks: traefik-public: external: true这里有两个容易踩坑的点。第一个是volumes里的sock挂载结尾我写了:ro只读意思是不允许容器往socket里写东西。这层保护并不绝对因为Traefik还是能通过socket读到Docker的全部信息但至少能防止误操作导致的对Docker守护进程的异常写入。第二个是letsencrypt目录的挂载。证书文件acme.json需要持久化保存否则每次容器重建都重新申请证书容易触发Lets Encrypt的速率限制。我一开始没做持久化测试两天后发现正式环境的证书不下来了查日志才知道是重复申请触发了限流后来把目录持久化就好多了。注意如果宿主机上已经有letsencrypt目录确保目录存在且权限正确。我的做法是先mkdir -p ./letsencrypt然后chmod 600 ./letsencrypt/acme.json首次启动后生成避免Traefik因为文件权限问题拒绝写入。4. 用两个真实案例跑通反向代理域名路由与子路径路由Config配置好、容器能跑起来只是第一步。接下来用一个实际场景来演示Traefik最核心的两种路由玩法域名路由和子路径路由。我这边有一个开源博客工具WordPress、一个监控面板Netdata还有一个内部API服务。下面用这三个目标来演示。4.1 案例一通过域名区分路由首先是WordPress我希望通过blog.example.com访问它。在它的docker-compose.yml里给WordPress容器加以下标签services: wordpress: image: wordpress:latest # 其他配置省略... labels: - traefik.enabletrue - traefik.http.routers.blog.ruleHost(blog.example.com) - traefik.http.routers.blog.entrypointsweb - traefik.http.routers.blog.serviceblog-svc - traefik.http.services.blog-svc.loadbalancer.server.port80解读一下labels的含义traefik.enabletrue因为前面设置了exposedByDefault: false这个标签是必须的。traefik.http.routers.blog.ruleHost(...)定义了一个路由器叫blog匹配条件是域名。traefik.http.routers.blog.entrypointsweb这个路由监听web入口HTTP。traefik.http.routers.blog.serviceblog-svc让该路由把流量转发给一个名为blog-svc的服务。traefik.http.services.blog-svc.loadbalancer.server.port80定义服务blog-svc指向容器的80端口。你可能注意到了路由器和服务我用了两个名字其中blog-svc这个名字如果没有这一行的话Traefik会自动生成一个默认服务名基于容器名。但显式写出来可以在后面做灰度发布或者加权负载均衡时更方便。这个我们细说。启动这个WordPress容器之后Traefik如果已经处于运行状态它会秒级感知到新容器并生成对应路由规则。打开http://blog.example.com正常情况下就能访问到WordPress的安装界面了。4.2 案例二通过子路径区分路由有时候我们只有一个域名但想通过不同路径访问不同服务。例如服务器上有netdata.example.com或者用同一个域名挂不同路径比如https://example.com/netdata。用子路径的方式配置如下services: netdata: image: netdata/netdata:latest labels: - traefik.enabletrue - traefik.http.routers.netdata.rulePathPrefix(/netdata) - traefik.http.routers.netdata.entrypointsweb - traefik.http.routers.netdata.middlewaresnetdata-stripprefix - traefik.http.middlewares.netdata-stripprefix.stripprefix.prefixes/netdata - traefik.http.services.netdata-svc.loadbalancer.server.port19999这里多了一个中间件netdata-stripprefix。它的作用是把请求路径里的/netdata前缀去掉再转发给后端容器。为什么呢因为像Netdata这类工具它内部所有资源路径默认都是/开头比如/api/v1。如果流量带着/netdata/api/v1进去Netdata会找不到对应的资源返回404。用stripprefix把/netdata剥掉后端看到的就是/api/v1一切正常。同理如果你再挂一个服务到/api也别忘了对应的strip中间件。这是新手最常见的子路径路由问题——路由规则配了访问也通了但页面样式和接口全部404。4.3 多个容器同时提供同一个服务的负载均衡场景Traefik名字里带着负载均衡它天然支持多个容器实例共享同一组标签来做轮询。比如我有个API服务我起了两个容器都是同一个镜像labels完全一样services: api-1: image: my-api:latest labels: - traefik.enabletrue - traefik.http.routers.api.ruleHost(api.example.com) - traefik.http.services.api-svc.loadbalancer.server.port8080 api-2: image: my-api:latest labels: - traefik.enabletrue - traefik.http.routers.api.ruleHost(api.example.com) - traefik.http.services.api-svc.loadbalancer.server.port8080Traefik会自动把这两个容器归入同一个api-svc负载均衡组流量在两个实例之间轮询分发。你也可以通过loadbalancer.server.weight来设置权重做金丝雀发布的时候非常好用。比如新版本容器权重设成1旧版本设成10那大概10.9%的流量会打到新版本上观察一段时间没问题再调整权重到100%。5. 自动HTTPS证书让浏览器告别不安全警告Traefik让我最爽的一个特性就是自动HTTPS。以前用Nginx搭配Certbot每三个月要手动续期一次证书还要写钩子脚本去reload Nginx。Traefik内置了ACME自动证书管理协议客户端可以自动申请、自动续期、自动重载配置。基本配置后一劳永逸。5.1 对接Lets Encrypt的完整配置前面静态配置里已经定义了一个证书解析器letsencrypt。接下来只要在路由规则里声明用哪个解析器即可。以WordPress为例在它的labels里再补一行- traefik.http.routers.blog.entrypointswebsecure - traefik.http.routers.blog.tls.certresolverletsencrypt这里我把入口从web改成了websecure即443端口然后声明使用letsencrypt解析器。Traefik收到这个TLS请求后会通过HTTP-01验证方式在80端口返回一个验证文件来证明你对域名的控制权验证通过后自动申请证书并保存到acme.json里。这里必须注意HTTP-01验证需要从外网能访问到你的80端口所以即便你的服务只用HTTPS80端口也不能关掉。Traefik需要它来完成验证。5.2 强制跳转HTTPS有了证书之后最好把HTTP请求统一跳转到HTTPS。Traefik里做这个非常简洁加一个全局的重定向中间件- traefik.http.routers.blog.middlewaresredirect-to-https - traefik.http.middlewares.redirect-to-https.redirectscheme.schemehttps需要注意的是中间件声明和作用的位置traefik.http.middlewares...是在容器上声明一个可复用的中间件定义。traefik.http.routers....middlewares是在某个路由器上引用这个中间件。如果你有多个服务不想在每个容器里重复定义中间件可以把中间件定义放在Traefik自己的动态配置里用文件提供者这样全局复用。我生产环境里的做法是维护一个middlewares.yml里面放几个通用中间件比如强制HTTPS、基础认证、安全响应头等然后每个容器的路由器labels里只写引用名。5.3 本地测试环境的自签名证书如果你只是在内网测试或者访问域名还没有真正解析到服务器让Traefik去申请Lets Encrypt证书会失败验证过程中外网访问不到你。这种情况下可以先让Traefik生成一个自签名证书来测试流程。Traefik支持给路由配置tls.options来指定TLS选项但你也可以简单地在路由器上只声明tls: true不给证书解析器这样Traefik会启动时生成一个内部自签名证书给该路由使用。浏览器会显示不受信任但至少能验证HTTPS链路是否通。等域名解析到位后再加上certresolver自动换正式证书即可。- traefik.http.routers.blog.tlstrue注意这里的tlstrue写法在实际的docker labels里会解析为布尔值true。这种方式只用于测试。6. 中间件实战认证、安全头与速率控制Traefik的中间件系统是它另一个大杀器。你可以把中间件理解成请求在到达后端之前的加工流水线可以叠加多个。下面分享三个我实际用得最多的中间件场景。6.1 Basic Auth保护内部工具像Netdata、Portainer这类管理工具我不希望它们彻底暴露在公网但有时候又需要在外面访问这时候加一层HTTP基础认证就很实用。首先生成用户名密码的哈希。Traefik的BasicAuth中间件需要的是htpasswd格式的哈希值我们可以在宿主机上生成htpasswd -nb admin 你的强密码在Docker环境中你也可以用容器来生成比如docker run --rm httpd:alpine htpasswd -nb admin 你的强密码把输出的整行字符串形如admin:$apr1$xxxxx...填到labels里- traefik.http.middlewares.admin-auth.basicauth.usersadmin:$apr1$xxxxx...然后在对应的路由器上引用它- traefik.http.routers.netdata.middlewaresadmin-auth这里有一个细节要特别注意labels值里的$符号在Compose文件中有特殊含义变量插值。如果直接在docker-compose.yml里写哈希串$会被Compose尝试解释。解决办法有两个一是在docker-compose.yml里将整个labels文件用$$转义二是直接把labels写在一个独立的labels文件里用docker-compose的labels_from或者直接用docker run --labels-file取决于你的版本。最稳妥的是把$$apr1$$...写成$$apr1$$...即把$写成$$。6.2 安全响应头Headers中间件很多安全扫描工具会检查响应头里的X-Content-Type-Options、X-Frame-Options等字段。自己往应用代码里加这些头很麻烦用Traefik统一的中间件加就很简单- traefik.http.middlewares.security-headers.headers.customresponseheaders.X-Content-Type-Optionsnosniff - traefik.http.middlewares.security-headers.headers.customresponseheaders.X-Frame-OptionsSAMEORIGIN - traefik.http.middlewares.security-headers.headers.customresponseheaders.Referrer-Policystrict-origin-when-cross-origin需要注意的是某些框架比如WordPress的后台对X-Frame-Options比较敏感如果你在后台编辑页面里需要嵌入iframe可能要针对那条路由单独覆盖中间件去掉X-Frame-Options。6.3 速率限制如果你的服务是公开API为了防止被刷爆Traefik也内置了速率限制中间件- traefik.http.middlewares.api-ratelimit.ratelimit.average100 - traefik.http.middlewares.api-ratelimit.ratelimit.period1m - traefik.http.middlewares.api-ratelimit.ratelimit.burst50含义是平均每分钟最多100个请求突发情况下允许1秒内最多50个突发请求burst。这个中间件对抵御简单的恶意请求很有效但它不是WAFWeb应用防火墙更复杂的安全防护还是需要专业设备或云厂商的安全产品。7. 我踩过的坑路由不生效、证书异常、网络不通的排查链路这一节我想认真梳理一下我实际踩过的几个坑每个我都尽量还原完整的排查过程而不只是给出答案因为排查思路才是真正能复用一辈子的东西。7.1 坑一容器标签写了但Traefik仪表盘里就是看不到服务现象按照文档配好了labels重启容器访问Dashboard服务列表里没有新服务。排查过程我第一时间去看Traefik容器的日志docker logs traefik --tail 100。日志里有一行很关键——Trying to connect to backend或者unable to find the service之类。如果日志里连这个容器的发现事件都没有问题大概率在Docker socket连接或者网络。然后我检查了Traefik容器是否真的能访问Docker socket。一个常用的验证方法是进入Traefik容器docker exec -it traefik sh然后试着用wget访问本地的unix socket。如果不行说明socket挂载路径有问题。接着我检查容器是否在Traefik所期望的网络上。如果你在traefik.yml里设置了providers.docker.network: traefik-public那么目标容器必须也在这个网络上否则Traefik虽然看到了容器但拿不到可用的IP日志里会有error: unable to find the IP address相关提示。根因有一次是目标容器只加入了app-internal网络没加入traefik-public所以Traefik发现不了它。解决办法是把目标容器也挂到traefik-public网络上或者把providers.docker.network改成目标容器所在的网络。7.2 坑二路径路由配了但页面样式全乱现象用PathPrefix(/netdata)配好了Netdata能打开页面但UI很丑很多资源加载失败。排查过程打开浏览器开发者工具Network面板里看到一堆404请求路径都是/assets/...、/api/v1/...。意识到后端收到的路径是/netdata/assets而不是/assets说明前缀没剥掉。确认中间件stripprefix是否生效先看labels里中间件名称是否拼写一致再看路由上是否声明了该中间件。两个配置必须同时存在。根因漏了stripprefix处理。补上后一切正常。这个坑我见过好几个同事踩过根源在于没有理解反向代理转发的不只是IP和端口还有路径这个概念。7.3 坑三申请证书失败一直报ACME challenge error现象正式域名配置好之后访问https发现证书没下来日志里看到challenge failed一类的错误。排查过程先在外部环境比如手机4G网络访问一下http://你的域名/.well-known/acme-challenge/xxx看能否正常返回内容。用4G是因为很多家用宽带有NAT回流问题自己在内网访问不通不代表性外访问也不通。检查80端口是否真的从外网可达。有些云服务器安全组默认只开了443/22忘了开80端口。检查防火墙是不是拦截了ACME验证请求。HTTP-01验证是Lets Encrypt服务器主动来访问你的80端口如果你的防火墙只允许特定IP访问80那就麻烦了。根因有一次确实是安全组没放行80端口。另一次则是DNS解析记录生效太慢。这些都排查清楚之后再去看Traefik侧的日志就会很顺利。7.4 坑四Dashboard暴露在公网导致的信息泄露现象朋友提醒我说他通过我的公网IP直接访问8080端口能看到我服务器上所有的容器和服务拓扑。排查过程尽管我自认为只在内网使用Dashboard但实际上服务器安全组默认全放了而且Traefik容器把8080:8080映射到了所有接口上等于公网可访问。根因一开始图省事没有给Dashboard加访问控制。后面我做了两个动作一是把端口绑定从8080:8080改成127.0.0.1:8080:8080这样只有服务器本机能访问二是如果确实需要在公网访问Dashboard就给它套一层BasicAuth中间件。这个坑给我上了很深的一课任何管理类工具默认都不要暴露到公网。7.5 坑五容器重建后服务瞬间不可用现象部署新版本时我执行了docker-compose up -d --force-recreate理论上Traefik应该自动感知并重新路由。但期间有几十秒的502窗口。排查过程观察Traefik日志发现服务下线事件确实被捕捉到了但由于旧容器被立刻销毁新容器启动需要时间比如加载镜像、执行初始化Traefik发现没有可用后端所以返回502。解决办法对于需要零宕机的服务不要直接--force-recreate而是先起新容器等新容器在Traefik里注册成功、健康检查通过后再把旧容器关掉。或者干脆用上一节提到的加权轮询做滚动发布。Traefik的健康检查机制能帮助自动摘除不健康的实例。8. 一些关于生产环境落地的经验补充再补充几个偏生产向的经验这些不是Traefik特有的但如果你真的要用它承载线上业务应该会需要。8.1 Access Log的开启与轮转Traefik本身的Access Log功能默认是关闭的。如果要排查问题建议打开日志并做日志轮转避免日志文件无限增长。可以在traefik.yml里加accessLog: filePath: /var/log/traefik/access.log format: json并且在Compose里挂载日志目录配合宿主机上的logrotate来轮转。JSON格式的日志可以方便地在后续接入日志平台时做结构化分析。8.2 配置文件的版本兼容性Traefik的版本更新很快而且不同大版本的配置格式有差异。我早期写了不少基于v2版的配置升级到v3后部分字段就被改名了。常见的有entryPoint改名如果你用v2的话traefik.http.routers.myrouter.entryPoints在v3要改成entrypoints。动态配置提供者的字段命名变化。如果是从旧版本迁移一定要对照官方迁移指南逐项检查。我的建议是如果是新项目直接上最新稳定版不要抱着旧版本不放。8.3 容器健康检查与Traefik的联动Traefik可以通过容器的健康状态来决定是否把流量发到该容器。在Compose文件中为目标容器配置healthcheckhealthcheck: test: [CMD, curl, -f, http://localhost:80/healthz] interval: 30s timeout: 5s retries: 3然后Traefik侧无需额外配置默认情况下Traefik会只把流量路由到健康检查通过healthy的实例上。如果所有实例都不健康Traefik也会返回503而不是继续转发。这种做法在滚动发布时特别有用能避免将流量导入尚未就绪的新实例。9. 最后分享一个关于运维思路的小技巧如果前面这些你都跟着配置完了我想最后再分享一个运维层面的小技巧算是我用了这么多年Traefik之后领悟得最深的一点。不要试图用Traefik解决所有问题。Traefik擅长的是HTTP/HTTPS路由、中间件、自动服务发现这些能力很强。但它不是全能网关——如果你需要复杂的四层TCP/UDP流量管理比如数据库负载均衡、自定义协议转发它虽然支持TCP路由但配置复杂度会上升不少而且生态上的资料远不如Nginx Stream模块丰富。我现在的混合架构是外部流量先经过云厂商的负载均衡器LB或云防火墙再到Nginx处理一些专门场景比如特殊的TCP端口映射最后才到Traefik做容器服务的HTTP路由。每一层干自己最擅长的事整个链路既清晰又好维护。另外如果你打算长期用Traefik建议把Traefik的Dashboard和Access Log接入你现有的监控系统。Traefik本身会暴露Prometheus指标可以配置metrics.prometheus然后用Grafana做可视化。这样当某个服务的请求量异常时不用再靠感觉判断直接看面板就能定位到是哪个路由在扛压。我实际用下来最大的感受是Traefik不是那种功能少、所以好上手的工具而是功能多但用得越深越顺手的工具。一开始你只需要掌握Labels的路由规则就足以替代一整套Nginx手工配置再往后你会上瘾于自动证书、东西向中间件、滚动发布。只要在细节上多留心网络、端口、版本兼容、安全它会成为你在Docker环境里最值得信任的入口层组件。
返回列表