大概半年前,我负责的那个后端服务还在用最原始的方式发版:本地打包、scp 上传、SSH 上去重启进程,一次发版少说十几分钟,赶上网络抖动能折腾半小时。后来我把项目迁到了基于 drone+docker 的自动化部署流程上,代码一推送,镜像自动构建,远程服务器自动更新,整个流程基本不需要人碰。今天把这套方案从架构选型、环境搭建、流水线编写到排障实录完整梳理一遍,想在小团队里落地自动化发布体系的同学,可以直接照抄。
这套方案最核心的价值,是把“发布”这件事从手工操作变成了一次 Git 提交。你要做的只是在仓库根目录放一个.drone.yml,在控制台点一下激活,剩下的 build、push、deploy 全交给系统。drone 负责调度,docker 负责执行环境隔离,两者搭在一起,部署链路非常轻,但又足够真实业务使用。
1. drone+docker 选型:轻量 CI/CD 方案为什么更适合小团队
1.1 跟 Jenkins 和 GitLab CI 比,drone 的取舍到底在哪
先回答一个很多人会问的问题:为什么不用 Jenkins?
Jenkins 确实生态庞大,插件多到数不清,但它的“重”在中小团队里往往是负担。你要单独维护一个服务,装一堆插件,界面配置路径很深,一个 pipeline 写成 Groovy 脚本之后,普通后端同学根本不敢碰。而且 Jenkins 的安装、升级、插件兼容问题,会持续消耗团队的运维精力。
GitLab CI 如果你们本来就在重度使用 GitLab,那它是很自然的选择。但它的一个隐性前提是:你愿意把自己绑在 GitLab 生态里。很多人用的代码托管平台并不是 GitLab,或者团队里 GitHub、Gitea、自建 Git 平台混用,这时候 GitLab CI 就不太好统一。
drone 的做法完全不同。它的 server 只负责接收 webhook、解析.drone.yml、调度任务;执行任务的是配套的 runner。runner 拿到一个 pipeline 后,会把每一步 step 都当作一个 Docker 容器来跑。这意味着什么?意味着构建环境不再依赖某个固定的 Jenkins 节点,你不需要在一台机器上装 Java、Node、Go、Python 全家桶。每个 step 可以在自己的镜像里工作,比如前端步骤用 node 镜像,后端步骤用 golang 镜像,互不干扰,干净利落。
我自己的体会是,drone 的学习成本低到几乎可以忽略。配置文件就是一坨 YAML,每一步做什么写得很直白。新同学看一遍.drone.yml,比看 Jenkins 那种几百行的 Groovy 脚本要轻松太多。
1.2 docker runner 的执行链路:一次构建是如何完整跑起来的
可以把 drone 理解成“调度中心 + 工位”的结构。drone server 是调度中心,负责接单、排期、记录结果;docker runner 是工位,负责真正干活。
一次构建的完整链路是这样的:
- 你在 Git 仓库 push 代码,托管平台收到 push 事件后,向 drone server 的
/hook地址发一个 webhook。 - drone server 根据仓库关联的
.drone.yml解析出 pipeline,生成一个构建任务。 - server 把这个任务通过 RPC 推给一个空闲的 runner。
- runner 按顺序启动一个个 Docker 容器,每个容器对应 pipeline 里的一个 step。容器会挂载同一个 workspace 卷,所以前一步生成的文件,后一步还能看到。
- runner 把每个 step 的日志实时回传给 server,你在 drone 网页上能看到滚动输出。
- 所有 step 执行完后,server 把最终状态(成功/失败)展示出来,并触发后续通知。
这个设计里最关键的一点,是“每一步都是独立容器”。它带来两个直接好处:一是环境隔离,A 步骤装的依赖不会污染 B 步骤;二是可复现,同样的配置在任何机器上跑,结果几乎一致。你可以把它类比成流水线上的独立工位,每个工位自带工具箱,工人只在自己的工位上操作,做完传给下一个工位。
1.3 什么时候不应该用 drone,先给你泼盆冷水
任何工具都有边界。drone 适合中小团队、项目数量中等、构建任务以 Linux 容器为主的场景。但如果你遇到下面这些情况,建议慎重:
- 团队非常依赖可视化编排和拖拽式编辑,不接受写 YAML 的协作方式。
- 构建流程需要 Windows 环境支持,容器化解决不了。
- 对 pipeline 有非常复杂的审批流和权限隔离要求,比如要按部门、按角色做细粒度审批。
- 仓库只是静态托管,无法配置 webhook,或者网络策略不允许 server 被外部访问。
我当时把现有项目迁移到 drone 之前,特意先选了一个边缘服务做试点。第一个服务跑通后,才逐步把其他项目接进来。这条路比较稳,因为 drone 虽然简单,但“从零到一”的坑并不会因为你项目多就少踩。
2. 从零搭建 drone 服务:server 和 runner 的安装配置要点
2.1 用 Compose 一键拉起 server 与 runner
安装 drone 其实就是在 Docker 里跑两个容器。我用一个docker-compose.yml把 server 和 runner 直接编排起来。
先看一个最小的配置示例:
services: drone-server: image: drone/drone:2 ports: - "3080:80" volumes: - ./drone-data:/var/lib/drone environment: - DRONE_SERVER_HOST=drone.example.com - DRONE_SERVER_PROTO=https - DRONE_RPC_SECRET=your-shared-secret - DRONE_USER_CREATE=admin:your-git-username,admin # 对接代码托管平台的 OAuth 配置,不同平台前缀不同 - DRONE_<PROVIDER>_CLIENT_ID=your-client-id - DRONE_<PROVIDER>_CLIENT_SECRET=your-client-secret # 如果用自建 Git 服务,还需要指定服务器地址 - DRONE_<PROVIDER>_SERVER=https://git.example.com restart: always drone-runner: image: drone/drone-runner-docker:1 ports: - "3000:3000" volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - DRONE_RPC_SERVER=drone-server:80 - DRONE_RPC_SECRET=your-shared-secret - DRONE_RUNNER_CAPACITY=2 restart: always几个环境变量的作用需要理解到位。
DRONE_RPC_SECRET是 server 和 runner 之间的共享密钥,相当于两边对暗号。必须完全一致,否则 runner 连不上 server。这个值建议用随机字符串生成,别用弱密码。
DRONE_SERVER_HOST和DRONE_SERVER_PROTO决定了 server 对外暴露的地址和协议。如果 drone 控制台要通过 HTTPS 访问,这里就写https://drone.example.com,并且端口要映射正确。很多同学在这块踩坑:webhook 回调地址用的是内网 IP,Git 平台在外面访问不到,导致 push 后什么都不触发。
DRONE_USER_CREATE的作用是把某个 Git 账号初始化为 admin。比如你在 Git 平台上的用户名是 alice,这里配成alice:alice,admin,启动后这个账号登录 drone 就是管理员。不配这一项,第一个登录的人往往权限不够,没法激活仓库,会卡在很尴尬的位置。
runner 挂载了宿主机的/var/run/docker.sock,这是它创建构建容器的方式。挂上这个 socket,runner 就等于拿到了宿主机 Docker 的“驾驶权”,可以动态拉起和销毁容器。这是 docker runner 默认的工作模式,后面我会单独讲它的安全问题。
2.2 对接代码库:OAuth 回调、webhook 激活一个都不能少
drone 和 Git 平台的对接分两步:OAuth 授权和 webhook 推送。
OAuth 这一步,每个平台的具体入口不一样,但逻辑是一样的。你要在代码托管平台创建一个应用,拿到 client id 和 client secret,然后把这两个值填到 server 环境变量里。创建应用时填写的回调地址通常是:
https://drone.example.com/login因为你登录 drone 控制台时,它会把登录请求重定向到 Git 平台;用户授权后,平台回调到这个地址,drone 才能拿到身份信息完成登录。
第二步是激活仓库。仓库第一次进入 drone 控制台列表时,状态是未激活的。在界面里点“Activate”,drone 会自动去 Git 平台注册一个 webhook,地址一般是:
https://drone.example.com/hook之后只要有人往这个仓库 push,托管平台就会把这个事件 POST 到这个地址,drone 拿到事件就开始跑流水线。
如果激活后 webhook 没创建成功,通常是 OAuth 授权权限不够。比如账号对那个仓库没有管理员权限,drone 就没法替它注册 webhook。这种情况需要换一个有权限的账号,或者去仓库设置里手动补一条 webhook。
2.3 密钥管理:不要在 YAML 里裸奔
自动化部署免不了要接触各种密钥:镜像仓库的账号密码、SSH key、云厂商 token。这些绝不能直接写在.drone.yml里,因为配置文件通常会提交进代码仓库,等于把密钥公开了。
drone 提供了 Secret 机制,可以在控制台里给某个仓库设置密钥,也可以在命令行里设置:
# 安装 drone 命令行工具后 drone secret add --repository owner/repo --name registry_username --value your-username drone secret add --repository owner/repo --name registry_password --value your-password在流水线里引用时,用from_secret读取:
steps: - name: push image: plugins/docker settings: username: from_secret: registry_username password: from_secret: registry_password需要特别提醒的是:secret 只是对最终日志做了隐藏,它本身仍然是明文存在 server 的数据库里。所以 server 的数据库和备份文件本身也要当作敏感资产对待,别扔在裸磁盘上不做任何保护。
注意:Secret 的可见范围按仓库隔离。如果你有多个仓库要共用同一套部署密钥,最好提升到 organization 级别去配置,否则每个仓库都得重新填一遍,后面改密码时非常痛苦。
3. 手写一份可复用的 drone.yml:从代码提交到线上发布
3.1 流水线骨架与 workspace 挂载逻辑
drone 流水线的核心就是.drone.yml。先看一个最简骨架:
kind: pipeline type: docker name: build workspace: base: /workspace path: src/app steps: - name: test image: golang:1.22 commands: - go test ./...顶层kind: pipeline表示这是一个流水线,type: docker表示使用 docker runner 执行,name是这条流水线的名字。workspace是所有 step 共享的工作目录。假设你的仓库是某公司的 app 仓库,drone 会把代码 clone 到/workspace/src/app下面。前面步骤生成的文件,后面步骤在这个路径下还能看到。
这里有个细节:drone 默认会帮你 clone 代码,每个 step 启动时当前目录就是 workspace 中的仓库根目录。所以 commands 里写相对路径就行,不用反复 cd。
3.2 多阶段构建:从单元测试到镜像推送
实际项目里,流水线至少会拆成测试、编译、镜像、部署四个阶段。阶段之间用depends_on显式声明依赖,drone 才会按顺序执行。
下面是我当时在 Go 服务上用的完整配置,你可以参考改造:
kind: pipeline type: docker name: build workspace: base: /go path: src/app steps: - name: test image: golang:1.22 commands: - go mod download - go test -race -cover ./... depends_on: [] - name: compile image: golang:1.22 commands: - CGO_ENABLED=0 GOOS=linux go build -o app ./cmd/server depends_on: - test - name: push image: plugins/docker settings: repo: registry.internal/app tags: ${DRONE_BRANCH} registry: registry.internal username: from_secret: docker_username password: from_secret: docker_password depends_on: - compile when: branch: - main - dev - name: deploy-dev image: appleboy/drone-ssh settings: host: 192.168.1.20 username: deploy key: from_secret: ssh_key script: - docker pull registry.internal/app:dev - cd /srv/app && docker compose up -d app-dev depends_on: - push when: branch: dev - name: deploy-prod image: appleboy/drone-ssh settings: host: 192.168.1.10 username: deploy key: from_secret: ssh_key script: - docker pull registry.internal/app:main - cd /srv/app && docker compose up -d app-prod depends_on: - push when: branch: main这段配置里值得展开讲几个点。
tags: ${DRONE_BRANCH}是 drone 的内置变量。每次构建时,它会自动替换成当前分支的名字。比如 dev 分支 push 后,镜像 tag 就是 dev;main 分支 push 后,镜像 tag 就是 main。你不需要在 YAML 里硬编码死 tag,发布环境自然就按分支区分了。
when块是条件触发。deploy-dev 只有当分支是 dev 时执行,deploy-prod 只有在分支是 main 时执行。这样一条流水线就可以覆盖测试环境和生产环境,不需要为每个环境单独维护一份配置文件。
depends_on比step 顺序更可靠。drone 默认每个 step 在 Docker 容器里独立运行,如果某个 step 写错了依赖关系,它可能提前并行执行。比如 compile 依赖 test,就应该显式写出来,否则两个步骤可能同时跑,编译时依赖的测试产物可能还没生成。
经验:不要把 deploy 步骤直接写在 push 后面,除非你有十足的把握镜像一定 push 成功。分开写的好处是,如果 push 失败,deploy 根本不会被执行,避免线上拉到一个不存在的镜像然后服务全部挂掉。
3.3 部署环节:远程服务器如何拿到新镜像
部署这一步很多人会纠结:到底是让 drone 直接 ssh 到服务器执行命令,还是在服务器上装一个 agent?
我试下来最简单的做法是:drone 通过 SSH 插件远程登录目标服务器,在服务器上执行docker pull和docker compose up -d。这样服务器上只需要有 docker 和 docker compose,不需要安装任何额外 agent。
上面配置里用的appleboy/drone-ssh插件,支持 host、username、key 这些参数。SSH key 通过from_secret读取,确保密钥不落到仓库里。
为什么推荐在服务器本地 pull,而不是在构建节点构建完直接把镜像推到服务器?因为 docker 的 image 是分层结构,本机 pull 过之后,如果新镜像只有一层变了,那一次传输的数据量小很多;如果你每次都把整个镜像包传到服务器,带宽和耗时都会很难看。在服务器本地 pull 另一个好处是回滚方便,旧 tag 还在本地,切回去基本是秒级。
如果目标服务器需要登录私有镜像仓库,记得提前在服务器上执行docker login,或者用docker compose的credentials配置,否则 pull 时会报认证错误。
3.4 缓存与并发:怎么让流水线更快,少踩“每次全量装依赖”的坑
drone 的每个 step 都是全新容器,这意味着默认情况下,go mod download、npm install这类操作每次构建都会全量执行一遍。项目小的时候还能忍,依赖一多,你会发现构建时间被依赖下载吃掉了大半。
解决办法是挂缓存卷。单节点场景下,我用宿主机目录做缓存最简单:
volumes: - name: gocache host: path: /var/cache/drone/go steps: - name: deps image: golang:1.22 volumes: - name: gocache path: /go/pkg/mod commands: - go mod download这样go mod download下载的依赖模块会落在宿主机/var/cache/drone/go目录里,下一次构建直接复用,构建时间能从五分钟压到一分钟以内。
多节点 runner 的场景下,每个节点本地挂一个缓存目录可能不一致,更稳妥的方案是挂对象存储,用专门的 cache 插件把缓存上传到 S3 兼容服务,构建前再下载回来。这块配置会比单节点复杂,团队规模不大时可以先不上。
3.5 按标签发布:生产环境还是用 tag 更安全
上面例子是按分支部署到不同环境,但生产环境我更建议配合 tag 发布。原因很简单:分支是一个持续变动的引用,main 分支今天这个提交,明天可能就在别的提交上了。线上代码必须能追溯到精确版本。
做法是在流水线里追加一段生产发布流程:
- name: deploy-prod image: appleboy/drone-ssh settings: host: 192.168.1.10 username: deploy key: from_secret: ssh_key script: - docker pull registry.internal/app:${DRONE_TAG} - cd /srv/app && docker compose up -d app when: event: tag${DRONE_TAG}是 drone 内置的 tag 变量。触发条件event: tag表示只有推送 tag 时才执行这一步。你在本地打 tag 并 push 后,drone 会构建一个 tag 版本,并且只对生产环境发布。日常 dev 分支怎么推都不会影响线上,只有你主动发 tag 才意味着真正发版。
这套模式结合镜像 tag 的不可变性,相当于给发布加了一个闸门。万一新版本上线出问题,回滚到上一个 tag 也非常直接。
4. 自动化部署排障实录:docker 权限、镜像缓存、回滚方案
4.1 一上来就踩的 docker socket 权限坑
用 docker runner 最常见的问题,就是构建步骤报权限错误:
dial unix /var/run/docker.sock: connect: permission denied原因很简单:runner 容器里要访问宿主机的 docker socket,但容器内用户权限不够,宿主的 socket 又不允许非 root 用户访问。
我当时的处理方案有两个,根据情况选择。
方案一:直接给 runner 容器加 root。runner 镜像默认可能就是 root 用户,但如果你的运行环境限制了容器用户,需要显式加user: root或者在宿主机上调整/var/run/docker.sock的属组。这个方案最简单,但要把 runner 容器本身当作高权限服务对待,避免攻击面扩大。
方案二:用 Docker-in-Docker(DinD)。在流水线里挂一个 dind service,构建步骤通过 TCP 访问这个 service 来操作 docker。配置大概长这样:
services: - name: docker image: docker:dind environment: - DOCKER_TLS_CERTDIR= steps: - name: build-image image: docker environment: - DOCKER_HOST=tcp://docker:2375 commands: - docker build -t app .这个方案的好处是构建过程不再依赖宿主机的 docker daemon,隔离性更好。缺点是多了一层 dind 容器,性能有一点损耗,且两个容器网络通信需要配置正确,排障起来更绕。
小团队只有一台构建机的场景,我还是推荐方案一,简单直接,少绕路。等规模大了、安全要求高了,再切 dind 也不迟。
4.2 私有镜像仓库登录失败,镜像推不上去
另一个高频问题是 push 私有镜像时报 unauthorized 或者 authentication required。排查思路按下面顺序走:
- 确认 secret 有没有配置对。
from_secret引用的名字必须在仓库 secret 里存在,拼写不一致是最常见的低级错误。 - 确认 registry 参数。
plugins/docker插件的registry要填镜像仓库的地址,比如registry.internal。如果镜像地址是registry.internal/app,但 registry 参数漏了,插件会默认推到 Docker Hub,认证当然失败。 - 终端先手动登录一次:
docker login registry.internal。如果命令行都登不进去,基本可以断定是账号密码错或仓库服务本身有问题,和 drone 无关。
4.3 流水线卡在拉镜像,构建超时怎么定位
drone 流水线卡住,大多数人第一反应是不是哪里死循环了。但我遇到过最多的其实是镜像拉不下来,或者镜像特别大导致超时。
先看 runner 日志。drone server 界面上的日志是流水线 step 的输出,但如果 runner 本身有问题,server 上看不到太多信息。到 runner 机器上执行:
docker logs -f drone-runner能看到 runner 和 server 的通信状态,以及它在创建容器时拉取镜像的进度。如果镜像是从公共仓库拉取,网络状况又不理想,整个构建可能全卡在这一步。解决办法是给 docker daemon 配置registry-mirrors之类的公共镜像加速服务,改完记得重启 docker。
构建超时本身也有缓解手段。在 step 里可以设置较长的重试和超时策略,但治本的方法还是控制流水线复杂度。如果某个 step 动辄跑二十分钟,别急着加超时时间,先去查瓶颈在哪——是不是依赖下载太慢、是不是测试用例太重、是不是构建产物太大。把大步骤拆小,并行度提上来,比单纯调超时参数健康得多。
4.4 发布失败后的回滚,必须提前想清楚
自动化部署越顺畅,越要提前考虑一件事:新版本出问题了,怎么快速回滚?
我现在的做法是“镜像 tag 即版本,发布脚本支持回滚”。线上 docker compose 里的镜像结果是写死的,比如app:1.2.3。如果 1.2.3 有问题,我在服务器上执行:
docker compose up -d --pull app-1.2.2或者更简单粗暴一点,手动把 compose 文件里的版本改回去,重新docker compose up -d。
drone 在这里的角色是“提供回滚的弹药”。也就是说,每一个发往生产的 commit 或者 tag,都会构建并且保留镜像。当你需要回滚时,镜像已经在仓库里,不需要重新构建,直接拉旧 tag 就能起来。可以用 drone 再触发一次旧 tag 的构建,也可以直接在服务器上操作,看线上情况紧急程度。
注意:回滚不是把镜像拉回来就完事了。数据库迁移、缓存清空、日志清理这些关联动作,一定要写进回滚 checklist。否则你代码回滚了,数据库字段没回滚,照样在线上一脸懵。
4.5 常见问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| push 后不触发构建 | webhook 没注册,或 server 地址不可达 | 检查仓库 webhook 列表,确认https://drone.example.com/hook存在且可访问 |
| 构建一直 pending | runner 没有连上 server,或 RPC 密钥不匹配 | 看 runner 日志,核对DRONE_RPC_SECRET |
| step 报 permission denied | docker socket 权限不足 | runner 以 root 运行,或改用 dind 方案 |
| push 镜像报 unauthorized | secret 未配置/拼写错误,或 registry 参数缺失 | 检查 secret 名称,命令行手动docker login验证 |
| 流水线超时 | 镜像拉取慢,或单步执行时间过长 | 配置镜像加速器,拆大步骤,优化依赖缓存 |
| 部署后服务起不来 | 镜像启动命令或环境变量不匹配 | 登录服务器手动docker run复制启动参数,看具体报错日志 |
最后再分享几点我在实际使用中的体会。drone+docker 这套流程,真正用顺了之后,你会有一种“发布不过是顺带的事”的感觉。但它的顺滑是建立在前期准备上的:密钥先规划好,secret 统一管理,回滚方案提前写好。别把一个不重要的服务做实验,更别在没想好回滚路径的情况下就把生产环境接进来。先小规模跑,再逐步扩展,这套方案带来的收益一定会大于你踩坑付出的成本。
后续如果项目规模上来,还可以继续扩展:加第二台 runner 做横向扩容、用 runner 标签区分构建节点、接入钉钉或企业微信推送构建结果、加定时任务做日常巡检。drone 的插件机制让它能衔接的东西很多,但核心永远是那句:让机器把重复的事干好,人只去处理真正需要判断的事。