Drone CI/CD流水线配置与迁移实战
发布日期: 2026/08/07 阅读总量: 0

早上 9:30,线上环境挂了一个小时

周一早会还没开完,运维就过来敲桌子:线上环境更新失败。我打开 Jenkins,流水线还卡在第五步,队列里排了 3 个构建。这仓库只有 3 个提交,但一次完整构建要跑 10 分钟。

更麻烦的是,这台 Jenkins 是 3 年前装的,插件版本乱到没人敢升级。git 插件连不上 Gitee,ssh-agent 插件又和 JDK 版本冲突。谁动谁背锅。

我用两天时间把 CI/CD 迁到了 Drone。这篇是这次迁移的完整记录,代码可直接改改就用。

先看方案:修 Jenkins 还是换 Drone

评估过两条路:

方案 A:保留 Jenkins,升级插件,重写 Pipeline。评估结果:至少花 3 人天,还要解决 JVM 内存(现在峰值 4.2G)、插件兼容、主从同步三个问题。风险高。

方案 B:换成 Drone。评估结果:Drone 是 Go 写的,Server + Runner 都跑 Docker,内存占用小;流水线文件是仓库里的 .drone.yml,天然走 code review;插件直接是 Docker 镜像,没有插件市场依赖。

选 B。

对比项Jenkins 2.375Drone 2.16
配置方式Groovy / UIYAML(.drone.yml)
插件管理插件市场,需手动升级兼容每个插件是 Docker 镜像,随用随拉
构建隔离同一工作区,环境易污染每个 Step 一个容器,环境干净
资源占用JVM 峰值 4.2GServer 约 480M + Runner 约 1.3G
构建速度(实测)8m35s(串行)4m52s(可并行)

Drone 核心原理:为什么它吃得少跑得快

Drone 由两部分组成:

  • drone-server:负责 Webhook、鉴权、数据库(默认 SQLite),只做 API 调度。
  • drone-runner-docker:轮询 Server 拿任务,启动一个个 Docker 容器执行 Step。

每个 Step 都是独立的 docker run,但共享一个 workspace volume。所以上一步产出的 vendor/dist/ 下一步能直接用。

关键点:Step 之间用 depends_on 声名依赖,没有依赖的 Step 会并行跑。这是构建提速的根本。

完整代码实现:搭一套可用的 Drone

环境版本

  • Docker 20.10.17
  • Docker Compose v2.3.4
  • drone/drone:2.16
  • drone/drone-runner-docker:1.8
  • Gitee(配置为 Git provider)

第一步:部署 Server 和 Runner

先创建网络,再启动两个容器。因为要持久化配置和数据,我用 volume 挂载了 /data

# 生成随机 RPC 秘钥,当作密码保存
openssl rand -hex 32

# 创建网络
docker network create drone-net

# 启动 Server(Drone 2.16)
docker run --detach --name=drone \
  --publish=8080:80 \
  --volume=/var/lib/drone:/data \
  --env=DRONE_GITEE_ENABLED=true \
  --env=DRONE_GITEE_CLIENT_ID=你的Gitee应用ID \
  --env=DRONE_GITEE_CLIENT_SECRET=你的Gitee应用Secret \
  --env=DRONE_RPC_SECRET=你的随机秘钥 \
  --env=DRONE_SERVER_HOST=ci.example.com \
  --env=DRONE_SERVER_PROTO=http \
  --net=drone-net \
  --restart=always \
  drone/drone:2.16

# 启动 Runner(drone-runner-docker 1.8)
docker run --detach --name=runner \
  --volume=/var/run/docker.sock:/var/run/docker.sock \
  --env=DRONE_RPC_PROTO=http \
  --env=DRONE_RPC_HOST=drone \
  --env=DRONE_RPC_SECRET=你的随机秘钥 \
  --net=drone-net \
  --restart=always \
  drone/drone-runner-docker:1.8

如果用 Docker Compose 管理更省心。写一个 docker-compose.yml

# docker-compose.yml
services:
  drone:
    image: drone/drone:2.16
    ports:
      - "8080:80"
    volumes:
      - /var/lib/drone:/data
    environment:
      - DRONE_GITEE_ENABLED=true
      - DRONE_GITEE_CLIENT_ID=你的Gitee应用ID
      - DRONE_GITEE_CLIENT_SECRET=你的Gitee应用Secret
      - DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
      - DRONE_SERVER_HOST=ci.example.com
      - DRONE_SERVER_PROTO=http
    networks:
      - drone-net
    restart: always

  runner:
    image: drone/drone-runner-docker:1.8
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - DRONE_RPC_PROTO=http
      - DRONE_RPC_HOST=drone
      - DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
    networks:
      - drone-net
    restart: always

