如何管理并自动更新 HomeLab 中的容器

2048 字
11 分钟
...阅读
...评论

经过一段时间的探索和优化,我已经找到一个特别适合我的方法,来自动化管理我 HomeLab 中的众多容器。一直想写一篇文章来记录一下,拖到今天才开始动笔。

首先交代一下背景,我的 HomeLab 中目前总共管理着 50 多个容器,分布在群晖 NAS 以及另外两台 NUC 的虚拟机中。从最初的直接通过 compose 文件手动管理,到后来使用了 Portainer,再到现在的基于 GitLab CI 的自动化管理方式,逐渐变得更自动化、更方便、也更不容易出错。

为什么要自动化管理

一开始上 Portainer,是为了解决手动管理多个设备中的容器,频繁 SSH 到不同设备中比较麻烦的问题。用 Portainer 确实可以方便快捷地管理多个设备中的容器,当容器数量比较少的时候,还是非常推荐的。

随着容器数量的变多,Portainer 上我遇到两个不太好解决的问题:

  1. 容器的自动更新,Portainer 的 Business 版是提供了自动更新的功能的,但可惜 CE 版没有
  2. 容器的配置文件管理、配置和 compose 文件的备份、版本记录等

第二个问题比较好解决,通过 git 管理 compose 文件和配置文件,结合 GitLab CI 在文件变更的时候自动 SSH 到对应宿主机上执行容器的更新操作。

第一个问题,是我在本博客项目上使用 renovate 来更新前端依赖的时候,从他们的文章中看到,renovate 也可以检测 Docker 镜像的更新,于是灵光一现,基于上一步所有 compose 文件都已经在 GitLab 中管理了,那再结合 renovate,就可以实现容器的自动更新了。

最终形态

中间摸索的步骤由于已经过了差不多几个月了,不太容易复盘了,就把最终的形态分享一下。

如上图,所有容器的配置文件、compose 文件都放在 config-files 这个项目中,该项目通过 GitLab 管理,renovate 在定期检测到镜像更新的时候,会自动提交一个 MR,可以通过一定规则配置成自动合并或手动合并,当然也支持手动修改配置文件或 compose 文件。MR 合并或配置文件修改后触发 GitLab CI,通过 SSH 登录对应宿主机上,执行对应的配置文件更新或容器更新的操作。

我的 config-files 目录结构:

.
├── aliyun
│   ├── gateway
│   └── hk
├── homelab
│   ├── scripts
│   ├── synology
│   ├── vm-rocky-01
│   └── vm-rocky-02
├── .gitlab-ci.yml
└── renovate.json

aliyun 目录中的 gatewayhk 是我在杭州和香港的两台服务器,部署一些外网项目,也是通过上述的方式管理。homelab 中的 scripts 是一些脚本,与本文无关,但也是通过 config-files 项目统一管理的。

我的 renovate.json 如下:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended", "group:allNonMajor", ":semanticCommitTypeAll(chore)"],
  "packageRules": [
    {
      "matchManagers": ["docker-compose"],
      "matchPackageNames": ["sonatype/nexus3", "clickhouse/clickhouse-server"],
      "enabled": false
    },
    {
      "matchManagers": ["docker-compose"],
      "matchPackageNames": ["gitlab/gitlab-runner"],
      "versionCompatibility": "^(?<compatibility>.*)-(?<version>.*)$"
    },
    {
      "matchManagers": ["docker-compose"],
      "matchUpdateTypes": ["minor", "patch"],
      "automerge": true,
      "schedule": ["before 11am on Monday"]
    }
  ],
  "rangeStrategy": "bump",
  "timezone": "Asia/Shanghai"
}

里面有一些自定义的规则,比如禁用了 sonatype/nexus3clickhouse/clickhouse-server 的自动更新,以及适配了 gitlab/gitlab-runner 的不规则的版本号。

.gitlab-ci.yml 如下:

stages:
  - pass
  - deploy

pass:
  stage: pass
  script:
    - echo "Current branch is $CI_COMMIT_BRANCH, pass."
  except:
    - main

