使用Traefik方便地部署Docker容器服务

 · 12 分钟阅读
 · 教授
文章目录

前言


在使用Traefik之前,笔者一直使用Oneinstack作为服务器面板。对于PHP应用环境而言,Oneinstack全面的自动化配置,能很好地帮助我们集中化地管理如Nginx、MySQL、Redis等基础应用服务。但是,伴随着越来越多新型开源项目的出现,新的应用更多地被推荐使用Docker进行部署。这个时候就会发现,如果依然通过Oneinstack等面板进行手动配置(特别是Nginx的配置),将变得无比挑战。

于是,在了解到Traefik作为云原生反代的优势后,笔者义无反顾地将所有自用服务的部署全部切换至Traefik。到目前为止,一切都相当丝滑。曾经在甲骨文新开ARM实例,将旧实例中的所有服务通过Docker进行重新部署,总共花费的时间还不到一个小时,可谓是效率奇高。如果换成传统面板的无人值守安装,可能光是环境的配置就需要数个小时了。对于个人站长的运维而言,单机版本的Docker Compose配合Traefik及Portainer面板,将带来极大的生产力提升。

比较可惜的是,Traefik官方文档撰写得颇为驳杂,往往容易令初次接触的小伙伴感到迷惑。而国内的技术型人才分享的教程,又经常容易陷入炫技的泥潭,将配置文件写得极其复杂,非常不利于理解和后期的维护。

基于这些原因,笔者结合自己的实际使用经验,为大家提供一套可借鉴的经验,以便更容易地实现便捷的服务运维。本次的教程均基于单个Linux实例进行部署,尚不涉及Docker Swarm和K8s等多实例场景,相信对于个人站长而言,已经足够。

先决条件


  • 已拥有Linux实例(本文以Ubuntu操作系统为例,使用Oracle Cloud的ARM实例)
  • 已配置云服务网关的入站规则(Ingress Rule)

基础环境配置


1. Docker及Compose安装

