一次 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 端口
这次项目的目标很明确:
- 提交代码后,不走每次
push自动上线 - 通过打
tag的方式手动发布 - 生产服务器只对外开放一个端口
- 发布时尽量少手工操作
- 出问题时要容易排查
最开始想走的方案
一开始最自然的想法是这条路:
- Drone 拉代码
- Drone 构建业务镜像
- Drone 推送镜像到私有 Registry
- 生产服务器执行
docker compose pull - 生产服务器重启容器
这条链路在很多团队里都成立,而且理论上也更“标准”。
但在真实环境里,很快遇到了几个问题:
- Drone 当前仓库没有开启
Trusted - 不方便让 Drone 直接挂宿主机 Docker Socket
- 私有 Registry 配置和网络链路会增加额外排障成本
- 项目里还有浏览器登录态这类本地持久化数据,不只是纯后端服务
结果就是:理论方案很漂亮,但在当前权限和环境约束下,推进成本偏高。
最终落地的方案
最后选了一条更务实的链路:
- Drone 被
tag触发 - Drone 通过 SSH 登录生产服务器
- 生产服务器本地保留一份源码仓库
- Drone 触发服务器执行
git fetch --all --tags --prune - 服务器切到当前发布对应的 commit
- 服务器执行
docker compose up -d --build --remove-orphans - 部署脚本执行健康检查
也就是说,这套方案的核心思想不是“CI 负责构建一切”,而是:
Drone 负责触发,生产机负责部署。
这个方案的好处很直接:
- 不依赖 Drone 的
Trusted - 不需要让 CI 平台直接控制宿主机 Docker
- 生产机上保留源码,排查问题更方便
- Compose、Nginx、部署脚本可以跟着仓库版本一起更新
生产机目录怎么设计
建议把部署目录固定成一个稳定路径,例如:
/opt/docker/app-name然后目录结构尽量清晰:
/opt/docker/app-name/ ├── .env.production ├── docker-compose.server.yml ├── deploy/ │ └── nginx.conf ├── repo/ │ └── ...源码仓库... └── data/ ├── postgres/ ├── redis/ └── playwright/这里最关键的是两点:
repo/用来放服务器上的源码工作副本data/用来放不能随部署丢失的持久化数据
如果你的项目依赖浏览器登录态、缓存文件、数据库目录,这种结构会非常省心。
Drone 流水线怎么做
最终保留的流水线非常克制,只做一件事:远程触发部署。
一个脱敏后的思路示例:
kind:pipelinetype:dockername:build-and-deploytrigger:event:-tag-cronsteps:-name:deploy-via-sshimage:appleboy/drone-sshenvironment:REPO_GIT_USERNAME:from_secret:repo_git_usernameREPO_GIT_PASSWORD:from_secret:repo_git_passwordsettings:host:from_secret:deploy_hostusername:from_secret:deploy_userkey:from_secret:deploy_ssh_keyport:22script:-|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"]; thengit -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 - 做健康检查
- 清理悬空镜像
- 增加部署锁,避免并发发布
一个好的部署脚本,至少应该覆盖这几个问题:
- 两次发布同时开始怎么办
- 服务器上没有环境变量文件怎么办
- 构建成功但服务没起来怎么办
- 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最好也明确映射成:
ports:-"5433:5432"不要让:
- 文档写一个端口
.env写一个端口- Compose 又映射另一个端口
这种小问题看起来不大,但会直接把 Prisma、迁移、API 启动全部拖死。
6. 浏览器登录态一定要持久化
如果你的项目里用到了 Playwright、Selenium,或者依赖第三方平台登录态,那么这些状态文件不能跟着代码重建一起丢掉。
更稳的方式是:
- 单独放到
data/目录 - 明确不纳入 Git
- 部署时不覆盖
否则每次上线后都要重新扫码登录,运维体验会非常差。
生产机为什么只开放一个端口
我非常建议业务服务最终只开放一个端口给外部,例如:
3003 -> nginx
其余端口全部只保留在 Docker 内部网络里:
webapipostgresredis
这样做的好处有三个:
- 外部暴露面最小
- Nginx 可以统一做反向代理
- 后续上 HTTPS 也更简单
很多团队早期部署最容易犯的错,就是把3000、3001、5432、6379全都暴露出去。前期感觉方便,后期就是负担。
我推荐的发布规则
这次也顺手把 tag 规则统一了。
推荐格式:
Major.Minor.MMDD.Build例如:
1.1.0413.11.1.0413.2
含义是:
Major:主版本Minor:小版本MMDD:月日Build:当天第几次重发
这个规则的优点是:
- 比纯流水号更可读
- 比临时手写 tag 更统一
- 不依赖自动语义版本工具
对于内部工具项目,已经足够实用。
脱敏后仍然值得保留的配置原则
即使把所有敏感信息拿掉,我觉得下面这些原则仍然非常值得保留:
- 部署目录固定,不要今天一个路径明天一个路径
- 配置文件职责清晰,不要多个
.env互相覆盖 - 发布入口单一,只允许
tag和cron - 部署完成后必须做健康检查
- 生产只开放一个端口
- 登录态、数据库、缓存数据必须持久化
- 回滚路径提前准备,不要等故障时现想
一个适合中小团队的发布清单
每次发布前,我建议至少过一遍这个清单:
main分支代码已确认.env.production没被误覆盖- 生产机的
data/目录还在 - Drone Secrets 没过期
- 私有仓库仍可访问
- 基础镜像来源可正常拉取
- 本次 tag 已按规则命名
- 发布后验证首页和
/api/health
这个清单看起来朴素,但真正能减少线上事故。
结语
这次 CI/CD 落地给我最大的感受是:
真正难的从来不是“写出一份看起来高级的流水线配置”,而是根据当前权限、网络、历史包袱和项目形态,选出一条真的能跑通的路径。
如果你现在也在一个类似的环境里:
- 有 Drone
- 有 Docker
- 有一台 Linux 服务器
- 但权限不完整、环境不完美
那我建议你优先追求下面三个目标:
- 能上线
- 能回滚
- 能排障
等这三件事稳定以后,再逐步演进到更标准的镜像仓库发布、通知、灰度和自动回滚。
这才是更稳的工程节奏。
后记
2026年4月13日于上海,在codex 5.4辅助下完成。