[{"data":1,"prerenderedAt":1971},["ShallowReactive",2],{"navigation":3,"post-\u002Fposts\u002F2024\u002Fmanaging-containers-in-my-homelab":20,"surroundPosts-\u002Fposts\u002F2024\u002Fmanaging-containers-in-my-homelab":1958},[4,8,12,16],{"title":5,"path":6,"stem":7},"首页","\u002F","00.index",{"title":9,"path":10,"stem":11},"文章","\u002Fposts","01.posts",{"title":13,"path":14,"stem":15},"动态","\u002Fmoments","02.moments",{"title":17,"path":18,"stem":19},"关于","\u002Fabout","09.about",{"id":21,"title":22,"body":23,"class":1938,"cover":1938,"coverSize":1938,"date":1939,"description":29,"draft":1940,"extension":1941,"hideComments":1940,"location":1938,"meta":1942,"navigation":595,"path":1943,"readingTime":1944,"seo":1949,"sitemap":1950,"stem":1951,"tags":1952,"time":1938,"weather":1938,"__hash__":1957},"posts\u002Fposts\u002F2024\u002F20241212.managing-containers-in-my-homelab.md","如何管理并自动更新 HomeLab 中的容器",{"type":24,"value":25,"toc":1934},"minimark",[26,30,33,37,40,43,53,56,59,62,65,70,78,84,94,119,125,544,555,560,1893,1903,1924,1927,1930],[27,28,29],"p",{},"经过一段时间的探索和优化，我已经找到一个特别适合我的方法，来自动化管理我 HomeLab 中的众多容器。一直想写一篇文章来记录一下，拖到今天才开始动笔。",[27,31,32],{},"首先交代一下背景，我的 HomeLab 中目前总共管理着 50 多个容器，分布在群晖 NAS 以及另外两台 NUC 的虚拟机中。从最初的直接通过 compose 文件手动管理，到后来使用了 Portainer，再到现在的基于 GitLab CI 的自动化管理方式，逐渐变得更自动化、更方便、也更不容易出错。",[34,35,36],"h2",{"id":36},"为什么要自动化管理",[27,38,39],{},"一开始上 Portainer，是为了解决手动管理多个设备中的容器，频繁 SSH 到不同设备中比较麻烦的问题。用 Portainer 确实可以方便快捷地管理多个设备中的容器，当容器数量比较少的时候，还是非常推荐的。",[27,41,42],{},"随着容器数量的变多，Portainer 上我遇到两个不太好解决的问题：",[44,45,46,50],"ol",{},[47,48,49],"li",{},"容器的自动更新，Portainer 的 Business 版是提供了自动更新的功能的，但可惜 CE 版没有",[47,51,52],{},"容器的配置文件管理、配置和 compose 文件的备份、版本记录等",[27,54,55],{},"第二个问题比较好解决，通过 git 管理 compose 文件和配置文件，结合 GitLab CI 在文件变更的时候自动 SSH 到对应宿主机上执行容器的更新操作。",[27,57,58],{},"第一个问题，是我在本博客项目上使用 renovate 来更新前端依赖的时候，从他们的文章中看到，renovate 也可以检测 Docker 镜像的更新，于是灵光一现，基于上一步所有 compose 文件都已经在 GitLab 中管理了，那再结合 renovate，就可以实现容器的自动更新了。",[34,60,61],{"id":61},"最终形态",[27,63,64],{},"中间摸索的步骤由于已经过了差不多几个月了，不太容易复盘了，就把最终的形态分享一下。",[66,67],"post-image",{":dark-supported":68,"filename":69},"true","01.png",[27,71,72,73,77],{},"如上图，所有容器的配置文件、compose 文件都放在 ",[74,75,76],"code",{},"config-files"," 这个项目中，该项目通过 GitLab 管理，renovate 在定期检测到镜像更新的时候，会自动提交一个 MR，可以通过一定规则配置成自动合并或手动合并，当然也支持手动修改配置文件或 compose 文件。MR 合并或配置文件修改后触发 GitLab CI，通过 SSH 登录对应宿主机上，执行对应的配置文件更新或容器更新的操作。",[27,79,80,81,83],{},"我的 ",[74,82,76],{}," 目录结构：",[85,86,91],"pre",{"className":87,"code":89,"language":90},[88],"language-text",".\n├── aliyun\n│   ├── gateway\n│   └── hk\n├── homelab\n│   ├── scripts\n│   ├── synology\n│   ├── vm-rocky-01\n│   └── vm-rocky-02\n├── .gitlab-ci.yml\n└── renovate.json\n","text",[74,92,89],{"__ignoreMap":93},"",[27,95,96,99,100,103,104,107,108,111,112,115,116,118],{},[74,97,98],{},"aliyun"," 目录中的 ",[74,101,102],{},"gateway"," 和 ",[74,105,106],{},"hk"," 是我在杭州和香港的两台服务器，部署一些外网项目，也是通过上述的方式管理。",[74,109,110],{},"homelab"," 中的 ",[74,113,114],{},"scripts"," 是一些脚本，与本文无关，但也是通过 ",[74,117,76],{}," 项目统一管理的。",[27,120,80,121,124],{},[74,122,123],{},"renovate.json"," 如下：",[85,126,130],{"className":127,"code":128,"language":129,"meta":93,"style":93},"language-json shiki shiki-themes material-theme-lighter github-light github-dark","{\n  \"$schema\": \"https:\u002F\u002Fdocs.renovatebot.com\u002Frenovate-schema.json\",\n  \"extends\": [\"config:recommended\", \"group:allNonMajor\", \":semanticCommitTypeAll(chore)\"],\n  \"packageRules\": [\n    {\n      \"matchManagers\": [\"docker-compose\"],\n      \"matchPackageNames\": [\"sonatype\u002Fnexus3\", \"clickhouse\u002Fclickhouse-server\"],\n      \"enabled\": false\n    },\n    {\n      \"matchManagers\": [\"docker-compose\"],\n      \"matchPackageNames\": [\"gitlab\u002Fgitlab-runner\"],\n      \"versionCompatibility\": \"^(?\u003Ccompatibility>.*)-(?\u003Cversion>.*)$\"\n    },\n    {\n      \"matchManagers\": [\"docker-compose\"],\n      \"matchUpdateTypes\": [\"minor\", \"patch\"],\n      \"automerge\": true,\n      \"schedule\": [\"before 11am on Monday\"]\n    }\n  ],\n  \"rangeStrategy\": \"bump\",\n  \"timezone\": \"Asia\u002FShanghai\"\n}\n","json",[74,131,132,141,171,215,230,236,261,293,308,314,319,340,362,382,387,392,413,445,462,486,492,498,519,538],{"__ignoreMap":93},[133,134,137],"span",{"class":135,"line":136},"line",1,[133,138,140],{"class":139},"sP7_E","{\n",[133,142,144,148,152,155,158,162,166,168],{"class":135,"line":143},2,[133,145,147],{"class":146},"s39Yj","  \"",[133,149,151],{"class":150},"sseR_","$schema",[133,153,154],{"class":146},"\"",[133,156,157],{"class":139},":",[133,159,161],{"class":160},"sjJ54"," \"",[133,163,165],{"class":164},"s_sjI","https:\u002F\u002Fdocs.renovatebot.com\u002Frenovate-schema.json",[133,167,154],{"class":160},[133,169,170],{"class":139},",\n",[133,172,174,176,179,181,183,186,188,191,193,196,198,201,203,205,207,210,212],{"class":135,"line":173},3,[133,175,147],{"class":146},[133,177,178],{"class":150},"extends",[133,180,154],{"class":146},[133,182,157],{"class":139},[133,184,185],{"class":139}," [",[133,187,154],{"class":160},[133,189,190],{"class":164},"config:recommended",[133,192,154],{"class":160},[133,194,195],{"class":139},",",[133,197,161],{"class":160},[133,199,200],{"class":164},"group:allNonMajor",[133,202,154],{"class":160},[133,204,195],{"class":139},[133,206,161],{"class":160},[133,208,209],{"class":164},":semanticCommitTypeAll(chore)",[133,211,154],{"class":160},[133,213,214],{"class":139},"],\n",[133,216,218,220,223,225,227],{"class":135,"line":217},4,[133,219,147],{"class":146},[133,221,222],{"class":150},"packageRules",[133,224,154],{"class":146},[133,226,157],{"class":139},[133,228,229],{"class":139}," [\n",[133,231,233],{"class":135,"line":232},5,[133,234,235],{"class":139},"    {\n",[133,237,239,242,246,248,250,252,254,257,259],{"class":135,"line":238},6,[133,240,241],{"class":146},"      \"",[133,243,245],{"class":244},"sZMiF","matchManagers",[133,247,154],{"class":146},[133,249,157],{"class":139},[133,251,185],{"class":139},[133,253,154],{"class":160},[133,255,256],{"class":164},"docker-compose",[133,258,154],{"class":160},[133,260,214],{"class":139},[133,262,264,266,269,271,273,275,277,280,282,284,286,289,291],{"class":135,"line":263},7,[133,265,241],{"class":146},[133,267,268],{"class":244},"matchPackageNames",[133,270,154],{"class":146},[133,272,157],{"class":139},[133,274,185],{"class":139},[133,276,154],{"class":160},[133,278,279],{"class":164},"sonatype\u002Fnexus3",[133,281,154],{"class":160},[133,283,195],{"class":139},[133,285,161],{"class":160},[133,287,288],{"class":164},"clickhouse\u002Fclickhouse-server",[133,290,154],{"class":160},[133,292,214],{"class":139},[133,294,296,298,301,303,305],{"class":135,"line":295},8,[133,297,241],{"class":146},[133,299,300],{"class":244},"enabled",[133,302,154],{"class":146},[133,304,157],{"class":139},[133,306,307],{"class":146}," false\n",[133,309,311],{"class":135,"line":310},9,[133,312,313],{"class":139},"    },\n",[133,315,317],{"class":135,"line":316},10,[133,318,235],{"class":139},[133,320,322,324,326,328,330,332,334,336,338],{"class":135,"line":321},11,[133,323,241],{"class":146},[133,325,245],{"class":244},[133,327,154],{"class":146},[133,329,157],{"class":139},[133,331,185],{"class":139},[133,333,154],{"class":160},[133,335,256],{"class":164},[133,337,154],{"class":160},[133,339,214],{"class":139},[133,341,343,345,347,349,351,353,355,358,360],{"class":135,"line":342},12,[133,344,241],{"class":146},[133,346,268],{"class":244},[133,348,154],{"class":146},[133,350,157],{"class":139},[133,352,185],{"class":139},[133,354,154],{"class":160},[133,356,357],{"class":164},"gitlab\u002Fgitlab-runner",[133,359,154],{"class":160},[133,361,214],{"class":139},[133,363,365,367,370,372,374,376,379],{"class":135,"line":364},13,[133,366,241],{"class":146},[133,368,369],{"class":244},"versionCompatibility",[133,371,154],{"class":146},[133,373,157],{"class":139},[133,375,161],{"class":160},[133,377,378],{"class":164},"^(?\u003Ccompatibility>.*)-(?\u003Cversion>.*)$",[133,380,381],{"class":160},"\"\n",[133,383,385],{"class":135,"line":384},14,[133,386,313],{"class":139},[133,388,390],{"class":135,"line":389},15,[133,391,235],{"class":139},[133,393,395,397,399,401,403,405,407,409,411],{"class":135,"line":394},16,[133,396,241],{"class":146},[133,398,245],{"class":244},[133,400,154],{"class":146},[133,402,157],{"class":139},[133,404,185],{"class":139},[133,406,154],{"class":160},[133,408,256],{"class":164},[133,410,154],{"class":160},[133,412,214],{"class":139},[133,414,416,418,421,423,425,427,429,432,434,436,438,441,443],{"class":135,"line":415},17,[133,417,241],{"class":146},[133,419,420],{"class":244},"matchUpdateTypes",[133,422,154],{"class":146},[133,424,157],{"class":139},[133,426,185],{"class":139},[133,428,154],{"class":160},[133,430,431],{"class":164},"minor",[133,433,154],{"class":160},[133,435,195],{"class":139},[133,437,161],{"class":160},[133,439,440],{"class":164},"patch",[133,442,154],{"class":160},[133,444,214],{"class":139},[133,446,448,450,453,455,457,460],{"class":135,"line":447},18,[133,449,241],{"class":146},[133,451,452],{"class":244},"automerge",[133,454,154],{"class":146},[133,456,157],{"class":139},[133,458,459],{"class":146}," true",[133,461,170],{"class":139},[133,463,465,467,470,472,474,476,478,481,483],{"class":135,"line":464},19,[133,466,241],{"class":146},[133,468,469],{"class":244},"schedule",[133,471,154],{"class":146},[133,473,157],{"class":139},[133,475,185],{"class":139},[133,477,154],{"class":160},[133,479,480],{"class":164},"before 11am on Monday",[133,482,154],{"class":160},[133,484,485],{"class":139},"]\n",[133,487,489],{"class":135,"line":488},20,[133,490,491],{"class":139},"    }\n",[133,493,495],{"class":135,"line":494},21,[133,496,497],{"class":139},"  ],\n",[133,499,501,503,506,508,510,512,515,517],{"class":135,"line":500},22,[133,502,147],{"class":146},[133,504,505],{"class":150},"rangeStrategy",[133,507,154],{"class":146},[133,509,157],{"class":139},[133,511,161],{"class":160},[133,513,514],{"class":164},"bump",[133,516,154],{"class":160},[133,518,170],{"class":139},[133,520,522,524,527,529,531,533,536],{"class":135,"line":521},23,[133,523,147],{"class":146},[133,525,526],{"class":150},"timezone",[133,528,154],{"class":146},[133,530,157],{"class":139},[133,532,161],{"class":160},[133,534,535],{"class":164},"Asia\u002FShanghai",[133,537,381],{"class":160},[133,539,541],{"class":135,"line":540},24,[133,542,543],{"class":139},"}\n",[27,545,546,547,103,549,551,552,554],{},"里面有一些自定义的规则，比如禁用了 ",[74,548,279],{},[74,550,288],{}," 的自动更新，以及适配了 ",[74,553,357],{}," 的不规则的版本号。",[27,556,557,124],{},[74,558,559],{},".gitlab-ci.yml",[85,561,565],{"className":562,"code":563,"language":564,"meta":93,"style":93},"language-yaml shiki shiki-themes material-theme-lighter github-light github-dark","stages:\n  - pass\n  - deploy\n\npass:\n  stage: pass\n  script:\n    - echo \"Current branch is $CI_COMMIT_BRANCH, pass.\"\n  except:\n    - main\n\ndeploy:\n  stage: deploy\n  before_script:\n    - eval $(ssh-agent -s)\n    - echo \"$SSH_PRIVATE_KEY\" | ssh-add -\n    - mkdir -p ~\u002F.ssh\n    - echo \"$SSH_KNOWN_HOSTS\" > ~\u002F.ssh\u002Fknown_hosts\n\n  script:\n    - MODIFIED_FILES=$(git diff --name-only --diff-filter=ACMRT $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA)\n    - DELETED_FILES=$(git diff --name-status --diff-filter=DR $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | awk '{print $2}')\n    - |\n      for FILE in $DELETED_FILES; do\n        echo \"文件移除 $FILE\"\n        # 如果文件是 compose.yaml，执行 docker compose down\n        if [[ \"$FILE\" == \"*\u002Fcompose.yaml\" ]]; then\n          if [[ \"$FILE\" == \"homelab\u002Fsynology\u002F*\" ]]; then\n            DEST_DIR=\"\u002Fvolume3\u002Fdocker\u002F$(dirname ${FILE#homelab\u002F})\"\n            echo \"退出容器 synology:$(dirname ${FILE#homelab\u002Fsynology\u002F})\"\n            ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && cd $DEST_DIR && docker-compose down\"\n          fi\n\n          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002F*\" ]]; then\n            DEST_DIR=\"\u002Froot\u002F$(dirname ${FILE#homelab\u002Fvm-rocky-01\u002F})\"\n            echo \"退出容器 vm-rocky-01:$(dirname ${FILE#homelab\u002Fvm-rocky-01\u002F})\"\n            ssh root@vm-rocky-01.home \"cd $DEST_DIR && docker compose down\"\n          fi\n\n          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002F*\" ]]; then\n            DEST_DIR=\"\u002Froot\u002F$(dirname ${FILE#homelab\u002Fvm-rocky-02\u002F})\"\n            echo \"退出容器 vm-rocky-02:$(dirname ${FILE#homelab\u002Fvm-rocky-02\u002F})\"\n            ssh root@vm-rocky-02.home \"cd $DEST_DIR && docker compose down\"\n          fi\n\n          # if [[ \"$FILE\" == \"aliyun\u002Fgateway\u002F*\" ]]; then\n          #   DEST_DIR=\"\u002Froot\u002Fconfig-files\u002Fgateway\u002F$(dirname ${FILE#aliyun\u002Fgateway\u002F})\"\n          #   echo \"退出容器 gateway:$(dirname ${FILE#aliyun\u002Fgateway\u002F})\"\n          #   ssh root@gateway.aliyun \"cd $DEST_DIR && docker compose down\"\n          # fi\n        fi\n\n        # 删除对应文件\n        if [[ \"$FILE\" == \"homelab\u002Fsynology\u002F*\" ]]; then\n          FILE_PATH=\"\u002Fvolume3\u002Fdocker\u002F${FILE#homelab\u002F}\"\n          echo \"从 Synology 删除文件: $FILE_PATH\"\n          ssh -p 5110 root@synology.home \"rm -f $FILE_PATH\"\n        fi\n\n        if [[  \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002F*\" ]]; then\n          FILE_PATH=\"\u002Froot\u002F${FILE#homelab\u002Fvm-rocky-01\u002F}\"\n          echo \"从 vm-rocky-01 删除文件: $FILE_PATH\"\n          ssh root@vm-rocky-01.home \"rm -f $FILE_PATH\"\n        fi\n\n        if [[  \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002F*\" ]]; then\n          FILE_PATH=\"\u002Froot\u002F${FILE#homelab\u002Fvm-rocky-02\u002F}\"\n          echo \"从 vm-rocky-02 删除文件: $FILE_PATH\"\n          ssh root@vm-rocky-02.home \"rm -f $FILE_PATH\"\n        fi\n\n        if [[ \"$FILE\" == \"aliyun\u002Fgateway\u002F*\" ]]; then\n          FILE_PATH=\"\u002Froot\u002Fconfig-files\u002Fgateway\u002F${FILE#aliyun\u002Fgateway\u002F}\"\n          echo \"从 gateway.aliyun 删除文件: $FILE_PATH\"\n          ssh root@gateway.aliyun \"rm -f $FILE_PATH\"\n        fi\n      done\n\n      for FILE in $MODIFIED_FILES; do\n        echo \"文件变更 $FILE\"\n        if [ -e \"$FILE\" ]; then\n          if [[ \"$FILE\" == \"homelab\u002Fsynology\u002F*\" ]]; then\n            DEST_DIR=\"\u002Fvolume3\u002Fdocker\u002F$(dirname ${FILE#homelab\u002F})\"\n            ssh -p 5110 root@synology.home \"mkdir -p $DEST_DIR\"\n            echo \"复制文件 $FILE 到 Synology: $DEST_DIR\"\n            scp -O -P 5110 $FILE root@synology.home:$DEST_DIR\n          fi\n\n          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002F*\" ]]; then\n            DEST_DIR=\"\u002Froot\u002F$(dirname ${FILE#homelab\u002Fvm-rocky-01\u002F})\"\n            ssh root@vm-rocky-01.home \"mkdir -p $DEST_DIR\"\n            echo \"复制文件 $FILE 到 vm-rocky-01: $DEST_DIR\"\n            scp -O $FILE root@vm-rocky-01.home:$DEST_DIR\n          fi\n\n          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002F*\" ]]; then\n            DEST_DIR=\"\u002Froot\u002F$(dirname ${FILE#homelab\u002Fvm-rocky-02\u002F})\"\n            ssh root@vm-rocky-02.home \"mkdir -p $DEST_DIR\"\n            echo \"复制文件 $FILE 到 vm-rocky-02: $DEST_DIR\"\n            scp -O $FILE root@vm-rocky-02.home:$DEST_DIR\n          fi\n\n          if [[ \"$FILE\" == \"aliyun\u002Fgateway\u002F*\" ]]; then\n            DEST_DIR=\"\u002Froot\u002Fconfig-files\u002Fgateway\u002F$(dirname ${FILE#aliyun\u002Fgateway\u002F})\"\n            ssh root@gateway.aliyun \"mkdir -p $DEST_DIR\"\n            echo \"复制文件 $FILE 到 gateway.aliyun: $DEST_DIR\"\n            scp -O $FILE root@gateway.aliyun:$DEST_DIR\n          fi\n\n          if [[ \"$FILE\" == \"aliyun\u002Fhk\u002Fnginx\u002F*\" ]]; then\n            DEST_DIR=\"\u002Fetc\u002Fnginx\u002F$(dirname ${FILE#aliyun\u002Fhk\u002Fnginx\u002F})\"\n            ssh root@hk.aliyun \"mkdir -p $DEST_DIR\"\n            echo \"复制文件 $FILE 到 hk.aliyun: $DEST_DIR\"\n            scp -O $FILE root@hk.aliyun:$DEST_DIR\n          fi\n        fi\n      done\n\n      for FILE in $MODIFIED_FILES; do\n        if [ -e \"$FILE\" ]; then\n          # 如果文件是 compose.yaml，执行 docker compose up -d\n          if [[ \"$FILE\" == \"*\u002Fcompose.yaml\" ]]; then\n            if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002Frenovate\u002Fcompose.yaml\" ]]; then\n              echo \"跳过部署 vm-rocky-01:renovate\"\n              continue\n            fi\n\n            if [[ \"$FILE\" == \"homelab\u002Fsynology\u002F*\" ]]; then\n              DEST_DIR=\"\u002Fvolume3\u002Fdocker\u002F$(dirname ${FILE#homelab\u002F})\"\n              echo \"部署容器 synology:$(dirname ${FILE#homelab\u002Fsynology\u002F})\"\n              ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && cd $DEST_DIR && docker-compose up -d\"\n            fi\n\n            if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002F*\" ]]; then\n              DEST_DIR=\"\u002Froot\u002F$(dirname ${FILE#homelab\u002Fvm-rocky-01\u002F})\"\n              echo \"部署容器 vm-rocky-01:$(dirname ${FILE#homelab\u002Fvm-rocky-01\u002F})\"\n              ssh root@vm-rocky-01.home \"cd $DEST_DIR && docker compose up -d\"\n            fi\n\n            if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002F*\" ]]; then\n              DEST_DIR=\"\u002Froot\u002F$(dirname ${FILE#homelab\u002Fvm-rocky-02\u002F})\"\n              echo \"部署容器 vm-rocky-02:$(dirname ${FILE#homelab\u002Fvm-rocky-02\u002F})\"\n              ssh root@vm-rocky-02.home \"cd $DEST_DIR && docker compose up -d\"\n            fi\n\n            # if [[ \"$FILE\" == \"aliyun\u002Fgateway\u002F*\" ]]; then\n            #   DEST_DIR=\"\u002Froot\u002Fconfig-files\u002Fgateway\u002F$(dirname ${FILE#aliyun\u002Fgateway\u002F})\"\n            #   echo \"部署容器 gateway.aliyun:$(dirname ${FILE#aliyun\u002Fgateway\u002F})\"\n            #   ssh root@gateway.aliyun \"cd $DEST_DIR && docker compose up -d\"\n            # fi\n\n            echo \"容器重新部署完成\"\n            continue\n          fi\n\n          # 如果文件是 homelab\u002Fsynology\u002Fnginx\u002F*, 重新启动 nginx\n          if [[ \"$FILE\" == \"homelab\u002Fsynology\u002Fnginx\u002F*\" ]]; then\n            echo \"重新启动 synology:nginx 服务\"\n            ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && docker exec nginx nginx -s reload\"\n          fi\n\n          # 如果文件是 aliyun\u002Fgateway\u002Fnginx\u002F*, 重新启动 nginx\n          if [[ \"$FILE\" == \"aliyun\u002Fgateway\u002Fnginx\u002F*\" ]]; then\n            echo \"重新启动 gateway.aliyun:nginx\"\n            ssh root@gateway.aliyun \"nginx -s reload\"\n          fi\n\n          # 如果文件是 aliyun\u002Fhk\u002Fnginx\u002F*, 重新启动 nginx\n          if [[ \"$FILE\" == \"aliyun\u002Fhk\u002Fnginx\u002F*\" ]]; then\n            echo \"重新启动 hk.aliyun:nginx\"\n            ssh root@hk.aliyun \"nginx -s reload\"\n          fi\n\n          # 如果文件是 rinetd.conf, 重新启动 rinetd\n          if [[ \"$FILE\" == \"homelab\u002Fsynology\u002Frinetd\u002Fconfig\u002Frinetd.conf\" ]]; then\n            echo \"重新启动 synology:rinetd 服务\"\n            ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && cd \u002Fvolume3\u002Fdocker\u002Fsynology\u002Frinetd && docker-compose restart\"\n          fi\n\n          # 如果文件是 gitlab.rb, 重新配置 gitlab\n          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002Fgitlab\u002Fconfig\u002Fgitlab.rb\" ]]; then\n            echo \"重新配置 vm-rocky-01:gitlab\"\n            ssh root@vm-rocky-01.home \"docker exec gitlab gitlab-ctl reconfigure\"\n          fi\n\n          # 如果文件是 prometheus.yml, 重新启动 prometheus\n          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002Fprometheus\u002Fconfig\u002Fprometheus.yml\" ]]; then\n            echo \"重新启动 vm-rocky-01:prometheus\"\n            ssh root@vm-rocky-01.home \"cd \u002Froot\u002Fprometheus && docker compose restart\"\n          fi\n\n          # 如果文件是 telegraf.conf, 重新启动 telegraf\n          if [[ \"$FILE\" == \"homelab\u002Fsynology\u002Ftelegraf\u002Fconf\u002Ftelegraf.conf\" ]]; then\n            echo \"重新启动 synology:telegraf\"\n            ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && cd \u002Fvolume3\u002Fdocker\u002Fsynology\u002Ftelegraf && docker-compose restart\"\n          fi\n\n          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002Ftelegraf\u002Fconf\u002Ftelegraf.conf\" ]]; then\n            echo \"重新启动 vm-rocky-01:telegraf\"\n            ssh root@vm-rocky-01.home \"cd \u002Froot\u002Ftelegraf && docker compose restart\"\n          fi\n\n          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002Ftelegraf\u002Fconf\u002Ftelegraf.conf\" ]]; then\n            echo \"重新启动 vm-rocky-02:telegraf\"\n            ssh root@vm-rocky-02.home \"cd \u002Froot\u002Ftelegraf && docker compose restart\"\n          fi\n\n          # 如果文件是 bots\u002Fconfig\u002Fclash\u002F*.yaml, 更新 clash 配置\n          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002Fbots\u002Fconfig\u002Fclash\u002F*.yaml\" ]]; then\n            echo \"重新启动 vm-rocky-02:bots\"\n            ssh root@vm-rocky-02.home \"cd \u002Froot\u002Fbots && docker compose restart\"\n            echo \"等待 10 秒\"\n            sleep 10\n            echo \"重新启动 synology:clash-premium\"\n            ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && cd \u002Fvolume3\u002Fdocker\u002Fsynology\u002Fclash-premium && docker-compose restart\"\n          fi\n\n          # 如果文件是 artalk\u002Fdata\u002Fartalk.yml, 重新启动 artalk\n          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002Fartalk\u002Fdata\u002Fartalk.yml\" ]]; then\n            echo \"重新启动 vm-rocky-02:artalk\"\n            ssh root@vm-rocky-02.home \"cd \u002Froot\u002Fartalk && docker compose restart\"\n          fi\n\n        fi\n      done\n    - echo \"部署完成\"\n  only:\n    - main\n","yaml",[74,566,567,576,584,591,597,604,613,620,628,635,642,646,653,661,668,675,682,689,696,700,706,713,720,728,733,739,745,751,757,763,769,775,781,786,792,798,804,810,815,820,826,832,838,844,849,854,860,866,872,878,884,890,895,901,907,913,919,925,930,935,941,947,953,959,964,969,975,981,987,993,998,1003,1009,1015,1021,1027,1032,1038,1043,1049,1055,1061,1066,1071,1077,1083,1089,1094,1099,1104,1109,1115,1121,1127,1132,1137,1142,1147,1153,1159,1165,1170,1175,1181,1187,1193,1199,1205,1210,1215,1221,1227,1233,1239,1245,1250,1255,1260,1265,1270,1275,1281,1287,1293,1299,1305,1311,1316,1322,1328,1334,1340,1345,1350,1356,1362,1368,1374,1379,1384,1390,1396,1402,1408,1413,1418,1424,1430,1436,1442,1448,1453,1459,1465,1470,1475,1481,1487,1493,1499,1504,1509,1515,1521,1527,1533,1538,1543,1549,1554,1560,1566,1571,1576,1582,1588,1594,1600,1605,1610,1616,1622,1628,1634,1639,1644,1650,1656,1662,1668,1673,1678,1684,1690,1696,1702,1707,1712,1718,1724,1730,1735,1740,1746,1752,1758,1763,1768,1774,1780,1786,1792,1798,1804,1810,1816,1821,1826,1832,1838,1844,1850,1855,1860,1865,1870,1878,1886],{"__ignoreMap":93},[133,568,569,573],{"class":135,"line":136},[133,570,572],{"class":571},"sQzsp","stages",[133,574,575],{"class":139},":\n",[133,577,578,581],{"class":135,"line":143},[133,579,580],{"class":139},"  -",[133,582,583],{"class":164}," pass\n",[133,585,586,588],{"class":135,"line":173},[133,587,580],{"class":139},[133,589,590],{"class":164}," deploy\n",[133,592,593],{"class":135,"line":217},[133,594,596],{"emptyLinePlaceholder":595},true,"\n",[133,598,599,602],{"class":135,"line":232},[133,600,601],{"class":571},"pass",[133,603,575],{"class":139},[133,605,606,609,611],{"class":135,"line":238},[133,607,608],{"class":571},"  stage",[133,610,157],{"class":139},[133,612,583],{"class":164},[133,614,615,618],{"class":135,"line":263},[133,616,617],{"class":571},"  script",[133,619,575],{"class":139},[133,621,622,625],{"class":135,"line":295},[133,623,624],{"class":139},"    -",[133,626,627],{"class":164}," echo \"Current branch is $CI_COMMIT_BRANCH, pass.\"\n",[133,629,630,633],{"class":135,"line":310},[133,631,632],{"class":571},"  except",[133,634,575],{"class":139},[133,636,637,639],{"class":135,"line":316},[133,638,624],{"class":139},[133,640,641],{"class":164}," main\n",[133,643,644],{"class":135,"line":321},[133,645,596],{"emptyLinePlaceholder":595},[133,647,648,651],{"class":135,"line":342},[133,649,650],{"class":571},"deploy",[133,652,575],{"class":139},[133,654,655,657,659],{"class":135,"line":364},[133,656,608],{"class":571},[133,658,157],{"class":139},[133,660,590],{"class":164},[133,662,663,666],{"class":135,"line":384},[133,664,665],{"class":571},"  before_script",[133,667,575],{"class":139},[133,669,670,672],{"class":135,"line":389},[133,671,624],{"class":139},[133,673,674],{"class":164}," eval $(ssh-agent -s)\n",[133,676,677,679],{"class":135,"line":394},[133,678,624],{"class":139},[133,680,681],{"class":164}," echo \"$SSH_PRIVATE_KEY\" | ssh-add -\n",[133,683,684,686],{"class":135,"line":415},[133,685,624],{"class":139},[133,687,688],{"class":164}," mkdir -p ~\u002F.ssh\n",[133,690,691,693],{"class":135,"line":447},[133,692,624],{"class":139},[133,694,695],{"class":164}," echo \"$SSH_KNOWN_HOSTS\" > ~\u002F.ssh\u002Fknown_hosts\n",[133,697,698],{"class":135,"line":464},[133,699,596],{"emptyLinePlaceholder":595},[133,701,702,704],{"class":135,"line":488},[133,703,617],{"class":571},[133,705,575],{"class":139},[133,707,708,710],{"class":135,"line":494},[133,709,624],{"class":139},[133,711,712],{"class":164}," MODIFIED_FILES=$(git diff --name-only --diff-filter=ACMRT $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA)\n",[133,714,715,717],{"class":135,"line":500},[133,716,624],{"class":139},[133,718,719],{"class":164}," DELETED_FILES=$(git diff --name-status --diff-filter=DR $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | awk '{print $2}')\n",[133,721,722,724],{"class":135,"line":521},[133,723,624],{"class":139},[133,725,727],{"class":726},"sVHd0"," |\n",[133,729,730],{"class":135,"line":540},[133,731,732],{"class":164},"      for FILE in $DELETED_FILES; do\n",[133,734,736],{"class":135,"line":735},25,[133,737,738],{"class":164},"        echo \"文件移除 $FILE\"\n",[133,740,742],{"class":135,"line":741},26,[133,743,744],{"class":164},"        # 如果文件是 compose.yaml，执行 docker compose down\n",[133,746,748],{"class":135,"line":747},27,[133,749,750],{"class":164},"        if [[ \"$FILE\" == \"*\u002Fcompose.yaml\" ]]; then\n",[133,752,754],{"class":135,"line":753},28,[133,755,756],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fsynology\u002F*\" ]]; then\n",[133,758,760],{"class":135,"line":759},29,[133,761,762],{"class":164},"            DEST_DIR=\"\u002Fvolume3\u002Fdocker\u002F$(dirname ${FILE#homelab\u002F})\"\n",[133,764,766],{"class":135,"line":765},30,[133,767,768],{"class":164},"            echo \"退出容器 synology:$(dirname ${FILE#homelab\u002Fsynology\u002F})\"\n",[133,770,772],{"class":135,"line":771},31,[133,773,774],{"class":164},"            ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && cd $DEST_DIR && docker-compose down\"\n",[133,776,778],{"class":135,"line":777},32,[133,779,780],{"class":164},"          fi\n",[133,782,784],{"class":135,"line":783},33,[133,785,596],{"emptyLinePlaceholder":595},[133,787,789],{"class":135,"line":788},34,[133,790,791],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002F*\" ]]; then\n",[133,793,795],{"class":135,"line":794},35,[133,796,797],{"class":164},"            DEST_DIR=\"\u002Froot\u002F$(dirname ${FILE#homelab\u002Fvm-rocky-01\u002F})\"\n",[133,799,801],{"class":135,"line":800},36,[133,802,803],{"class":164},"            echo \"退出容器 vm-rocky-01:$(dirname ${FILE#homelab\u002Fvm-rocky-01\u002F})\"\n",[133,805,807],{"class":135,"line":806},37,[133,808,809],{"class":164},"            ssh root@vm-rocky-01.home \"cd $DEST_DIR && docker compose down\"\n",[133,811,813],{"class":135,"line":812},38,[133,814,780],{"class":164},[133,816,818],{"class":135,"line":817},39,[133,819,596],{"emptyLinePlaceholder":595},[133,821,823],{"class":135,"line":822},40,[133,824,825],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002F*\" ]]; then\n",[133,827,829],{"class":135,"line":828},41,[133,830,831],{"class":164},"            DEST_DIR=\"\u002Froot\u002F$(dirname ${FILE#homelab\u002Fvm-rocky-02\u002F})\"\n",[133,833,835],{"class":135,"line":834},42,[133,836,837],{"class":164},"            echo \"退出容器 vm-rocky-02:$(dirname ${FILE#homelab\u002Fvm-rocky-02\u002F})\"\n",[133,839,841],{"class":135,"line":840},43,[133,842,843],{"class":164},"            ssh root@vm-rocky-02.home \"cd $DEST_DIR && docker compose down\"\n",[133,845,847],{"class":135,"line":846},44,[133,848,780],{"class":164},[133,850,852],{"class":135,"line":851},45,[133,853,596],{"emptyLinePlaceholder":595},[133,855,857],{"class":135,"line":856},46,[133,858,859],{"class":164},"          # if [[ \"$FILE\" == \"aliyun\u002Fgateway\u002F*\" ]]; then\n",[133,861,863],{"class":135,"line":862},47,[133,864,865],{"class":164},"          #   DEST_DIR=\"\u002Froot\u002Fconfig-files\u002Fgateway\u002F$(dirname ${FILE#aliyun\u002Fgateway\u002F})\"\n",[133,867,869],{"class":135,"line":868},48,[133,870,871],{"class":164},"          #   echo \"退出容器 gateway:$(dirname ${FILE#aliyun\u002Fgateway\u002F})\"\n",[133,873,875],{"class":135,"line":874},49,[133,876,877],{"class":164},"          #   ssh root@gateway.aliyun \"cd $DEST_DIR && docker compose down\"\n",[133,879,881],{"class":135,"line":880},50,[133,882,883],{"class":164},"          # fi\n",[133,885,887],{"class":135,"line":886},51,[133,888,889],{"class":164},"        fi\n",[133,891,893],{"class":135,"line":892},52,[133,894,596],{"emptyLinePlaceholder":595},[133,896,898],{"class":135,"line":897},53,[133,899,900],{"class":164},"        # 删除对应文件\n",[133,902,904],{"class":135,"line":903},54,[133,905,906],{"class":164},"        if [[ \"$FILE\" == \"homelab\u002Fsynology\u002F*\" ]]; then\n",[133,908,910],{"class":135,"line":909},55,[133,911,912],{"class":164},"          FILE_PATH=\"\u002Fvolume3\u002Fdocker\u002F${FILE#homelab\u002F}\"\n",[133,914,916],{"class":135,"line":915},56,[133,917,918],{"class":164},"          echo \"从 Synology 删除文件: $FILE_PATH\"\n",[133,920,922],{"class":135,"line":921},57,[133,923,924],{"class":164},"          ssh -p 5110 root@synology.home \"rm -f $FILE_PATH\"\n",[133,926,928],{"class":135,"line":927},58,[133,929,889],{"class":164},[133,931,933],{"class":135,"line":932},59,[133,934,596],{"emptyLinePlaceholder":595},[133,936,938],{"class":135,"line":937},60,[133,939,940],{"class":164},"        if [[  \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002F*\" ]]; then\n",[133,942,944],{"class":135,"line":943},61,[133,945,946],{"class":164},"          FILE_PATH=\"\u002Froot\u002F${FILE#homelab\u002Fvm-rocky-01\u002F}\"\n",[133,948,950],{"class":135,"line":949},62,[133,951,952],{"class":164},"          echo \"从 vm-rocky-01 删除文件: $FILE_PATH\"\n",[133,954,956],{"class":135,"line":955},63,[133,957,958],{"class":164},"          ssh root@vm-rocky-01.home \"rm -f $FILE_PATH\"\n",[133,960,962],{"class":135,"line":961},64,[133,963,889],{"class":164},[133,965,967],{"class":135,"line":966},65,[133,968,596],{"emptyLinePlaceholder":595},[133,970,972],{"class":135,"line":971},66,[133,973,974],{"class":164},"        if [[  \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002F*\" ]]; then\n",[133,976,978],{"class":135,"line":977},67,[133,979,980],{"class":164},"          FILE_PATH=\"\u002Froot\u002F${FILE#homelab\u002Fvm-rocky-02\u002F}\"\n",[133,982,984],{"class":135,"line":983},68,[133,985,986],{"class":164},"          echo \"从 vm-rocky-02 删除文件: $FILE_PATH\"\n",[133,988,990],{"class":135,"line":989},69,[133,991,992],{"class":164},"          ssh root@vm-rocky-02.home \"rm -f $FILE_PATH\"\n",[133,994,996],{"class":135,"line":995},70,[133,997,889],{"class":164},[133,999,1001],{"class":135,"line":1000},71,[133,1002,596],{"emptyLinePlaceholder":595},[133,1004,1006],{"class":135,"line":1005},72,[133,1007,1008],{"class":164},"        if [[ \"$FILE\" == \"aliyun\u002Fgateway\u002F*\" ]]; then\n",[133,1010,1012],{"class":135,"line":1011},73,[133,1013,1014],{"class":164},"          FILE_PATH=\"\u002Froot\u002Fconfig-files\u002Fgateway\u002F${FILE#aliyun\u002Fgateway\u002F}\"\n",[133,1016,1018],{"class":135,"line":1017},74,[133,1019,1020],{"class":164},"          echo \"从 gateway.aliyun 删除文件: $FILE_PATH\"\n",[133,1022,1024],{"class":135,"line":1023},75,[133,1025,1026],{"class":164},"          ssh root@gateway.aliyun \"rm -f $FILE_PATH\"\n",[133,1028,1030],{"class":135,"line":1029},76,[133,1031,889],{"class":164},[133,1033,1035],{"class":135,"line":1034},77,[133,1036,1037],{"class":164},"      done\n",[133,1039,1041],{"class":135,"line":1040},78,[133,1042,596],{"emptyLinePlaceholder":595},[133,1044,1046],{"class":135,"line":1045},79,[133,1047,1048],{"class":164},"      for FILE in $MODIFIED_FILES; do\n",[133,1050,1052],{"class":135,"line":1051},80,[133,1053,1054],{"class":164},"        echo \"文件变更 $FILE\"\n",[133,1056,1058],{"class":135,"line":1057},81,[133,1059,1060],{"class":164},"        if [ -e \"$FILE\" ]; then\n",[133,1062,1064],{"class":135,"line":1063},82,[133,1065,756],{"class":164},[133,1067,1069],{"class":135,"line":1068},83,[133,1070,762],{"class":164},[133,1072,1074],{"class":135,"line":1073},84,[133,1075,1076],{"class":164},"            ssh -p 5110 root@synology.home \"mkdir -p $DEST_DIR\"\n",[133,1078,1080],{"class":135,"line":1079},85,[133,1081,1082],{"class":164},"            echo \"复制文件 $FILE 到 Synology: $DEST_DIR\"\n",[133,1084,1086],{"class":135,"line":1085},86,[133,1087,1088],{"class":164},"            scp -O -P 5110 $FILE root@synology.home:$DEST_DIR\n",[133,1090,1092],{"class":135,"line":1091},87,[133,1093,780],{"class":164},[133,1095,1097],{"class":135,"line":1096},88,[133,1098,596],{"emptyLinePlaceholder":595},[133,1100,1102],{"class":135,"line":1101},89,[133,1103,791],{"class":164},[133,1105,1107],{"class":135,"line":1106},90,[133,1108,797],{"class":164},[133,1110,1112],{"class":135,"line":1111},91,[133,1113,1114],{"class":164},"            ssh root@vm-rocky-01.home \"mkdir -p $DEST_DIR\"\n",[133,1116,1118],{"class":135,"line":1117},92,[133,1119,1120],{"class":164},"            echo \"复制文件 $FILE 到 vm-rocky-01: $DEST_DIR\"\n",[133,1122,1124],{"class":135,"line":1123},93,[133,1125,1126],{"class":164},"            scp -O $FILE root@vm-rocky-01.home:$DEST_DIR\n",[133,1128,1130],{"class":135,"line":1129},94,[133,1131,780],{"class":164},[133,1133,1135],{"class":135,"line":1134},95,[133,1136,596],{"emptyLinePlaceholder":595},[133,1138,1140],{"class":135,"line":1139},96,[133,1141,825],{"class":164},[133,1143,1145],{"class":135,"line":1144},97,[133,1146,831],{"class":164},[133,1148,1150],{"class":135,"line":1149},98,[133,1151,1152],{"class":164},"            ssh root@vm-rocky-02.home \"mkdir -p $DEST_DIR\"\n",[133,1154,1156],{"class":135,"line":1155},99,[133,1157,1158],{"class":164},"            echo \"复制文件 $FILE 到 vm-rocky-02: $DEST_DIR\"\n",[133,1160,1162],{"class":135,"line":1161},100,[133,1163,1164],{"class":164},"            scp -O $FILE root@vm-rocky-02.home:$DEST_DIR\n",[133,1166,1168],{"class":135,"line":1167},101,[133,1169,780],{"class":164},[133,1171,1173],{"class":135,"line":1172},102,[133,1174,596],{"emptyLinePlaceholder":595},[133,1176,1178],{"class":135,"line":1177},103,[133,1179,1180],{"class":164},"          if [[ \"$FILE\" == \"aliyun\u002Fgateway\u002F*\" ]]; then\n",[133,1182,1184],{"class":135,"line":1183},104,[133,1185,1186],{"class":164},"            DEST_DIR=\"\u002Froot\u002Fconfig-files\u002Fgateway\u002F$(dirname ${FILE#aliyun\u002Fgateway\u002F})\"\n",[133,1188,1190],{"class":135,"line":1189},105,[133,1191,1192],{"class":164},"            ssh root@gateway.aliyun \"mkdir -p $DEST_DIR\"\n",[133,1194,1196],{"class":135,"line":1195},106,[133,1197,1198],{"class":164},"            echo \"复制文件 $FILE 到 gateway.aliyun: $DEST_DIR\"\n",[133,1200,1202],{"class":135,"line":1201},107,[133,1203,1204],{"class":164},"            scp -O $FILE root@gateway.aliyun:$DEST_DIR\n",[133,1206,1208],{"class":135,"line":1207},108,[133,1209,780],{"class":164},[133,1211,1213],{"class":135,"line":1212},109,[133,1214,596],{"emptyLinePlaceholder":595},[133,1216,1218],{"class":135,"line":1217},110,[133,1219,1220],{"class":164},"          if [[ \"$FILE\" == \"aliyun\u002Fhk\u002Fnginx\u002F*\" ]]; then\n",[133,1222,1224],{"class":135,"line":1223},111,[133,1225,1226],{"class":164},"            DEST_DIR=\"\u002Fetc\u002Fnginx\u002F$(dirname ${FILE#aliyun\u002Fhk\u002Fnginx\u002F})\"\n",[133,1228,1230],{"class":135,"line":1229},112,[133,1231,1232],{"class":164},"            ssh root@hk.aliyun \"mkdir -p $DEST_DIR\"\n",[133,1234,1236],{"class":135,"line":1235},113,[133,1237,1238],{"class":164},"            echo \"复制文件 $FILE 到 hk.aliyun: $DEST_DIR\"\n",[133,1240,1242],{"class":135,"line":1241},114,[133,1243,1244],{"class":164},"            scp -O $FILE root@hk.aliyun:$DEST_DIR\n",[133,1246,1248],{"class":135,"line":1247},115,[133,1249,780],{"class":164},[133,1251,1253],{"class":135,"line":1252},116,[133,1254,889],{"class":164},[133,1256,1258],{"class":135,"line":1257},117,[133,1259,1037],{"class":164},[133,1261,1263],{"class":135,"line":1262},118,[133,1264,596],{"emptyLinePlaceholder":595},[133,1266,1268],{"class":135,"line":1267},119,[133,1269,1048],{"class":164},[133,1271,1273],{"class":135,"line":1272},120,[133,1274,1060],{"class":164},[133,1276,1278],{"class":135,"line":1277},121,[133,1279,1280],{"class":164},"          # 如果文件是 compose.yaml，执行 docker compose up -d\n",[133,1282,1284],{"class":135,"line":1283},122,[133,1285,1286],{"class":164},"          if [[ \"$FILE\" == \"*\u002Fcompose.yaml\" ]]; then\n",[133,1288,1290],{"class":135,"line":1289},123,[133,1291,1292],{"class":164},"            if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002Frenovate\u002Fcompose.yaml\" ]]; then\n",[133,1294,1296],{"class":135,"line":1295},124,[133,1297,1298],{"class":164},"              echo \"跳过部署 vm-rocky-01:renovate\"\n",[133,1300,1302],{"class":135,"line":1301},125,[133,1303,1304],{"class":164},"              continue\n",[133,1306,1308],{"class":135,"line":1307},126,[133,1309,1310],{"class":164},"            fi\n",[133,1312,1314],{"class":135,"line":1313},127,[133,1315,596],{"emptyLinePlaceholder":595},[133,1317,1319],{"class":135,"line":1318},128,[133,1320,1321],{"class":164},"            if [[ \"$FILE\" == \"homelab\u002Fsynology\u002F*\" ]]; then\n",[133,1323,1325],{"class":135,"line":1324},129,[133,1326,1327],{"class":164},"              DEST_DIR=\"\u002Fvolume3\u002Fdocker\u002F$(dirname ${FILE#homelab\u002F})\"\n",[133,1329,1331],{"class":135,"line":1330},130,[133,1332,1333],{"class":164},"              echo \"部署容器 synology:$(dirname ${FILE#homelab\u002Fsynology\u002F})\"\n",[133,1335,1337],{"class":135,"line":1336},131,[133,1338,1339],{"class":164},"              ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && cd $DEST_DIR && docker-compose up -d\"\n",[133,1341,1343],{"class":135,"line":1342},132,[133,1344,1310],{"class":164},[133,1346,1348],{"class":135,"line":1347},133,[133,1349,596],{"emptyLinePlaceholder":595},[133,1351,1353],{"class":135,"line":1352},134,[133,1354,1355],{"class":164},"            if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002F*\" ]]; then\n",[133,1357,1359],{"class":135,"line":1358},135,[133,1360,1361],{"class":164},"              DEST_DIR=\"\u002Froot\u002F$(dirname ${FILE#homelab\u002Fvm-rocky-01\u002F})\"\n",[133,1363,1365],{"class":135,"line":1364},136,[133,1366,1367],{"class":164},"              echo \"部署容器 vm-rocky-01:$(dirname ${FILE#homelab\u002Fvm-rocky-01\u002F})\"\n",[133,1369,1371],{"class":135,"line":1370},137,[133,1372,1373],{"class":164},"              ssh root@vm-rocky-01.home \"cd $DEST_DIR && docker compose up -d\"\n",[133,1375,1377],{"class":135,"line":1376},138,[133,1378,1310],{"class":164},[133,1380,1382],{"class":135,"line":1381},139,[133,1383,596],{"emptyLinePlaceholder":595},[133,1385,1387],{"class":135,"line":1386},140,[133,1388,1389],{"class":164},"            if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002F*\" ]]; then\n",[133,1391,1393],{"class":135,"line":1392},141,[133,1394,1395],{"class":164},"              DEST_DIR=\"\u002Froot\u002F$(dirname ${FILE#homelab\u002Fvm-rocky-02\u002F})\"\n",[133,1397,1399],{"class":135,"line":1398},142,[133,1400,1401],{"class":164},"              echo \"部署容器 vm-rocky-02:$(dirname ${FILE#homelab\u002Fvm-rocky-02\u002F})\"\n",[133,1403,1405],{"class":135,"line":1404},143,[133,1406,1407],{"class":164},"              ssh root@vm-rocky-02.home \"cd $DEST_DIR && docker compose up -d\"\n",[133,1409,1411],{"class":135,"line":1410},144,[133,1412,1310],{"class":164},[133,1414,1416],{"class":135,"line":1415},145,[133,1417,596],{"emptyLinePlaceholder":595},[133,1419,1421],{"class":135,"line":1420},146,[133,1422,1423],{"class":164},"            # if [[ \"$FILE\" == \"aliyun\u002Fgateway\u002F*\" ]]; then\n",[133,1425,1427],{"class":135,"line":1426},147,[133,1428,1429],{"class":164},"            #   DEST_DIR=\"\u002Froot\u002Fconfig-files\u002Fgateway\u002F$(dirname ${FILE#aliyun\u002Fgateway\u002F})\"\n",[133,1431,1433],{"class":135,"line":1432},148,[133,1434,1435],{"class":164},"            #   echo \"部署容器 gateway.aliyun:$(dirname ${FILE#aliyun\u002Fgateway\u002F})\"\n",[133,1437,1439],{"class":135,"line":1438},149,[133,1440,1441],{"class":164},"            #   ssh root@gateway.aliyun \"cd $DEST_DIR && docker compose up -d\"\n",[133,1443,1445],{"class":135,"line":1444},150,[133,1446,1447],{"class":164},"            # fi\n",[133,1449,1451],{"class":135,"line":1450},151,[133,1452,596],{"emptyLinePlaceholder":595},[133,1454,1456],{"class":135,"line":1455},152,[133,1457,1458],{"class":164},"            echo \"容器重新部署完成\"\n",[133,1460,1462],{"class":135,"line":1461},153,[133,1463,1464],{"class":164},"            continue\n",[133,1466,1468],{"class":135,"line":1467},154,[133,1469,780],{"class":164},[133,1471,1473],{"class":135,"line":1472},155,[133,1474,596],{"emptyLinePlaceholder":595},[133,1476,1478],{"class":135,"line":1477},156,[133,1479,1480],{"class":164},"          # 如果文件是 homelab\u002Fsynology\u002Fnginx\u002F*, 重新启动 nginx\n",[133,1482,1484],{"class":135,"line":1483},157,[133,1485,1486],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fsynology\u002Fnginx\u002F*\" ]]; then\n",[133,1488,1490],{"class":135,"line":1489},158,[133,1491,1492],{"class":164},"            echo \"重新启动 synology:nginx 服务\"\n",[133,1494,1496],{"class":135,"line":1495},159,[133,1497,1498],{"class":164},"            ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && docker exec nginx nginx -s reload\"\n",[133,1500,1502],{"class":135,"line":1501},160,[133,1503,780],{"class":164},[133,1505,1507],{"class":135,"line":1506},161,[133,1508,596],{"emptyLinePlaceholder":595},[133,1510,1512],{"class":135,"line":1511},162,[133,1513,1514],{"class":164},"          # 如果文件是 aliyun\u002Fgateway\u002Fnginx\u002F*, 重新启动 nginx\n",[133,1516,1518],{"class":135,"line":1517},163,[133,1519,1520],{"class":164},"          if [[ \"$FILE\" == \"aliyun\u002Fgateway\u002Fnginx\u002F*\" ]]; then\n",[133,1522,1524],{"class":135,"line":1523},164,[133,1525,1526],{"class":164},"            echo \"重新启动 gateway.aliyun:nginx\"\n",[133,1528,1530],{"class":135,"line":1529},165,[133,1531,1532],{"class":164},"            ssh root@gateway.aliyun \"nginx -s reload\"\n",[133,1534,1536],{"class":135,"line":1535},166,[133,1537,780],{"class":164},[133,1539,1541],{"class":135,"line":1540},167,[133,1542,596],{"emptyLinePlaceholder":595},[133,1544,1546],{"class":135,"line":1545},168,[133,1547,1548],{"class":164},"          # 如果文件是 aliyun\u002Fhk\u002Fnginx\u002F*, 重新启动 nginx\n",[133,1550,1552],{"class":135,"line":1551},169,[133,1553,1220],{"class":164},[133,1555,1557],{"class":135,"line":1556},170,[133,1558,1559],{"class":164},"            echo \"重新启动 hk.aliyun:nginx\"\n",[133,1561,1563],{"class":135,"line":1562},171,[133,1564,1565],{"class":164},"            ssh root@hk.aliyun \"nginx -s reload\"\n",[133,1567,1569],{"class":135,"line":1568},172,[133,1570,780],{"class":164},[133,1572,1574],{"class":135,"line":1573},173,[133,1575,596],{"emptyLinePlaceholder":595},[133,1577,1579],{"class":135,"line":1578},174,[133,1580,1581],{"class":164},"          # 如果文件是 rinetd.conf, 重新启动 rinetd\n",[133,1583,1585],{"class":135,"line":1584},175,[133,1586,1587],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fsynology\u002Frinetd\u002Fconfig\u002Frinetd.conf\" ]]; then\n",[133,1589,1591],{"class":135,"line":1590},176,[133,1592,1593],{"class":164},"            echo \"重新启动 synology:rinetd 服务\"\n",[133,1595,1597],{"class":135,"line":1596},177,[133,1598,1599],{"class":164},"            ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && cd \u002Fvolume3\u002Fdocker\u002Fsynology\u002Frinetd && docker-compose restart\"\n",[133,1601,1603],{"class":135,"line":1602},178,[133,1604,780],{"class":164},[133,1606,1608],{"class":135,"line":1607},179,[133,1609,596],{"emptyLinePlaceholder":595},[133,1611,1613],{"class":135,"line":1612},180,[133,1614,1615],{"class":164},"          # 如果文件是 gitlab.rb, 重新配置 gitlab\n",[133,1617,1619],{"class":135,"line":1618},181,[133,1620,1621],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002Fgitlab\u002Fconfig\u002Fgitlab.rb\" ]]; then\n",[133,1623,1625],{"class":135,"line":1624},182,[133,1626,1627],{"class":164},"            echo \"重新配置 vm-rocky-01:gitlab\"\n",[133,1629,1631],{"class":135,"line":1630},183,[133,1632,1633],{"class":164},"            ssh root@vm-rocky-01.home \"docker exec gitlab gitlab-ctl reconfigure\"\n",[133,1635,1637],{"class":135,"line":1636},184,[133,1638,780],{"class":164},[133,1640,1642],{"class":135,"line":1641},185,[133,1643,596],{"emptyLinePlaceholder":595},[133,1645,1647],{"class":135,"line":1646},186,[133,1648,1649],{"class":164},"          # 如果文件是 prometheus.yml, 重新启动 prometheus\n",[133,1651,1653],{"class":135,"line":1652},187,[133,1654,1655],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002Fprometheus\u002Fconfig\u002Fprometheus.yml\" ]]; then\n",[133,1657,1659],{"class":135,"line":1658},188,[133,1660,1661],{"class":164},"            echo \"重新启动 vm-rocky-01:prometheus\"\n",[133,1663,1665],{"class":135,"line":1664},189,[133,1666,1667],{"class":164},"            ssh root@vm-rocky-01.home \"cd \u002Froot\u002Fprometheus && docker compose restart\"\n",[133,1669,1671],{"class":135,"line":1670},190,[133,1672,780],{"class":164},[133,1674,1676],{"class":135,"line":1675},191,[133,1677,596],{"emptyLinePlaceholder":595},[133,1679,1681],{"class":135,"line":1680},192,[133,1682,1683],{"class":164},"          # 如果文件是 telegraf.conf, 重新启动 telegraf\n",[133,1685,1687],{"class":135,"line":1686},193,[133,1688,1689],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fsynology\u002Ftelegraf\u002Fconf\u002Ftelegraf.conf\" ]]; then\n",[133,1691,1693],{"class":135,"line":1692},194,[133,1694,1695],{"class":164},"            echo \"重新启动 synology:telegraf\"\n",[133,1697,1699],{"class":135,"line":1698},195,[133,1700,1701],{"class":164},"            ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && cd \u002Fvolume3\u002Fdocker\u002Fsynology\u002Ftelegraf && docker-compose restart\"\n",[133,1703,1705],{"class":135,"line":1704},196,[133,1706,780],{"class":164},[133,1708,1710],{"class":135,"line":1709},197,[133,1711,596],{"emptyLinePlaceholder":595},[133,1713,1715],{"class":135,"line":1714},198,[133,1716,1717],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-01\u002Ftelegraf\u002Fconf\u002Ftelegraf.conf\" ]]; then\n",[133,1719,1721],{"class":135,"line":1720},199,[133,1722,1723],{"class":164},"            echo \"重新启动 vm-rocky-01:telegraf\"\n",[133,1725,1727],{"class":135,"line":1726},200,[133,1728,1729],{"class":164},"            ssh root@vm-rocky-01.home \"cd \u002Froot\u002Ftelegraf && docker compose restart\"\n",[133,1731,1733],{"class":135,"line":1732},201,[133,1734,780],{"class":164},[133,1736,1738],{"class":135,"line":1737},202,[133,1739,596],{"emptyLinePlaceholder":595},[133,1741,1743],{"class":135,"line":1742},203,[133,1744,1745],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002Ftelegraf\u002Fconf\u002Ftelegraf.conf\" ]]; then\n",[133,1747,1749],{"class":135,"line":1748},204,[133,1750,1751],{"class":164},"            echo \"重新启动 vm-rocky-02:telegraf\"\n",[133,1753,1755],{"class":135,"line":1754},205,[133,1756,1757],{"class":164},"            ssh root@vm-rocky-02.home \"cd \u002Froot\u002Ftelegraf && docker compose restart\"\n",[133,1759,1761],{"class":135,"line":1760},206,[133,1762,780],{"class":164},[133,1764,1766],{"class":135,"line":1765},207,[133,1767,596],{"emptyLinePlaceholder":595},[133,1769,1771],{"class":135,"line":1770},208,[133,1772,1773],{"class":164},"          # 如果文件是 bots\u002Fconfig\u002Fclash\u002F*.yaml, 更新 clash 配置\n",[133,1775,1777],{"class":135,"line":1776},209,[133,1778,1779],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002Fbots\u002Fconfig\u002Fclash\u002F*.yaml\" ]]; then\n",[133,1781,1783],{"class":135,"line":1782},210,[133,1784,1785],{"class":164},"            echo \"重新启动 vm-rocky-02:bots\"\n",[133,1787,1789],{"class":135,"line":1788},211,[133,1790,1791],{"class":164},"            ssh root@vm-rocky-02.home \"cd \u002Froot\u002Fbots && docker compose restart\"\n",[133,1793,1795],{"class":135,"line":1794},212,[133,1796,1797],{"class":164},"            echo \"等待 10 秒\"\n",[133,1799,1801],{"class":135,"line":1800},213,[133,1802,1803],{"class":164},"            sleep 10\n",[133,1805,1807],{"class":135,"line":1806},214,[133,1808,1809],{"class":164},"            echo \"重新启动 synology:clash-premium\"\n",[133,1811,1813],{"class":135,"line":1812},215,[133,1814,1815],{"class":164},"            ssh -p 5110 root@synology.home \"export PATH=\\$PATH:\u002Fusr\u002Flocal\u002Fbin && cd \u002Fvolume3\u002Fdocker\u002Fsynology\u002Fclash-premium && docker-compose restart\"\n",[133,1817,1819],{"class":135,"line":1818},216,[133,1820,780],{"class":164},[133,1822,1824],{"class":135,"line":1823},217,[133,1825,596],{"emptyLinePlaceholder":595},[133,1827,1829],{"class":135,"line":1828},218,[133,1830,1831],{"class":164},"          # 如果文件是 artalk\u002Fdata\u002Fartalk.yml, 重新启动 artalk\n",[133,1833,1835],{"class":135,"line":1834},219,[133,1836,1837],{"class":164},"          if [[ \"$FILE\" == \"homelab\u002Fvm-rocky-02\u002Fartalk\u002Fdata\u002Fartalk.yml\" ]]; then\n",[133,1839,1841],{"class":135,"line":1840},220,[133,1842,1843],{"class":164},"            echo \"重新启动 vm-rocky-02:artalk\"\n",[133,1845,1847],{"class":135,"line":1846},221,[133,1848,1849],{"class":164},"            ssh root@vm-rocky-02.home \"cd \u002Froot\u002Fartalk && docker compose restart\"\n",[133,1851,1853],{"class":135,"line":1852},222,[133,1854,780],{"class":164},[133,1856,1858],{"class":135,"line":1857},223,[133,1859,596],{"emptyLinePlaceholder":595},[133,1861,1863],{"class":135,"line":1862},224,[133,1864,889],{"class":164},[133,1866,1868],{"class":135,"line":1867},225,[133,1869,1037],{"class":164},[133,1871,1873,1875],{"class":135,"line":1872},226,[133,1874,624],{"class":139},[133,1876,1877],{"class":164}," echo \"部署完成\"\n",[133,1879,1881,1884],{"class":135,"line":1880},227,[133,1882,1883],{"class":571},"  only",[133,1885,575],{"class":139},[133,1887,1889,1891],{"class":135,"line":1888},228,[133,1890,624],{"class":139},[133,1892,641],{"class":164},[27,1894,1895,1896,103,1899,1902],{},"其中，",[74,1897,1898],{},"SSH_KNOWN_HOSTS",[74,1900,1901],{},"SSH_PRIVATE_KEY"," 存储在 GitLab CI\u002FCD 的变量中，用于 SSH 登录到对应的宿主机上。",[27,1904,1905,1906,1909,1910,1913,1914,1916,1917,1920,1921,1923],{},"当我需要改某个容器的配置文件时，例如当我需要修改 ",[74,1907,1908],{},"synology"," 上的 ",[74,1911,1912],{},"nginx"," 配置，我只需要去 ",[74,1915,76],{}," 项目中修改 ",[74,1918,1919],{},"homelab\u002Fsynology\u002Fnginx\u002Fnginx.conf"," 文件，然后 commit，push，等待几秒钟，GitLab CI 就会自动帮我完成配置文件的更新以及 ",[74,1922,1912],{}," 的重启。",[27,1925,1926],{},"每周一，renovate 会自动发起并合并更新容器镜像版本的 MR，然后 GitLab CI 会自动帮我更新容器。",[27,1928,1929],{},"对了，GitLab 、GitLab Runner、Renovate 也都是通过容器私有化部署在 HomeLab 中的，这些就不赘述。",[1931,1932,1933],"style",{},"html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sseR_, html code.shiki .sseR_{--shiki-light:#9C3EDA;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sQzsp, html code.shiki .sQzsp{--shiki-light:#E53935;--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}",{"title":93,"searchDepth":143,"depth":143,"links":1935},[1936,1937],{"id":36,"depth":143,"text":36},{"id":61,"depth":143,"text":61},null,"2024-12-12",false,"md",{},"\u002Fposts\u002F2024\u002Fmanaging-containers-in-my-homelab",{"text":1945,"minutes":1946,"time":1947,"words":1948},"11 min read",10.24,614400,2048,{"title":22,"description":29},{"loc":1943},"posts\u002F2024\u002F20241212.managing-containers-in-my-homelab",[1953,1954,1955,1956],"技术","DevOps","HomeLab","Docker","sSWijwbQSH5iLEufQ0ZZuj2tXgOjghNQbswIr9u7b4A",[1959,1965],{"title":1960,"path":1961,"stem":1962,"date":1963,"description":1964,"children":-1},"2024 年度总结","\u002Fposts\u002F2025\u002Fend-of-2024","posts\u002F2025\u002F20250102.end-of-2024","2025-01-02","又是一年过去，但 2024 年我可以比较自豪地说，我达成了很多了不起的成就。",{"title":1966,"path":1967,"stem":1968,"date":1969,"description":1970,"children":-1},"汽车天窗漏水的经历","\u002Fposts\u002F2024\u002Fcar-leak-experience","posts\u002F2024\u002F20241104.car-leak-experience","2024-11-04","昨天早上要起早送儿子去足球比赛，我先下去把车子开出来，车子刚动的时候我发现不对劲，听到水声。我往副驾座位下一看，完蛋，看到地毯湿了。",1777580271284]