Docker及Compose一键安装脚本(请确保使用的是完整版Ubuntu系统,如果使用minimal版本,还需提前完成网卡相关配置

# 如遇到权限不够的问题,可以使用 sudo -i 切换root用户执行,成功后再退出为普通用户即可
bash <(curl -sSL https://cdn.jsdelivr.net/gh/SuperManito/LinuxMirrors@main/DockerInstallation.sh)

也可以使用官方安装命令:

# 官方脚本
bash <(curl -fsSL https://get.docker.com -o get-docker.sh)

# 安装compose
sudo apt-get install docker-compose-plugin

2. 部署思路

Traefik作为容器服务的关键节点,其作用和架构可以很直观地通过官方文档的这张图来理解:

Traefik架构

Traefik的服务依赖于配置文件,包含静态配置traefik.yml和动态配置dynamic.yml两个文件,动态配置支持热更新。其中,静态配置主要包含基础性的服务配置,比如entryPoints及providers等等。动态配置的核心是构建中间件,方便其他容器服务共用。

Traefik的配置参数也可以通过在docker-compose.yml文件中通过command和labels字段进行指定,但为了后续便于管理,不建议使用这种方式。我们应尽可能追求配置的逻辑清晰和复用的简洁性。

除了Traefik自身的配置文件,我们还需要考虑容器数据持久化的问题。此前,发现有相当多的教程都是将持久化的容器数据分散放于不同的系统文件夹,比如/opt/docker/<application>/data/docker/<application>,这种方式对于我们后续进行统一化管理或备份十分繁琐。笔者建议应当将所有容器化服务的数据进行统一管理,也包括Traefik的相关配置。

我们尝试在当前用户的主目录下新建src文件夹进行数据持久化的统一管理。在src文件夹下分别新建appscore两个文件夹,分别用于存储新创建的容器化服务和核心的Traefik & Portainer服务。这样,后续如果需要备份,就只用处理src文件夹即可,能够极大地提升效率。

该部署思路来自于Raf Rasenberg,属于笔者实践过程中感觉比较便捷和高效的方式。

3. 构建工作目录

基于第2点中的部署思路,我们可以直接从GitHub克隆相关配置文件的工作目录:

git clone https://github.com/rafrasenberg/docker-traefik-portainer ./src

然后 cd 进入 src 文件夹,使用tree命令查看目录结构,如下所示:

.
└── src/
    ├── core/
    │   ├── traefik-data/
    │   │   ├── configurations/
    │   │   │   └── dynamic.yml
    │   │   ├── traefik.yml
    │   │   └── acme.json
    │   └── docker-compose.yml
    └── apps/

4. Traefik配置文件示例 - traefik.yml

现在,我们开始更新配置文件traefik.yml,主要是更新acme账户的email:

使用nano命令编辑traefik.yml文件:

nano src/core/traefik-data/traefik.yml

traefik.yml配置文件示例:

global:
  sendanonymoususage: false
  checknewversion: false

api:
  dashboard: true

ping:
  entryPoint: "ping"
  manualRouting: false
  terminatingStatusCode: 204

entryPoints:
  ping:
    address: ":8082"
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
          priority: 10
  websecure:
    address: ":443"
    http:
      middlewares:
        - secureHeaders@file
        - https-redirect@file
      tls:
        certResolver: letsencrypt
  webudp:
    address: ":8000/udp"
    udp:
      timeout: 10s

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    watch: true
    exposedByDefault: false
    swarmMode: false
    swarmModeRefreshSeconds: 15
    useBindPortIP: false
    network: proxy
  file:
    # 目录地址: "/home/username/src/core/traefik-data/configurations"
    filename: /configurations/dynamic.yml
    watch: true
    debugloggeneratedtemplate: true
  providersThrottleDuration: 10

certificatesResolvers:
  letsencrypt:
    acme:
      # 请更新为您自己的邮件地址
      email: your@email.com
      storage: acme.json
      keyType: EC384
      httpChallenge:
        entryPoint: web

# 试验性的插件,可不用装
experimental:
  plugins:
    fail2ban:
      moduleName: "github.com/tomMoulard/fail2ban"
      version: "v0.6.6"

log:
  level: warn
  format: common

提示:在traefik.yml配置中,我们配置了

  • ping用于后续的healthcheck;
  • entryPoints将80端口的http流量转发至443端口https的逻辑;
  • letsencrypt自动为服务签发证书

5. Traefik配置文件示例 - dynamic.yml

在配置dynamic.yml之前,让我们先完成一些准备工作。

首先,使用htpasswd创建Traefik控制面板的用户名和密码:

# 如果未安装htpasswd,要先安装
sudo apt install apache2-utils

# 生成用户名密码,后续用于更新至dynamic.yml中
echo $(htpasswd -nb <username> <password>)

# 这里是用户名密码示例:
echo $(htpasswd -nb username password)

运行结果:

# 请记录该结果,后续配置时需要用到。
username:$apr1$KkPgzi8h$yDdbH4ORUgbY0m/MT0IJ81

然后,我们创建新的docker网络proxy

sudo docker network create proxy

最后,我们开始更新dynamic.yml,主要是更新认证用户名和密码

编辑dynamic.yml的命令:

# 先删掉示例文件吧
rm src/core/traefik-data/configurations/dynamic.yml

# 新建dynamic.yml文件
nano src/core/traefik-data/configurations/dynamic.yml

dynamic.yml内容:

# 动态配置
http:
  middlewares:
    # 使用方法:traefik.http.routers.myRouter.middlewares: "default@file"
    # 等效于:traefik.http.routers.myRouter.middlewares: "default-security-headers@file,error-pages@file,gzip@file"
    # 此处未将自定义的error-pages加入default,因为并非所有服务均适合默认加入traefik的自定义错误页中间件,可能导致部分服务的正常功能被错误页中间件提前拦截
    default:
      chain:
        middlewares:
          - https-redirect
          - nonwww-redirect
          - secureHeaders
          #- error-pages
          - gzip
    https-redirect:
      redirectScheme:
        scheme: https
        permanent: true
        port: 443
    nonwww-redirect: 
      redirectRegex: 
        regex: "^https?://(?:www\\.)?(.+)" 
        replacement: "https://${1}" 
        permanent: true
    secureHeaders:
      headers:
        #sslRedirect: true 该转发方式已被官方弃用
        browserXssFilter: true
        contentTypeNosniff: true
        frameDeny: true
        referrerPolicy: "strict-origin-when-cross-origin"
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000
    # 错误页中间件,后文中我们教大家单独配置自定义的错误页面来替换Traefik默认的错误页
    error-pages:
      errors:
        query: "/{status}.html"
        service: traefik-errors
        status:
          - "400-599"
    # 启用GZIP压缩(https://docs.traefik.io/middlewares/compress/)
    #   if the response body is larger than 1400 bytes
    #   if the Accept-Encoding request header contains gzip
    #   if the response is not already compressed (Content-Encoding is not set)
    # 使用方法:traefik.http.routers.myRouter.middlewares: "gzip@file"
    gzip:
      compress: {}

    user-auth:
      basicAuth:
        users:
          # 使用前面生成的用户名密码
          - "username:$apr1$KkPgzi8h$yDdbH4ORUgbY0m/MT0IJ81"

  services:
    traefik-errors:
      loadbalancer:
        servers:
          port:
            - 80

tls:
  options:
    default:
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
      minVersion: VersionTLS12

6. Traefik配置文件示例 - docker-compose.yml

设置好静态和动态配置文件以后,我们就可以创建docker-compose.yml文件,来构建Traefik和Portainer容器镜像(本例中启用了Protainer的Edge功能)

编辑docker-compose.yml

# 先删掉默认的文件
rm src/core/docker-compose.yml

# 新建docker-compose.yml
nano src/core/docker-compose.yml

配置docker-compose.yml为以下内容:

version: "3.9"

services:
  traefik:
    image: "traefik:latest"
    container_name: traefik
    restart: always
    security_opt:
      - "no-new-privileges:true"
    networks:
      - proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./traefik-data/traefik.yml:/traefik.yml:ro"
      - "./traefik-data/acme.json:/acme.json"
      - "./traefik-data/configurations:/configurations"
    labels:
      - traefik.enable=true
      - traefik.docker.network=proxy
      - traefik.http.routers.traefik-secure.entrypoints=websecure
      - traefik.http.routers.traefik-secure.rule=Host(`traefik.domain.com`)
      - traefik.http.routers.traefik-secure.service=api@internal
      - traefik.http.routers.traefik-secure.middlewares=default@file
      - traefik.http.routers.traefik-secure.middlewares=user-auth@file
      # 此时还未配置自定义错误页的服务,下面这条可暂时注释掉;不过由于Traefik是所有其他服务的基础,也不太建议对Traefik启用自定义错误页,否则服务稳定性可能受其他服务影响而不稳定
      # - traefik.http.routers.traefik-secure.middlewares=error-pages
    healthcheck:
      test:
        [
          "CMD", "traefik", "healthcheck", "--ping"
        ]
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 5s
    logging:
      driver: "json-file"
      options:
        max-size: "1m"

  portainer:
    image: "portainer/portainer-ce:latest"
    container_name: portainer
    restart: always
    security_opt:
      - "no-new-privileges:true"
    networks:
      - proxy
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./portainer-data:/data"
    labels:
      # Frontend
      - "traefik.enable=true"
      - "traefik.http.routers.frontend.rule=Host(`portainer.domain.com`)"
      - "traefik.http.routers.frontend.entrypoints=websecure"
      - "traefik.http.services.frontend.loadbalancer.server.port=9000"
      - "traefik.http.routers.frontend.service=frontend"
      - "traefik.http.routers.frontend.tls.certresolver=letsencrypt"
      # 自定义配置的自定义错误页,不建议对Portainer启用
      # - "traefik.http.routers.frontend.middlewares=error-pages"
      # Edge
      - "traefik.http.routers.edge.rule=Host(`edge.domain.com`)"
      - "traefik.http.routers.edge.entrypoints=websecure"
      - "traefik.http.services.edge.loadbalancer.server.port=8000"
      - "traefik.http.routers.edge.service=edge"
      - "traefik.http.routers.edge.tls.certresolver=letsencrypt"

networks:
  proxy:
    external: true

7. 启动Traefik服务

到目前一切都看起来不错。

我们还有最后一些配置需要完成。acme.json文件默认的读写权限为644,当开启容器服务时,我们可能会遇到一些权限相关的问题。因此,让我们进入core文件夹为acme.json设置正确的读写权限:

sudo chmod 600 ./traefik-data/acme.json

确保当前处于core文件夹中,然后使用docker compose启动服务(第一次运行不要带 -d,以便能及时发现错误。成功后可停止服务,然后重新开启服务带-d

sudo docker compose up

服务成功启动后,现在就可以访问之前配置的域名 traefik.domain.com 和 portainer.domain.com 来查看您的服务了。

注意:portainer首次访问时需要设置管理员账号,请尽快进行配置,如果超时,将需要重来一遍。

traefik dashboard

portainer login page

如果服务一切正常,并且终端中也未提示错误,您便可以使用Ctrl+C停止运行,并使用下方命令重启服务,并在后台持久运行:

sudo docker compose down && sudo docker compose up -d

提醒:如果在compose配置文件中变更过服务名称,在停止容器时,请使用下述命令:

sudo docker compose down --remove-orphans

在对配置进行修改的过程中,您可能会频繁用到以下命令,供参考:

# 删除命令,慎用
rm -rf docker-compose.yml

rm -rf traefik-data/traefik.yml

rm -rf traefik-data/configurations/dynamic.yml

rm -rf traefik-data/configurations/dynamic.yml traefik-data/traefik.yml docker-compose.yml

# 编辑文档
nano traefik-data/traefik.yml

nano traefik-data/configurations/dynamic.yml

nano docker-compose.yml

sudo docker compose down --remove-orphans && sudo docker compose up -d

8. 为Traefik配置通用错误页面

配置的逻辑上,应首先构建nginx容器来host所有的错误状态码页面(如404.html),然后添加错误页面的traefik中间件及服务(以供其他容器使用),最后将traefik中间件加入其他容器构建服务时的label标签,即可正常使用。

具体步骤如下:

  • apps文件夹下新建errorpage文件夹并进入

  • 提前准备好错误页面,以供nginx容器使用,可以直接使用github上开源项目tarampampam/error-pages中已渲染好的包

wget https://github.com/tarampampam/error-pages/archive/refs/heads/gh-pages.zip
  • 下载后解压并按需重命名文件夹:
# 解压至pages文件夹
unzip -d pages gh-pages.zip

# 移动子文件夹
mv pages/error-pages-gh-pages/* pages/
  • 提前配置nginx的默认配置文件:
# 新建nginx文件夹
mkdir nginx

# 新建default.conf文件
nano nginx/default.conf

nginx配置文件示例:

server {
    listen        80;
    server_name   localhost;

    charset       utf-8;
    gzip on;

    access_log    off;
    log_not_found off;
    server_tokens off;

    location / {
     root   /usr/share/nginx/error-pages;
        internal;
#        index  index.html;
    }

    error_page 400 /400.html;
    error_page 401 /401.html;
    error_page 403 /403.html;
    error_page 404 /404.html;
    error_page 405 /405.html;
    error_page 407 /407.html;
    error_page 408 /408.html;
    error_page 409 /409.html;
    error_page 410 /410.html;
    error_page 411 /411.html;
    error_page 412 /412.html;
    error_page 413 /413.html;
    error_page 416 /416.html;
    error_page 418 /418.html;
    error_page 429 /429.html;
    error_page 500 /500.html;
    error_page 502 /502.html;
    error_page 503 /503.html;
    error_page 504 /504.html;
    error_page 505 /505.html;

    location = /favicon.ico {
        add_header 'Content-Type' 'image/x-icon';
        return 200 "";
    }

    location = /robots.txt {
        return 200 "User-agent: *\nDisallow: /";
    }
}
  • 构建错误页面的nginx容器,添加错误页面的traefik中间件及服务,以供其他容器使用。具体的 docker-compose.yml 文件如下:
version: '3'

services:

  errorpage-nginx:
    image: nginx:1.24-alpine
    volumes:
      - ./pages/app-down:/usr/share/nginx/error-pages:ro
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.error-router.rule=HostRegexp(`{host:.+}`)
      - traefik.http.routers.error-router.priority=1
      - traefik.http.routers.error-router.entrypoints=websecure
      - traefik.http.routers.error-router.middlewares=error-pages
      - traefik.http.middlewares.error-pages.errors.status=400-599
      - traefik.http.middlewares.error-pages.errors.service=traefik-errors
      - traefik.http.middlewares.error-pages.errors.query=/{status}.html
      - traefik.http.services.traefik-errors.loadbalancer.server.port=80


networks:
  proxy:
    external: true
  • 此时,error-pages中间件已经可用,只需在需要显示自定义错误页面的容器服务构建时,添加如下label标签挂载error-pages中间件即可(注意将CONTAINER_NAME替换为对应的服务名称):
      - traefik.http.routers.CONTAINER_NAME.middlewares=error-pages

现在,我们的容器服务就拥有漂亮的自定义错误页面啦:

image.png

提示:对于稳定性要求高的服务,建议不要配置通用错误页面,因为自定义错误页面的服务依赖于其他容器服务,如果其他容器服务挂了,我们的服务也就无法访问了。笔者就曾经手贱,将host错误页的nginx服务在portainer里停了,结果导致所有使用error-pages中间件的容器全部都访问失败,变成了traefik默认的404提示:

404 page not found image.png

简直离了个大谱!

总结


本篇教程中,与大家探讨了一种逻辑清晰的Traefik配置使用方式。在配置文件中,笔者只加入了最常见的一些服务和中间件,实际上Traefik强大的功能配置远不止这些。在理解配置逻辑之后,相信您再去阅读官方文档,将会发现更多有趣的地方。Traefik即将迎来3.0版本,相信未来会有更多强大而有趣的功能开放给大家,让我们一起期待吧!

参考资料