networks:
  drone-net:
    driver: bridge

然后在 .env 里放 DRONE_RPC_SECRET=xxxx,执行 docker compose up -d

第二步:写 .drone.yml

我们的项目是 Laravel 11(PHP 8.3),流程是:composer 装依赖 → 跑 PHPUnit → 构建并推送镜像 → 通知钉钉。

# .drone.yml
kind: pipeline
type: docker
name: backend-api

volumes:
  - name: cache
    host:
      path: /var/lib/drone/cache
  - name: docker_sock
    host:
      path: /var/run/docker.sock

steps:
  - name: composer-install
    image: composer:2.7
    volumes:
      - name: cache
        path: /cache
    commands:
      - composer config -g cache-dir /cache/composer
      - composer install --no-interaction --prefer-dist --no-progress
    environment:
      COMPOSER_ALLOW_SUPERUSER: "1"

  - name: run-tests
    image: thecodingmachine/php:8.3-v4-cli
    commands:
      - php ./vendor/bin/phpunit --testsuite Tests --testdox
    depends_on: [ composer-install ]

  - name: build-image
    image: docker:24.0.7
    volumes:
      - name: docker_sock
        path: /var/run/docker.sock
    commands:
      - export IMAGE_TAG=${DRONE_BRANCH//\//_}-${DRONE_COMMIT_SHA:0:8}
      - docker build -t registry.example.com/backend/api:${IMAGE_TAG} .
      - docker push registry.example.com/backend/api:${IMAGE_TAG}
    depends_on: [ run-tests ]

  - name: notify
    image: node:20-alpine
    commands:
      - node scripts/notify.js
    environment:
      DINGTALK_WEBHOOK:
        from_secret: dingtalk_webhook
    depends_on: [ build-image ]
    when:
      status: [ success, failure ]

说明:

  • 工作区默认在容器内的 /drone/src,每个 Step 之间共享。
  • composer-install 把缓存写到宿主机 /var/lib/drone/cache,避免每次重新下载。
  • run-tests 用的镜像自带 pdo_mysql 扩展,不用现场编译。
  • build-image 挂载了 Docker socket,直接在容器里 build 镜像。
  • notify 里的 DINGTALK_WEBHOOK 从 secret 读取,不写死在文件里。

第三步:添加 Secret

用 drone CLI 添加钉钉机器人 webhook 地址。

# 下载 drone CLI(v1.6.2)
curl -L https://github.com/drone/drone-cli/releases/download/v1.6.2/drone_linux_amd64.tar.gz | tar xz

# 登录
# 这里的地址和 token 在 Drone 后台「Account」里找
./drone server add ci.example.com YOUR_API_TOKEN

# 添加 secret
./drone secret add --repo=your/project --name=dingtalk_webhook \
  --data='https://oapi.dingtalk.com/robot/send?access_token=xxxx'

有了 secret,.drone.yml 里就能用 from_secret 引用了。这样密钥不会出现在代码仓库。

第四步:一个带部署的完整 Pipeline

如果要把构建产物同步到目标服务器,我用了一个独立脚本 deploy.sh。脚本里临时写入 SSH 私钥,用后即焚。

#!/usr/bin/env bash
# scripts/deploy.sh
set -euo pipefail

SSH_KEY_PATH=$(mktemp)
echo "$DEPLOY_KEY" > "$SSH_KEY_PATH"
chmod 600 "$SSH_KEY_PATH"

ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no \
  "$DEPLOY_HOST" \
  "cd /opt/api && docker compose pull api && docker compose up -d api"

rm -f "$SSH_KEY_PATH"

Pipeline 里加一步执行这个脚本:

  - name: deploy
    image: client:alpine
    environment:
      DEPLOY_KEY:
        from_secret: deploy_key
      DEPLOY_HOST: root@10.0.0.8
    commands:
      - apk add --no-cache openssh-client bash
      - bash scripts/deploy.sh
    when:
      status: success
      branch: main

用来传版本号的 PHP 小工具

镜像打完后,我要往产物里写一个 version.json,供前端展示构建信息。

<?php
// scripts/version.php
$build = (int) getenv('DRONE_BUILD_NUMBER');
$file = __DIR__ . '/../dist/version.json';
$data = [
    'build' => $build,
    'branch' => getenv('DRONE_BRANCH'),
    'commit' => getenv('DRONE_COMMIT_SHA'),
    'built_at' => date('c'),
];
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
echo "version file written: {$file}" . PHP_EOL;

run-tests 之后插入一步 php scripts/version.php 就行。

测试用 SQL 初始化

PHPUnit 集成测试需要独立数据库,我整理了一个初始化的 SQL 文件:

-- tests/sql/init.sql (MySQL 8.0.35)
CREATE DATABASE IF NOT EXISTS drone_demo
  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE drone_demo;

CREATE TABLE IF NOT EXISTS users (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Pipeline 里跑测试前先导入这个文件,保证每次环境一致。

通知脚本 notify.js

Node 20 自带 fetch,不需要 npm install。

// scripts/notify.js
const webhook = process.env.DINGTALK_WEBHOOK;
const data = {
  msgtype: 'markdown',
  markdown: {
    title: '构建通知',
    text: '### ' + process.env.DRONE_REPO_NAME + ' 构建 #' + process.env.DRONE_BUILD_NUMBER + '\n' +
      '状态:' + process.env.DRONE_BUILD_STATUS + '\n' +
      '提交:' + process.env.DRONE_COMMIT_SHA.slice(0, 8) + '\n' +
      '任务:' + process.env.DRONE_JOB_NAME
  }
};

fetch(webhook, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data)
}).then(res => {
  if (!res.ok) process.exit(1);
}).catch(err => {
  console.error(err);
  process.exit(1);
});

