一次 Drone CI/CD 落地实战复盘:从“理想方案”到“真正能上线”

一次 Drone CI/CD 落地实战复盘:从"理想方案"到"真正能上线"

前言

这篇文章记录的是一套真实项目的 CI/CD 落地过程,但我会把所有敏感信息都做脱敏处理,包括:

  • 仓库地址
  • 域名
  • 服务器 IP
  • 用户名
  • 私钥
  • 业务账号
  • 第三方平台配置

文章重点不是讲 Drone 的概念,而是分享一套在公司内网环境里,如何把 Git + Drone + Docker Compose + SSH 真正跑通,并且能稳定上线的过程。

项目背景

先说一下这类项目的典型环境:

  • 代码托管:自建 Git 服务
  • CI/CD:Drone
  • 部署目标:Ubuntu Linux 服务器
  • 应用形态:web + api + postgres + redis + nginx
  • 进程编排:Docker Compose
  • 对外入口:只开放一个 Nginx 端口

这次项目的目标很明确:

  1. 提交代码后,不走每次 push 自动上线
  2. 通过打 tag 的方式手动发布
  3. 生产服务器只对外开放一个端口
  4. 发布时尽量少手工操作
  5. 出问题时要容易排查

最开始想走的方案

一开始最自然的想法是这条路:

  1. Drone 拉代码
  2. Drone 构建业务镜像
  3. Drone 推送镜像到私有 Registry
  4. 生产服务器执行 docker compose pull
  5. 生产服务器重启容器

这条链路在很多团队里都成立,而且理论上也更"标准"。

但在真实环境里,很快遇到了几个问题:

  • Drone 当前仓库没有开启 Trusted
  • 不方便让 Drone 直接挂宿主机 Docker Socket
  • 私有 Registry 配置和网络链路会增加额外排障成本
  • 项目里还有浏览器登录态这类本地持久化数据,不只是纯后端服务

结果就是:理论方案很漂亮,但在当前权限和环境约束下,推进成本偏高。

最终落地的方案

最后选了一条更务实的链路:

  1. Drone 被 tag 触发
  2. Drone 通过 SSH 登录生产服务器
  3. 生产服务器本地保留一份源码仓库
  4. Drone 触发服务器执行 git fetch --all --tags --prune
  5. 服务器切到当前发布对应的 commit
  6. 服务器执行 docker compose up -d --build --remove-orphans
  7. 部署脚本执行健康检查

也就是说,这套方案的核心思想不是"CI 负责构建一切",而是:

Drone 负责触发,生产机负责部署。

这个方案的好处很直接:

  • 不依赖 Drone 的 Trusted
  • 不需要让 CI 平台直接控制宿主机 Docker
  • 生产机上保留源码,排查问题更方便
  • Compose、Nginx、部署脚本可以跟着仓库版本一起更新

生产机目录怎么设计

建议把部署目录固定成一个稳定路径,例如:

bash 复制代码
/opt/docker/app-name

然后目录结构尽量清晰:

text 复制代码
/opt/docker/app-name/
├── .env.production
├── docker-compose.server.yml
├── deploy/
│   └── nginx.conf
├── repo/
│   └── ...源码仓库...
└── data/
    ├── postgres/
    ├── redis/
    └── playwright/

这里最关键的是两点:

  • repo/ 用来放服务器上的源码工作副本
  • data/ 用来放不能随部署丢失的持久化数据

如果你的项目依赖浏览器登录态、缓存文件、数据库目录,这种结构会非常省心。

Drone 流水线怎么做

最终保留的流水线非常克制,只做一件事:远程触发部署。

一个脱敏后的思路示例:

yaml 复制代码
kind: pipeline
type: docker
name: build-and-deploy

trigger:
  event:
    - tag
    - cron

steps:
  - name: deploy-via-ssh
    image: appleboy/drone-ssh
    environment:
      REPO_GIT_USERNAME:
        from_secret: repo_git_username
      REPO_GIT_PASSWORD:
        from_secret: repo_git_password
    settings:
      host:
        from_secret: deploy_host
      username:
        from_secret: deploy_user
      key:
        from_secret: deploy_ssh_key
      port: 22
      script:
        - |
          set -e
          DEPLOY_PATH="/opt/docker/app-name"
          REPO_URL="https://git.example.com/org/app-name.git"
          AUTH_HEADER="$(printf '%s:%s' "$REPO_GIT_USERNAME" "$REPO_GIT_PASSWORD" | base64 | tr -d '\n')"

          mkdir -p "$DEPLOY_PATH"
          if [ ! -d "$DEPLOY_PATH/repo/.git" ]; then
            git -c http.extraHeader="Authorization: Basic $AUTH_HEADER" clone "$REPO_URL" "$DEPLOY_PATH/repo"
          fi

          cd "$DEPLOY_PATH/repo"
          git remote set-url origin "$REPO_URL"
          git -c http.extraHeader="Authorization: Basic $AUTH_HEADER" fetch --all --tags --prune
          git checkout -f "$DRONE_COMMIT"

          bash scripts/deploy-remote.sh "${DRONE_TAG:-$DRONE_COMMIT}"