deploy:
  stage: deploy
  before_script:
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
    - mkdir -p ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts

  script:
    - MODIFIED_FILES=$(git diff --name-only --diff-filter=ACMRT $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA)
    - DELETED_FILES=$(git diff --name-status --diff-filter=DR $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | awk '{print $2}')
    - |
      for FILE in $DELETED_FILES; do
        echo "文件移除 $FILE"
        # 如果文件是 compose.yaml,执行 docker compose down
        if [[ "$FILE" == "*/compose.yaml" ]]; then
          if [[ "$FILE" == "homelab/synology/*" ]]; then
            DEST_DIR="/volume3/docker/$(dirname ${FILE#homelab/})"
            echo "退出容器 synology:$(dirname ${FILE#homelab/synology/})"
            ssh -p 5110 [email protected] "export PATH=\$PATH:/usr/local/bin && cd $DEST_DIR && docker-compose down"
          fi

          if [[ "$FILE" == "homelab/vm-rocky-01/*" ]]; then
            DEST_DIR="/root/$(dirname ${FILE#homelab/vm-rocky-01/})"
            echo "退出容器 vm-rocky-01:$(dirname ${FILE#homelab/vm-rocky-01/})"
            ssh [email protected] "cd $DEST_DIR && docker compose down"
          fi

          if [[ "$FILE" == "homelab/vm-rocky-02/*" ]]; then
            DEST_DIR="/root/$(dirname ${FILE#homelab/vm-rocky-02/})"
            echo "退出容器 vm-rocky-02:$(dirname ${FILE#homelab/vm-rocky-02/})"
            ssh [email protected] "cd $DEST_DIR && docker compose down"
          fi

          # if [[ "$FILE" == "aliyun/gateway/*" ]]; then
          #   DEST_DIR="/root/config-files/gateway/$(dirname ${FILE#aliyun/gateway/})"
          #   echo "退出容器 gateway:$(dirname ${FILE#aliyun/gateway/})"
          #   ssh [email protected] "cd $DEST_DIR && docker compose down"
          # fi
        fi
        
        # 删除对应文件
        if [[ "$FILE" == "homelab/synology/*" ]]; then
          FILE_PATH="/volume3/docker/${FILE#homelab/}"
          echo "从 Synology 删除文件: $FILE_PATH"
          ssh -p 5110 [email protected] "rm -f $FILE_PATH"
        fi

        if [[  "$FILE" == "homelab/vm-rocky-01/*" ]]; then
          FILE_PATH="/root/${FILE#homelab/vm-rocky-01/}"
          echo "从 vm-rocky-01 删除文件: $FILE_PATH"
          ssh [email protected] "rm -f $FILE_PATH"
        fi

        if [[  "$FILE" == "homelab/vm-rocky-02/*" ]]; then
          FILE_PATH="/root/${FILE#homelab/vm-rocky-02/}"
          echo "从 vm-rocky-02 删除文件: $FILE_PATH"
          ssh [email protected] "rm -f $FILE_PATH"
        fi

        if [[ "$FILE" == "aliyun/gateway/*" ]]; then
          FILE_PATH="/root/config-files/gateway/${FILE#aliyun/gateway/}"
          echo "从 gateway.aliyun 删除文件: $FILE_PATH"
          ssh [email protected] "rm -f $FILE_PATH"
        fi
      done

      for FILE in $MODIFIED_FILES; do
        echo "文件变更 $FILE"
        if [ -e "$FILE" ]; then
          if [[ "$FILE" == "homelab/synology/*" ]]; then
            DEST_DIR="/volume3/docker/$(dirname ${FILE#homelab/})"
            ssh -p 5110 [email protected] "mkdir -p $DEST_DIR"
            echo "复制文件 $FILE 到 Synology: $DEST_DIR"
            scp -O -P 5110 $FILE [email protected]:$DEST_DIR
          fi

          if [[ "$FILE" == "homelab/vm-rocky-01/*" ]]; then
            DEST_DIR="/root/$(dirname ${FILE#homelab/vm-rocky-01/})"
            ssh [email protected] "mkdir -p $DEST_DIR"
            echo "复制文件 $FILE 到 vm-rocky-01: $DEST_DIR"
            scp -O $FILE [email protected]:$DEST_DIR
          fi

          if [[ "$FILE" == "homelab/vm-rocky-02/*" ]]; then
            DEST_DIR="/root/$(dirname ${FILE#homelab/vm-rocky-02/})"
            ssh [email protected] "mkdir -p $DEST_DIR"
            echo "复制文件 $FILE 到 vm-rocky-02: $DEST_DIR"
            scp -O $FILE [email protected]:$DEST_DIR
          fi

          if [[ "$FILE" == "aliyun/gateway/*" ]]; then
            DEST_DIR="/root/config-files/gateway/$(dirname ${FILE#aliyun/gateway/})"
            ssh [email protected] "mkdir -p $DEST_DIR"
            echo "复制文件 $FILE 到 gateway.aliyun: $DEST_DIR"
            scp -O $FILE [email protected]:$DEST_DIR
          fi

          if [[ "$FILE" == "aliyun/hk/nginx/*" ]]; then
            DEST_DIR="/etc/nginx/$(dirname ${FILE#aliyun/hk/nginx/})"
            ssh [email protected] "mkdir -p $DEST_DIR"
            echo "复制文件 $FILE 到 hk.aliyun: $DEST_DIR"
            scp -O $FILE [email protected]:$DEST_DIR
          fi
        fi
      done

      for FILE in $MODIFIED_FILES; do
        if [ -e "$FILE" ]; then
          # 如果文件是 compose.yaml,执行 docker compose up -d
          if [[ "$FILE" == "*/compose.yaml" ]]; then
            if [[ "$FILE" == "homelab/vm-rocky-01/renovate/compose.yaml" ]]; then
              echo "跳过部署 vm-rocky-01:renovate"
              continue
            fi

            if [[ "$FILE" == "homelab/synology/*" ]]; then
              DEST_DIR="/volume3/docker/$(dirname ${FILE#homelab/})"
              echo "部署容器 synology:$(dirname ${FILE#homelab/synology/})"
              ssh -p 5110 [email protected] "export PATH=\$PATH:/usr/local/bin && cd $DEST_DIR && docker-compose up -d"
            fi

            if [[ "$FILE" == "homelab/vm-rocky-01/*" ]]; then
              DEST_DIR="/root/$(dirname ${FILE#homelab/vm-rocky-01/})"
              echo "部署容器 vm-rocky-01:$(dirname ${FILE#homelab/vm-rocky-01/})"
              ssh [email protected] "cd $DEST_DIR && docker compose up -d"
            fi

            if [[ "$FILE" == "homelab/vm-rocky-02/*" ]]; then
              DEST_DIR="/root/$(dirname ${FILE#homelab/vm-rocky-02/})"
              echo "部署容器 vm-rocky-02:$(dirname ${FILE#homelab/vm-rocky-02/})"
              ssh [email protected] "cd $DEST_DIR && docker compose up -d"
            fi

            # if [[ "$FILE" == "aliyun/gateway/*" ]]; then
            #   DEST_DIR="/root/config-files/gateway/$(dirname ${FILE#aliyun/gateway/})"
            #   echo "部署容器 gateway.aliyun:$(dirname ${FILE#aliyun/gateway/})"
            #   ssh [email protected] "cd $DEST_DIR && docker compose up -d"
            # fi

            echo "容器重新部署完成"
            continue
          fi

          # 如果文件是 homelab/synology/nginx/*, 重新启动 nginx
          if [[ "$FILE" == "homelab/synology/nginx/*" ]]; then
            echo "重新启动 synology:nginx 服务"
            ssh -p 5110 [email protected] "export PATH=\$PATH:/usr/local/bin && docker exec nginx nginx -s reload"
          fi

          
          # 如果文件是 aliyun/gateway/nginx/*, 重新启动 nginx
          if [[ "$FILE" == "aliyun/gateway/nginx/*" ]]; then
            echo "重新启动 gateway.aliyun:nginx"
            ssh [email protected] "nginx -s reload"
          fi

          # 如果文件是 aliyun/hk/nginx/*, 重新启动 nginx
          if [[ "$FILE" == "aliyun/hk/nginx/*" ]]; then
            echo "重新启动 hk.aliyun:nginx"
            ssh [email protected] "nginx -s reload"
          fi

          # 如果文件是 rinetd.conf, 重新启动 rinetd
          if [[ "$FILE" == "homelab/synology/rinetd/config/rinetd.conf" ]]; then
            echo "重新启动 synology:rinetd 服务"
            ssh -p 5110 [email protected] "export PATH=\$PATH:/usr/local/bin && cd /volume3/docker/synology/rinetd && docker-compose restart"
          fi

          # 如果文件是 gitlab.rb, 重新配置 gitlab
          if [[ "$FILE" == "homelab/vm-rocky-01/gitlab/config/gitlab.rb" ]]; then
            echo "重新配置 vm-rocky-01:gitlab"
            ssh [email protected] "docker exec gitlab gitlab-ctl reconfigure"
          fi

          # 如果文件是 prometheus.yml, 重新启动 prometheus
          if [[ "$FILE" == "homelab/vm-rocky-01/prometheus/config/prometheus.yml" ]]; then
            echo "重新启动 vm-rocky-01:prometheus"
            ssh [email protected] "cd /root/prometheus && docker compose restart"
          fi
          
          # 如果文件是 telegraf.conf, 重新启动 telegraf
          if [[ "$FILE" == "homelab/synology/telegraf/conf/telegraf.conf" ]]; then
            echo "重新启动 synology:telegraf"
            ssh -p 5110 [email protected] "export PATH=\$PATH:/usr/local/bin && cd /volume3/docker/synology/telegraf && docker-compose restart"
          fi

          if [[ "$FILE" == "homelab/vm-rocky-01/telegraf/conf/telegraf.conf" ]]; then
            echo "重新启动 vm-rocky-01:telegraf"
            ssh [email protected] "cd /root/telegraf && docker compose restart"
          fi

          if [[ "$FILE" == "homelab/vm-rocky-02/telegraf/conf/telegraf.conf" ]]; then
            echo "重新启动 vm-rocky-02:telegraf"
            ssh [email protected] "cd /root/telegraf && docker compose restart"
          fi

          # 如果文件是 bots/config/clash/*.yaml, 更新 clash 配置
          if [[ "$FILE" == "homelab/vm-rocky-02/bots/config/clash/*.yaml" ]]; then
            echo "重新启动 vm-rocky-02:bots"
            ssh [email protected] "cd /root/bots && docker compose restart"
            echo "等待 10 秒"
            sleep 10
            echo "重新启动 synology:clash-premium"
            ssh -p 5110 [email protected] "export PATH=\$PATH:/usr/local/bin && cd /volume3/docker/synology/clash-premium && docker-compose restart"
          fi

          # 如果文件是 artalk/data/artalk.yml, 重新启动 artalk
          if [[ "$FILE" == "homelab/vm-rocky-02/artalk/data/artalk.yml" ]]; then
            echo "重新启动 vm-rocky-02:artalk"
            ssh [email protected] "cd /root/artalk && docker compose restart"
          fi

        fi
      done
    - echo "部署完成"
  only:
    - main

其中,SSH_KNOWN_HOSTSSSH_PRIVATE_KEY 存储在 GitLab CI/CD 的变量中,用于 SSH 登录到对应的宿主机上。

当我需要改某个容器的配置文件时,例如当我需要修改 synology 上的 nginx 配置,我只需要去 config-files 项目中修改 homelab/synology/nginx/nginx.conf 文件,然后 commit,push,等待几秒钟,GitLab CI 就会自动帮我完成配置文件的更新以及 nginx 的重启。

每周一,renovate 会自动发起并合并更新容器镜像版本的 MR,然后 GitLab CI 会自动帮我更新容器。

对了,GitLab 、GitLab Runner、Renovate 也都是通过容器私有化部署在 HomeLab 中的,这些就不赘述。

评论区
Copyright © Bean Deng