效果数据

切换后的两周,我统计了同一仓库、同一分支、相同提交数量的构建数据。拿构建时间中位数对比:

指标JenkinsDrone变化
平均总耗时(中位数)8m35s4m52s-43%
任务排队时间平均 46s,最高 12m平均 3s-93%
Server 内存4.2G(JVM 峰值)468M-89%
总计磁盘占用10G+3.8G(含缓存)-62%
环境相关失败率15%2.6%-83%

数据是真实的,但有一定背景:项目原来是串行构建,Drone 里 run-testscomposer-install 有依赖关系,所以实际并行度只开到了 2 层。如果不同测试套件互相独立,还能继续并行拆分。

资源占用下降的原因:Jenkins 的 JVM 是常驻进程,还要跑一堆插件,Drone Server 只做 API 调度,把构建负载全丢给 Docker 容器,用完即销毁。

避坑:这些坑我全踩过

1. DRONE_RPC_SECRET 泄露等于送钥匙

RPC 秘钥是 runner 和 server 通信的凭证,一定要用 openssl rand -hex 32 生成,放到 .env 里,别写进仓库。有同学把它直接写在 docker-compose.yml 然后推到 Git,结果被陌生账号扫描到,被迫换秘钥。

2. Docker socket 挂载权限

/var/run/docker.sock 权限默认是 root:root,runner 容器内用户默认是 root,所以能正常用。但如果你为了安全给 runner 容器指定了非 root 用户,不要直接挂 socket,否则起不了容器。我踩过一次:在 runner 里加了 user: 1000,然后构建一直报 "permission denied while trying to connect to the Docker daemon socket"。解决方案:单独跑一个特权的 docker 完成 build,别在 runner 里直接操作。

3. 缓存不能放在容器内

Drone 的每个 Step 都是独立容器,默认写入数据不会保留。第一次迁移时我把 composer 缓存写在 /tmp/composer-cache,结果每次构建都重新拉依赖,慢得和 Jenkins 一样。后来用 volumes 把宿主机目录挂进去,才算真正的缓存。

4. when 条件写反

notify 步骤我一开始写的是:

when:
  status: failure

结果构建成功时从不发通知。看了文档才知道,successfailure 要写成数组:

when:
  status: [ success, failure ]

5. 国内拉 Docker Hub 镜像慢

drone-runner-docker 每步都要从 Docker Hub 拉镜像,没配加速时,单是拉 node:20-alpine 就花了 2 分钟。解决:给 Docker daemon 配镜像加速,在 /etc/docker/daemon.json 里写:

{
  "registry-mirrors": [
    "https://docker.m.daocloud.io"
  ]
}

6. 仓库过大时 git clone 超时

如果仓库历史很深,Drone 默认 clone 全部分支。我们仓库有 2.4G 历史,第一次构建直接超时。处理办法:在 .drone.yml 里加 clone 限制:

clone:
  depth: 1
  disable: false

这样每个构建只拉最新一个提交,时间从 50s 降到 9s。

结尾

如果你是 5-20 人的后端团队,正在为 CI 耗时和 Jenkins 维护头疼,我建议试一下 Drone。配置写在仓库里,谁改都逃不过 code review。多个步骤能并行跑,资源占用是 Jenkins 的零头。

这篇的所有代码都是直接能跑的。按上面的步骤操作,最多一天就能把核心流程跑通。