when: - event: tag tag: v* clone: git: image: woodpeckerci/plugin-git settings: remote: http://10.10.10.128:3000/becomer/cpdaily_xiaozhushou.git steps: build-image: name: 构建 Docker 镜像 image: docker commands: | docker build -t cpdaily-xiaozhushou:latest . docker tag cpdaily-xiaozhushou:latest cpdaily-xiaozhushou:${CI_COMMIT_TAG:-${CI_COMMIT_SHA:0:8}} volumes: - /var/run/docker.sock:/var/run/docker.sock deploy: name: 蓝绿部署 image: docker commands: | set -e IMAGE_TAG="${CI_COMMIT_TAG:-latest}" # 应用配置由宿主机 /opt/cpdaily-xiaozhushou/config.toml 运维维护 # (已移出版本库,避免密钥入库;容器以只读挂载方式读取) # 两个固定槽位(蓝/绿),任一时刻都保持"两个容器都活着": # 活跃槽 = 反代当前指向的端口;备用槽 = 另一个端口(上一版,供秒级回滚) SLOT_BLUE=8884 SLOT_GREEN=8885 # ============================================================ # 第一步:读取当前反代指向的端口,确定活跃/备用槽 # 反代入口为 1Panel 站点 jinrixiaoyuan.top(frps 公网链路): # /www/sites/jinrixiaoyuan.top/proxy/root.conf → proxy_pass http://127.0.0.1:8884|8885 # 该文件经 conf.d/jinrixiaoyuan.top.conf 的 include /www/sites/.../proxy/*.conf 载入。 # ============================================================ CURRENT_PORT=$(docker exec 1Panel-openresty-ESmn \ grep -oE 'proxy_pass http://[0-9.]+:[0-9]+' \ /www/sites/jinrixiaoyuan.top/proxy/root.conf | grep -o '[0-9]\+$') echo "=== 当前反代端口: $CURRENT_PORT ===" if [ "$CURRENT_PORT" = "$SLOT_BLUE" ]; then PORT_ACTIVE=$SLOT_BLUE PORT_STANDBY=$SLOT_GREEN elif [ "$CURRENT_PORT" = "$SLOT_GREEN" ]; then PORT_ACTIVE=$SLOT_GREEN PORT_STANDBY=$SLOT_BLUE else echo "ERROR: 未知反代端口 $CURRENT_PORT,中止部署(期望 $SLOT_BLUE/$SLOT_GREEN)" exit 1 fi echo " 活跃槽: $PORT_ACTIVE | 备用槽: $PORT_STANDBY" echo " 目标: 更新备用槽 :$PORT_STANDBY 到 $IMAGE_TAG 后,切换反代指向它(活跃槽保留,可回滚)" # ============================================================ # 第一步附:一次性「收养」旧命名容器 # 迁移前的容器名为 cpdaily-xiaozhushou(无端口后缀),本脚本改用 # cpdaily-xiaozhushou-<端口>。若不改名,下次部署 rm -f 找不到它 → 端口 # 被占用导致 docker run 冲突、部署失败。此处按它实际映射的端口改名(幂等)。 # 同时清理历史遗留的 cpdaily-blue 陈尸容器。 # ============================================================ LEGACY_NAME=cpdaily-xiaozhushou if docker inspect "$LEGACY_NAME" >/dev/null 2>&1; then LEGACY_PORT=$(docker port "$LEGACY_NAME" 8093 2>/dev/null | head -1 | grep -oE '[0-9]+$' || true) if [ -n "$LEGACY_PORT" ]; then if docker inspect "cpdaily-xiaozhushou-$LEGACY_PORT" >/dev/null 2>&1; then echo " 发现旧命名容器,但 cpdaily-xiaozhushou-$LEGACY_PORT 已存在,移除旧容器" docker rm -f "$LEGACY_NAME" || true else echo " 收养旧命名容器: $LEGACY_NAME → cpdaily-xiaozhushou-$LEGACY_PORT" docker rename "$LEGACY_NAME" "cpdaily-xiaozhushou-$LEGACY_PORT" fi else echo " 旧命名容器未映射 8093 端口,跳过收养" fi fi docker rm -f cpdaily-blue 2>/dev/null || true # ============================================================ # 第二步:更新「备用槽」容器(此槽当前无流量,停它不会影响线上) # ============================================================ echo "=== 停止并移除备用槽旧容器(:$PORT_STANDBY)===" docker rm -f cpdaily-xiaozhushou-$PORT_STANDBY 2>/dev/null || true echo "=== 在备用槽启动新容器 :$PORT_STANDBY (image: $IMAGE_TAG) ===" docker run -d --name cpdaily-xiaozhushou-$PORT_STANDBY \ --restart unless-stopped \ --network likedream-net \ -e TZ=Asia/Shanghai \ -p $PORT_STANDBY:8093 \ -v /opt/cpdaily-xiaozhushou/config.toml:/app/config.toml:ro \ cpdaily-xiaozhushou:$IMAGE_TAG # ============================================================ # 第三步:健康检查(新容器必须就绪,否则直接失败且不动反代) # 用 docker exec 在容器内自测,避免依赖宿主机到容器的网络路径。 # ============================================================ HEALTHY=false for i in $(seq 1 30); do if docker exec cpdaily-xiaozhushou-$PORT_STANDBY \ wget -q --timeout=3 -O /dev/null http://127.0.0.1:8093/health 2>/dev/null; then HEALTHY=true break fi echo " 等待新容器就绪... ($i/30)" sleep 2 done if [ "$HEALTHY" = false ]; then echo "ERROR: 新容器未通过健康检查,中止部署(线上仍由活跃槽 :$PORT_ACTIVE 提供服务,未受影响)" docker rm -f cpdaily-xiaozhushou-$PORT_STANDBY 2>/dev/null || true exit 1 fi echo " 新容器已就绪 ✓" # ============================================================ # 第四步:切换 1Panel 反代到备用槽(唯一一次代理切换) # 切换前先备份配置,nginx -t 通过后再 reload;reload 后等待旧 worker 退出, # 确保旧连接不再按旧配置转发。 # ============================================================ echo "=== 切换 1Panel 反代 :$PORT_ACTIVE → :$PORT_STANDBY ===" PROXY_CONF=/www/sites/jinrixiaoyuan.top/proxy/root.conf docker exec 1Panel-openresty-ESmn \ cp $PROXY_CONF $PROXY_CONF.bak docker exec 1Panel-openresty-ESmn \ sed -i "s|:$PORT_ACTIVE|:$PORT_STANDBY|g" $PROXY_CONF if ! docker exec 1Panel-openresty-ESmn nginx -t 2>&1; then echo "ERROR: nginx 配置校验失败,回滚反代配置" docker exec 1Panel-openresty-ESmn cp $PROXY_CONF.bak $PROXY_CONF docker rm -f cpdaily-xiaozhushou-$PORT_STANDBY 2>/dev/null || true exit 1 fi # 记录 reload 前的 worker pid,reload 后等其全部退出,避免旧 worker 按旧配置继续转发 OLD_WORKERS=$(docker exec 1Panel-openresty-ESmn \ sh -c 'ps -eo pid,cmd | grep "[n]ginx: worker" | awk "{print \$1}"' | tr '\n' ' ') docker exec 1Panel-openresty-ESmn nginx -s reload echo " Nginx 热加载完成,等待旧 worker 退出..." for i in $(seq 1 15); do STILL=0 for p in $OLD_WORKERS; do if docker exec 1Panel-openresty-ESmn sh -c "[ -d /proc/$p ]" 2>/dev/null; then STILL=$((STILL + 1)) fi done if [ "$STILL" = "0" ]; then echo " 旧 worker 已全部退出 ✓" break fi echo " 等待旧 worker 释放连接... ($i/15, 剩余 $STILL)" sleep 2 done echo " 反代已指向 :$PORT_STANDBY ✓" # ============================================================ # 第五步:验证线上确实由新槽服务 # ============================================================ if ! docker exec 1Panel-openresty-ESmn \ sh -c "wget -q --timeout=5 -O /dev/null http://127.0.0.1:$PORT_STANDBY/health" 2>/dev/null; then echo "WARN: 新槽健康检查异常,但反代已切换,请人工核查" else echo " 线上验证通过(:$PORT_STANDBY 正常服务)✓" fi # ============================================================ # 第六步:保留活跃槽用于回滚说明;清理多余历史镜像 # 注:旧槽容器不删除 —— 它即上一版本,仍需回滚时可直接把反代切回。 # 下次部署时旧槽会被当作「备用槽」重建,因此不会无限堆积。 # ============================================================ echo "=== 当前容器 ===" docker ps --filter "name=cpdaily-xiaozhushou-" --format '{{.Names}} → {{.Ports}} ({{.Status}})' echo "=== 清理历史镜像(保留 latest 与最近 2 个版本)===" docker images cpdaily-xiaozhushou --format '{{.Tag}}' \ | grep -v '^latest$' | head -n -2 | while read tag; do docker rmi cpdaily-xiaozhushou:$tag 2>/dev/null || true done echo "=== 部署完成 ✓ 活跃槽 :$PORT_STANDBY(旧槽 :$PORT_ACTIVE 保留,可回滚)===" volumes: - /var/run/docker.sock:/var/run/docker.sock