这里有两个重点:

  • 私有仓库拉取走 HTTPS Basic Auth
  • 部署逻辑不要全塞进 Drone 配置里,而是下沉到远程脚本

后者非常重要。因为一旦部署逻辑全写在 .drone.yml 里,后续排查会越来越痛苦。

远程部署脚本应该做什么

我建议把真正的部署动作都放在服务器上的一个脚本里,例如:

  • 校验目录和文件是否存在
  • 创建持久化目录
  • 同步最新的 Compose 和 Nginx 配置
  • 执行 docker compose up -d --build
  • 做健康检查
  • 清理悬空镜像
  • 增加部署锁,避免并发发布

一个好的部署脚本,至少应该覆盖这几个问题:

  1. 两次发布同时开始怎么办
  2. 服务器上没有环境变量文件怎么办
  3. 构建成功但服务没起来怎么办
  4. Nginx 正常但 API 已经挂了怎么办

不要把"部署成功"的判断只停留在 Docker 命令返回 0。真正有意义的是:

  • 首页能不能打开
  • /api/health 是否正常
  • 关键服务是否真的活着

为什么我最后没有坚持"Registry 推镜像"这条路

这不是说 Registry 方案不好,而是它有前提条件。

如果下面这些条件都成熟:

  • Drone 有足够权限
  • Registry 稳定可用
  • 网络链路简单
  • 团队已经有统一镜像治理方式

那当然应该优先用"构建镜像 -> 推 Registry -> 服务器拉镜像"的模式。

但如果你的现状是:

  • 权限不全
  • 环境复杂
  • 项目又急着上线

那先落一套"可运行、可回滚、可排障"的方案,往往更现实。

工程上最忌讳的不是"不够标准",而是"为了标准而长期落不了地"。

这次踩过的几个典型坑

1. appleboy/drone-ssh 的脚本格式坑

一开始把命令拆成多条写在 script: 里,看起来很清楚,但实际执行时插件有解析差异,某些写法会被拼坏。

后来改成单个 block script 之后,稳定性明显更高。

经验是:

  • 少在插件里做复杂 shell 拼装
  • 复杂逻辑尽量收敛到远程脚本里

2. 私有仓库 clone 不一定能直接用 SSH

理论上 Git over SSH 最干净,但实际环境里常见问题有:

  • 22 端口不通
  • 内网策略限制
  • Drone 容器里 SSH 已配置,但 Git 服务不认

最后如果 HTTPS 可用,很多时候直接用:

  • 仓库地址走 HTTPS
  • 用户名密码或 Token 走 Secret
  • 通过 http.extraHeader 注入认证头

这条链路反而更稳。

3. Docker 基础镜像会被镜像加速器坑到

这次就踩到了一个很实际的问题:

  • Docker build 里用了 node:22-bookworm-slim
  • 服务器 Docker Daemon 配了镜像加速
  • 某次拉取 docker.io/library/node 时被镜像源返回 403

这个问题最烦的地方在于:

  • 代码没错
  • Dockerfile 语法没错
  • 但部署就是过不去

后来的处理方式是:

  • 不把基础镜像写死
  • 通过 ARG 让基础镜像可配置
  • 在不同环境里切换成更稳定的镜像来源

这类问题的本质是:部署系统依赖的不只是代码,还依赖环境侧的镜像供应链。

4. 环境变量不要到处复制

这次也暴露了一个常见问题:

  • 根目录一个 .env
  • 子项目一个 .env
  • 生产还有一个 .env.production

只要项目一复杂,就很容易出现"代码读的是 A,开发者以为读的是 B"。

更稳的做法是:

  • 本地开发尽量统一读根目录 .env
  • 生产只读 .env.production
  • 文档里明确说明每个环境变量文件的职责

否则后面出错时,排查成本会非常高。

5. 数据端口与默认配置必须统一

本地开发里,如果默认 DATABASE_URL 写的是 localhost:5433,那本地 docker-compose.yml 最好也明确映射成:

