我前两年接手一个 web 项目的时候,发布还靠手动:本地打包、WinSCP 传到服务器、SSH 上去解压重启。上线日全组待命,最惨一次半夜一点被叫起来回滚版本。后来我花了一个周末研究 GitLab 和 Drone CI 这套持续集成自动部署方案,把代码提交、构建、镜像、部署整条链路串起来,web 项目从 push 代码到网站出新版本,全程不用人盯。今天这篇文章就把这套实际跑通的方案完整拆开讲,从选型理由、环境部署、流水线配置到 Nginx 转发和常见问题全都有,适合中小团队想上 CI/CD、又觉得 Jenkins 太重、GitLab Runner 不想折腾的那些朋友。
1. 方案选型:为什么是 GitLab + Drone CI
1.1 CI/CD 到底是哪几步
很多人一听持续集成就头大,实际上拆开就两个动作:持续集成(CI)解决的是"代码合入后自动验证",持续部署(CD)解决的是"验证通过后自动上线"。
拿一个 web 项目举例:开发把代码 push 到 GitLab 的 main 分支,CI 工具会自动拉取最新代码,在干净环境里安装依赖、跑 lint、跑单元测试、构建产物;这些步骤全部通过后,CD 环节再把产物发到线上服务器,重启服务或者更新容器。整个过程如果靠人手动做,任何一个步骤出错都可能拖慢上线节奏,而交给流水线去做,每次执行的结果都是可复现的。
在这套架构里,GitLab 负责代码托管、MR 审核、Webhook 事件推送,Drone CI 负责流程调度,Drone Runner 负责真正跑任务的执行节点。它们各管一段,配合起来非常清晰。
1.2 Drone 和 Jenkins、GitLab CI 怎么选
我见过很多团队一提到 CI/CD 就默认 Jenkins,我不否认 Jenkins 生态成熟,但它的确存在维护成本偏高的问题:插件要养、节点要管、权限要配,一个小团队往往要花不少精力去伺候它。
Drone 的设计理念完全不同,它把流水线定义成一个.drone.yml文件,跟代码一起放在仓库里,改流程就像改代码一样走 MR 评审;执行任务时每个 step 都是一个独立容器,环境天然隔离,宿主机上不会残留依赖。对比一下更直观:
| 对比维度 | Jenkins | Drone CI | GitLab CI |
|---|---|---|---|
| 部署复杂度 | 中,往往要装插件 | 低,两个容器搞定 | 依赖 GitLab 版本与 Runner 配置 |
| 流水线定义 | Jenkinsfile(Groovy) | .drone.yml(YAML) | .gitlab-ci.yml(YAML) |
| 任务执行方式 | 节点/容器均可 | 默认 Docker 容器 | Shell/Docker/Kubernetes |
| 界面体验 | 功能全但偏重 | 简洁直观 | 与 GitLab 原生集成 |
| 与 GitLab 亲和度 | 一般,要插插件 | OAuth 直连 | 天然一体 |
| 维护成本 | 相对较高 | 很低 | 中低 |
如果你用的是 GitLab 专业版或者旗舰版,自带的 GitLab CI 也完全够用;但很多团队用的是社区版,或者希望 CI 平台独立于代码平台,Drone 就更合适。加上 Drone 的配置几乎不需要写脚本,只是一层 YAML 包着命令,新手看半小时就能上手。
1.3 这套方案真正省心的地方
我自己用了大半年,最大的感受是三个字:不折腾。
配置入库这一点帮了大忙。以前 Jenkins 的流水线脚本散落在不同任务里,谁改过都不知道;现在.drone.yml跟着仓库走,改了什么一清二楚,出问题直接看提交记录就能找到责任人。
资源消耗也低。GitLab、Drone Server、Drone Runner 三个容器叠在一起,内存占用比 Jenkins 全家桶小得多,一台 4 核 8G 的服务器就能非常顺畅地跑起来。
还有一个隐藏优点:Drone 的插件机制虽然不如 Jenkins 丰富,但常用的 docker、ssh、scp、slack 通知都有现成插件,足够覆盖日常发布场景。真要缺什么功能,直接在 step 里挂一个自定义镜像写命令就行,自由度反而更高。
2. 环境准备:容器化部署 GitLab 和 Drone
2.1 先画一张整体架构图
部署之前建议先把整体链路看清楚,避免后面配置时一头雾水:
开发者 push 代码 │ ▼ GitLab(代码仓库 + Webhook 事件源) │ 通过 OAuth 建立信任关系 ▼ Drone Server(控制面,负责任务调度) │ RPC 通信(共享 Secret 鉴权) ▼ Drone Runner(执行面,跑流水线容器) │ 构建镜像 / 跑测试 / 打包产物 ▼ Web 服务器(SSH 远程部署) │ ▼ Nginx 入口 → 用户访问GitLab 负责"代码在哪",Drone Server 负责"接下来干什么",Runner 负责"实际去干"。三者的关系可以类比成:GitLab 是仓库管理员,Server 是调度员,Runner 是执行工人。调度员接到仓库管理员的"到货通知",再派给工人去拆包、质检、上架。
2.2 部署 GitLab 社区版
第一步先在服务器上把 GitLab 跑起来。用 Docker Compose 管理最简单,我平时习惯把数据目录都挂出来,方便备份和迁移。
version: '3.8' services: gitlab: image: gitlab/gitlab-ce:latest container_name: gitlab restart: always hostname: gitlab.local ports: - "8000:80" - "8443:443" - "2222:22" volumes: - ./gitlab/config:/etc/gitlab - ./gitlab/logs:/var/log/gitlab - ./gitlab/data:/var/opt/gitlab shm_size: '256m'这里端口规划要注意:宿主机 8000 转发到容器 80,是为了避免跟其他 Web 服务冲突;22 端口如果已被 SSH 占用,就映射成 2222。
启动后 GitLab 初始化比较慢,要等 2 到 5 分钟,看日志确认:
docker logs -f gitlab看到服务状态正常后,进入容器修改外部访问地址:
docker exec -it gitlab vi /etc/gitlab/gitlab.rb把external_url改成你的实际地址,比如:
external_url 'http://192.168.1.10:8000'保存后执行gitlab-ctl reconfigure。这个地址非常关键,直接影响后续 clone 地址和 OAuth 回调地址,建议一开始就填对。
注意:如果你把 SSH 端口映射成 2222,clone 仓库时要用
ssh://git@192.168.1.10:2222/group/repo.git这种格式,直接写默认 22 会连不上。
2.3 部署 Drone Server 和 Runner
GitLab 就绪后,继续用 Compose 部署 Drone 的两个核心服务:
drone-server: image: drone/drone:2 container_name: drone-server restart: always ports: - "8080:80" volumes: - ./drone/data:/data environment: - DRONE_GITLAB_SERVER=http://192.168.1.10:8000 - DRONE_GITLAB_CLIENT_ID=你的ApplicationID - DRONE_GITLAB_CLIENT_SECRET=你的ApplicationSecret - DRONE_RPC_SECRET=请用一个随机字符串 - DRONE_SERVER_HOST=192.168.1.10:8080 - DRONE_SERVER_PROTO=http - DRONE_USER_CREATE=username:你的GitLab用户名,admin:true - DRONE_LOGS_DEBUG=true drone-runner: image: drone/drone-runner-docker:1 container_name: drone-runner restart: always depends_on: - drone-server volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - DRONE_RPC_PROTO=http - DRONE_RPC_HOST=drone-server - DRONE_RPC_SECRET=和上面保持一致 - DRONE_RUNNER_NAME=runner-1 - DRONE_RUNNER_CAPACITY=2 - DRONE_RUNNER_ENABLE_VOLUMES=true几个关键参数逐个说明:
DRONE_GITLAB_SERVER要带协议前缀,填http://IP:8000,而DRONE_SERVER_HOST填浏览器访问 Drone 用的地址,不带http://。DRONE_RPC_SECRET是 Server 和 Runner 之间的通信密钥,两边的值必须完全一致。生成方式很简单:终端执行openssl rand -hex 32。DRONE_RUNNER_CAPACITY表示这个 Runner 最多同时跑几个任务,中小团队设 2 就够。- Runner 之所以要挂载宿主机的
/var/run/docker.sock,是因为它需要调用宿主机的 Docker 来启动每个 step 的容器。
2.4 在 GitLab 里配置 OAuth2 应用
这一步是整套配置里最容易出错的环节。Drone 要登录 GitLab,靠的是 OAuth2 授权,所以要在 GitLab 里创建一个 Application。
操作路径:浏览器打开 GitLab,用管理员账号登录,进入 Admin Area(管理后台),左侧菜单找到 Applications,点击 New Application。
填写时注意:
- Name 随便写,比如
drone。 - Redirect URI 必须填
http://192.168.1.10:8080/login,注意路径是/login,漏掉或者写错都会导致登录失败。 - Scopes 权限勾选
api、read_user、openid、profile、email,这些是 Drone 读取项目列表和用户信息所需的。
保存后会生成 Application ID 和 Application Secret,把这两个值填到 Drone Server 的环境变量里,然后重启 Drone Server。
提示:如果点击 GitLab 登录后报 redirect_uri 相关的错误,九成是回调地址没写对。先检查 Redirect URI 是不是
http://Drone访问地址/login,确认没有多空格、没有 http/https 混用。
3. 项目接入与 .drone.yml 流水线编写
3.1 激活仓库并确认 Webhook
环境就绪后,访问 Drone 页面,使用 GitLab 账号登录。如果登录后看不到项目列表,优先检查 OAuth 配置,而不是怀疑账号问题。
登录后 Drone 会拉取 GitLab 里你能访问的项目。进入目标仓库,点击右上角的激活按钮,Drone 会自动在 GitLab 项目里创建一条 Webhook。以后只要有 push、tag、MR 事件,GitLab 就会通知 Drone 干活。
可以在 GitLab 项目页的 Settings -> Webhooks 里看到这条记录,点 Test 可以测试连通性。GitLab 项目里如果还没有.drone.yml,Webhook 即使触发了也不会执行任何任务,因为 Drone 拿到事件后会先去仓库根目录找这个文件。
3.2 一个前端 web 项目的完整流水线
这里给一个直接能用的前端项目示例,包含安装依赖、代码检查、单元测试、构建镜像、远程部署五个步骤:
kind: pipeline type: docker name: web-demo trigger: branch: - main - release/* steps: - name: install-deps image: node:18-alpine commands: - npm config set registry https://registry.npmmirror.com - npm install --registry=https://registry.npmmirror.com - name: run-lint image: node:18-alpine commands: - npm run lint - name: unit-test image: node:18-alpine commands: - npm run test:unit - name: build image: node:18-alpine commands: - npm run build - name: build-docker-image image: plugins/docker settings: repo: registry.example.com/demo/web-demo tags: ${DRONE_BRANCH}-${DRONE_BUILD_NUMBER} registry: registry.example.com username: from_secret: REGISTRY_USER password: from_secret: REGISTRY_PASSWORD dockerfile: Dockerfile - name: deploy-to-server image: appleboy/drone-ssh settings: host: from_secret: SSH_HOST username: from_secret: SSH_USER key: from_secret: SSH_KEY port: 22 script: - docker login registry.example.com -u $REGISTRY_USER -p $REGISTRY_PASSWORD - docker pull registry.example.com/demo/web-demo:${DRONE_BRANCH}-${DRONE_BUILD_NUMBER} - docker stop web-demo || true - docker rm web-demo || true - docker run -d --name web-demo -p 8081:80 registry.example.com/demo/web-demo:${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}流水线文件拆开看其实并不复杂:
- 每个 step 都有单独镜像,互不干扰。比如 install-deps、run-lint、build 都用 node 镜像,但执行环境是彼此隔离的。
from_secret从 Drone 的 Secrets 里读取敏感信息,不会明文写到仓库里。- 镜像 tag 我用
${DRONE_BRANCH}-${DRONE_BUILD_NUMBER},既能看出分支,也能区分构建次数,回滚时指定 tag 重新部署就行。
3.3 Java、Python 项目的流水线改法
不是所有 web 项目都是前端,Java 和 Python 项目的思路类似,只是把构建命令换掉。
Java 项目把 node 镜像换成 maven 镜像:
- name: mvn-package image: maven:3.8-openjdk-17 commands: - mvn clean package -DskipTests=false构建产物如果是 jar/war,后续镜像构建步骤的dockerfile可以换成对应的多阶段 Dockerfile;Python Django 项目则用 python 镜像执行pip install -r requirements.txt,再跑相关的测试命令。
流水线的主体结构完全一样:装依赖 -> 跑检查 -> 测试 -> 构建容器 -> 部署。这套模式一旦跑通,换个语言只是改命令而已。
3.4 部署凭据统一用 Secret 管理
流水线里的镜像仓库账号、SSH 私钥、服务器地址这些敏感信息不能直接写在.drone.yml里,否则等于把密码放在仓库里,任何人都能看。
在 Drone 项目页面进入 Settings -> Secrets,添加以下常见 Secret:
| Secret 名称 | 用途 |
|---|---|
| REGISTRY_USER | 私有镜像仓库账号 |
| REGISTRY_PASSWORD | 私有镜像仓库密码 |
| SSH_HOST | 部署服务器地址 |
| SSH_USER | 部署服务器 SSH 用户 |
| SSH_KEY | SSH 私钥内容 |
添加 SSH 私钥时有个常见坑:私钥内容通常包含换行,在 Web 界面粘贴时不要手动替换换行符,直接整段粘贴即可。如果部署时提示Permission denied (publickey),优先怀疑私钥内容变形。
3.5 构建缓存与并发
流水线默认每次执行都是全新容器,npm 和 Maven 的依赖会反复下载,项目大了之后构建时间会明显变长。解决办法是把缓存目录挂载到宿主机,Runner 启用 volume 挂载能力后,在.drone.yml顶部声明卷:
volumes: - name: maven-cache host: path: /var/lib/drone-cache/maven steps: - name: mvn-package image: maven:3.8-openjdk-17 volumes: - name: maven-cache path: /root/.m2 commands: - mvn clean package这一招能让重复构建时间从几分钟降到几十秒,尤其是 Java 项目体验很明显。不过 Runner 默认可能不允许挂载宿主机目录,记得在 Runner 环境变量里确认已经设置DRONE_RUNNER_ENABLE_VOLUMES=true。
4. 自动部署到 Web 服务器的落地细节
4.1 单服务器部署:直接 SSH 远程执行
我日常用得最多的部署方式,是 Drone 通过 SSH 登录目标服务器,远程执行一段部署脚本。这种方式不需要在服务器上安装 Agent,只要有 SSH 权限就能用。
先把 Drone 所在机器的公钥加到目标服务器:
ssh-copy-id user@部署服务器IP然后确认部署用户有操作 Docker 的权限,最简单的方式是把它加入 docker 用户组:
sudo usermod -aG docker user这样前面的deploy-to-server步骤里,才能顺利执行docker pull、docker stop、docker run这些命令。生产服务器如果无法直接访问外网镜像仓库,需要提前解决镜像同步问题,否则docker pull会卡在拉取镜像这一步。
4.2 多项目规划:端口、目录、Nginx 转发
团队同时维护多个 web 项目时,最怕的就是端口冲突和目录混乱。我的习惯是:每个容器内部固定监听 80,宿主机映射到不同端口,Nginx 按域名转发到对应端口。
| 项目 | 访问域名 | 宿主机端口 | 容器端口 |
|---|---|---|---|
| 项目A | demo-a.example.com | 8081 | 80 |
| 项目B | demo-b.example.com | 8082 | 80 |
| 项目C | demo-c.example.com | 8083 | 80 |
这样每个项目之间互相隔离,部署时只需要替换对应容器,不影响其他服务。Nginx 的 server 块可以这样写:
server { listen 80; server_name demo-a.example.com; location / { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }对于纯前端项目,构建产物的静态文件完全可以直接交给 Nginx 管理,不需要容器。Drone 里用appleboy/drone-scp把dist目录传到服务器指定路径就行:
- name: scp-static image: appleboy/drone-scp settings: host: from_secret: SSH_HOST username: from_secret: SSH_USER key: from_secret: SSH_KEY port: 22 source: "dist/*" target: "/data/www/demo-a" strip_components: 1这种方案对静态站点尤其友好,Nginx 直接读磁盘文件,响应速度比过一层容器还快。
4.3 静态资源与缓存策略
不管用容器还是静态目录,Nginx 的静态资源缓存都值得花几分钟配置。CSS、JS、图片这类文件的文件名通常带 hash,可以放心设置长缓存:
location /static/ { alias /data/www/demo-a/static/; expires 30d; add_header Cache-Control "public, no-transform"; }HTML 入口文件不能长缓存,否则用户拿到的是旧版本;其他带 hash 的资源则可以放心缓存 30 天。同时开启 gzip:
gzip on; gzip_types text/plain text/css application/json application/javascript;这样哪怕代码没做太多优化,首屏加载速度也能提升一截。
5. 常见问题与排查技巧实录
5.1 一句话速查表
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| push 后不触发构建 | Webhook 没建成功 | GitLab 项目 Settings -> Webhooks 查看记录,点 Test 测试 |
| Runner 一直 offline | RPC Secret 不一致 | 检查 Server 和 Runner 的DRONE_RPC_SECRET是否相同 |
| 登录 Drone 报 login failed | OAuth 配置错误 | 检查 Application ID/Secret、Redirect URI 是否带/login |
| SSH 部署失败 | 私钥格式损坏 | 重新粘贴完整私钥,不要改换行 |
| 构建时 npm install 极慢 | 网络原因 | 使用 npmmirror 镜像源 |
| 多个任务并发把机器跑满 | 并发数过高 | 调低DRONE_RUNNER_CAPACITY |
| 容器内连不上数据库 | 网络模式问题 | 部署脚本里使用正确的数据库地址,容器间用服务名互通 |
5.2 我踩过的最典型的 5 个坑
第一个坑是激活仓库却不触发构建。我当时检查了 Drone 的配置,发现 GitLab 项目里的 Webhook 确实存在,但 Test 返回 500。查日志发现是 Drone Server 访问 GitLab 的地址填了localhost,而 Drone Server 容器里的localhost指向的是容器自己,自然连不上 GitLab。改成宿主机 IP 后一切正常。容器环境里尽量不要用 localhost 互相引用。
第二个坑是DRONE_RPC_SECRET不一致。Runner 状态一直 offline,UI 上始终看不到在线节点。排查思路是先看 Runner 日志,再对比两边 Secret。这类问题通常不是逻辑复杂,而是环境变量写错或漏配。
第三个坑是 OAuth 回调地址带不带/login的区别。GitLab 里 Redirect URI 写成了http://IP:8080,点击 GitLab 登录后直接报 redirect_uri 不合法。补上/login后就好了。这个问题排查起来会花不少时间,因为报错信息不会直接告诉你缺少路径。
第四个坑是 SSH 私钥换行问题。有一次部署一直报Permission denied,后台把 Secret 里存的私钥打出来看,发现换行符全丢了,私钥变成了一整行,SSH 根本没法解析。解决方式是重新整段粘贴私钥,或者在 UI 里直接选文件上传。
第五个坑是构建缓存卷挂载不生效。Runner 环境变量没开DRONE_RUNNER_ENABLE_VOLUMES,导致.drone.yml里声明 volume 后 Runner 直接报权限错误。加了这个环境变量并重启 Runner 后就好了。如果你的 Runner 版本较新,可能需要进一步确认卷功能是否默认开放,最稳妥的方式是看 Runner 启动日志里有没有 volume 相关提示。
5.3 给 GitLab 做一次备份
这套链路跑起来后,GitLab 里存着所有代码和 MR 记录,重要性不用多说。环境搭建完成后,建议顺手配置备份任务:
docker exec gitlab gitlab-backup create备份文件默认生成在 GitLab 容器内的备份目录,可以再通过 cron 同步到其他机器或者对象存储。Drone 的配置则不用特别备份,因为.drone.yml本身就在代码仓库里,真要恢复环境,重新拉代码再激活仓库就行。
还有一点,GitLab 历史上出现过安全通告,这类问题最直接的应对方式是关注官方发布的安全版本,安排时间升级小版本。不建议长期停留在很老的版本,安全补丁往往跟在新版本后面。升级前先跑一次备份,再把镜像 tag 换到新版本,启动后验证关键功能,流程不复杂但很值。
我自己的体会是,这套 GitLab 加 Drone CI 的组合,真正让我从"发布日不敢睡觉"变成了"上线流程走完还能抽空喝杯水"。第一次跑通的时候可能觉得配置繁琐,其实大部分时间都耗在 OAuth 回调地址和网络连通性这些细节上;只要把第一条流水线跑通,后面再接入新项目,复制.drone.yml改改项目名就能用。如果你们团队现在还在手动发布 web 项目,我建议先别追求一步到位,先把"代码 push 后自动构建镜像、自动部署到测试服务器"这件事跑起来,用顺了再往上加 lint、测试、生产环境限制这些环节。自动化这件事,能早一天做,就能早一天省心。