一次 Drone CI/CD 落地实战复盘从“理想方案”到“真正能上线”前言这篇文章记录的是一套真实项目的 CI/CD 落地过程但我会把所有敏感信息都做脱敏处理包括仓库地址域名服务器 IP用户名私钥业务账号第三方平台配置文章重点不是讲 Drone 的概念而是分享一套在公司内网环境里如何把Git Drone Docker Compose SSH真正跑通并且能稳定上线的过程。项目背景先说一下这类项目的典型环境代码托管自建 Git 服务CI/CDDrone部署目标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_URLhttps://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.extraHeaderAuthorization:Basic $AUTH_HEADER clone $REPO_URL $DEPLOY_PATH/repo fi cd $DEPLOY_PATH/repo git remote set-url origin $REPO_URLgit -c http.extraHeaderAuthorization: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辅助下完成。