yaml 复制代码
ports:
  - "5433:5432"

不要让:

  • 文档写一个端口
  • .env 写一个端口
  • Compose 又映射另一个端口

这种小问题看起来不大,但会直接把 Prisma、迁移、API 启动全部拖死。

6. 浏览器登录态一定要持久化

如果你的项目里用到了 Playwright、Selenium,或者依赖第三方平台登录态,那么这些状态文件不能跟着代码重建一起丢掉。

更稳的方式是:

  • 单独放到 data/ 目录
  • 明确不纳入 Git
  • 部署时不覆盖

否则每次上线后都要重新扫码登录,运维体验会非常差。

生产机为什么只开放一个端口

我非常建议业务服务最终只开放一个端口给外部,例如:

  • 3003 -> nginx

其余端口全部只保留在 Docker 内部网络里:

  • web
  • api
  • postgres
  • redis

这样做的好处有三个:

  1. 外部暴露面最小
  2. Nginx 可以统一做反向代理
  3. 后续上 HTTPS 也更简单

很多团队早期部署最容易犯的错,就是把 3000300154326379 全都暴露出去。前期感觉方便,后期就是负担。

我推荐的发布规则

这次也顺手把 tag 规则统一了。

推荐格式:

text 复制代码
Major.Minor.MMDD.Build

例如:

  • 1.1.0413.1
  • 1.1.0413.2

含义是:

  • Major:主版本
  • Minor:小版本
  • MMDD:月日
  • Build:当天第几次重发

这个规则的优点是:

  • 比纯流水号更可读
  • 比临时手写 tag 更统一
  • 不依赖自动语义版本工具

对于内部工具项目,已经足够实用。

脱敏后仍然值得保留的配置原则

即使把所有敏感信息拿掉,我觉得下面这些原则仍然非常值得保留:

  • 部署目录固定,不要今天一个路径明天一个路径
  • 配置文件职责清晰,不要多个 .env 互相覆盖
  • 发布入口单一,只允许 tagcron
  • 部署完成后必须做健康检查
  • 生产只开放一个端口
  • 登录态、数据库、缓存数据必须持久化
  • 回滚路径提前准备,不要等故障时现想

一个适合中小团队的发布清单

每次发布前,我建议至少过一遍这个清单:

  1. main 分支代码已确认
  2. .env.production 没被误覆盖
  3. 生产机的 data/ 目录还在
  4. Drone Secrets 没过期
  5. 私有仓库仍可访问
  6. 基础镜像来源可正常拉取
  7. 本次 tag 已按规则命名
  8. 发布后验证首页和 /api/health

这个清单看起来朴素,但真正能减少线上事故。

结语

这次 CI/CD 落地给我最大的感受是:

真正难的从来不是"写出一份看起来高级的流水线配置",而是根据当前权限、网络、历史包袱和项目形态,选出一条真的能跑通的路径。

如果你现在也在一个类似的环境里:

  • 有 Drone
  • 有 Docker
  • 有一台 Linux 服务器
  • 但权限不完整、环境不完美

那我建议你优先追求下面三个目标:

  1. 能上线
  2. 能回滚
  3. 能排障

等这三件事稳定以后,再逐步演进到更标准的镜像仓库发布、通知、灰度和自动回滚。

这才是更稳的工程节奏。

后记

2026年4月13日于上海,在codex 5.4辅助下完成。

相关推荐
xiaotao1312 天前
第二十一章:CI/CD 最佳实践
前端·ci/cd·vite·前端打包
夜珀3 天前
AtomGit CI/CD流水线全解析
ci/cd
M-Ellen3 天前
从零搭建 Windows + WSL2 + Docker + GitLab CI/CD 完整手册
ci/cd·docker·gitlab
REDcker4 天前
Jenkins 开源 CI/CD 平台概览与版本演进
ci/cd·开源·jenkins
独断万古他化7 天前
AI 赋能自动化测试实战:从用例生成到 CI/CD 全流程落地
人工智能·ci/cd·测试
郝学胜-神的一滴9 天前
CMake赋能持续集成|自动化测试落地的进阶指南 ✨
c++·ci/cd·软件工程·软件构建
AI成长日志10 天前
【GitHub开源项目】Harness CI/CD平台深度解析:架构设计、核心功能与实战指南
ci/cd·开源·github
清水白石00810 天前
Python 项目 CI/CD 信心模型:证据驱动部署,从“勇敢上线”到“零风险发版”实战指南
驱动开发·python·ci/cd
alan072110 天前
【持续集成、持续交付】jenkins实现CI/CD
运维·ci/cd·jenkins