
我前两年接手一个 web 项目的时候发布还靠手动本地打包、WinSCP 传到服务器、SSH 上去解压重启。上线日全组待命最惨一次半夜一点被叫起来回滚版本。后来我花了一个周末研究 GitLab 和 Drone CI 这套持续集成自动部署方案把代码提交、构建、镜像、部署整条链路串起来web 项目从 push 代码到网站出新版本全程不用人盯。今天这篇文章就把这套实际跑通的方案完整拆开讲从选型理由、环境部署、流水线配置到 Nginx 转发和常见问题全都有适合中小团队想上 CI/CD、又觉得 Jenkins 太重、GitLab Runner 不想折腾的那些朋友。1. 方案选型为什么是 GitLab Drone CI1.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 都是一个独立容器环境天然隔离宿主机上不会残留依赖。对比一下更直观对比维度JenkinsDrone CIGitLab CI部署复杂度中往往要装插件低两个容器搞定依赖 GitLab 版本与 Runner 配置流水线定义JenkinsfileGroovy.drone.ymlYAML.gitlab-ci.ymlYAML任务执行方式节点/容器均可默认 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 和 Drone2.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 端口映射成 2222clone 仓库时要用ssh://git192.168.1.10:2222/group/repo.git这种格式直接写默认 22 会连不上。2.3 部署 Drone Server 和 RunnerGitLab 就绪后继续用 Compose 部署 Drone 的两个核心服务drone-server: image: drone/drone:2 container_name: drone-server restart: always ports: - 8080:80 volumes: - ./drone/data:/data environment: - DRONE_GITLAB_SERVERhttp://192.168.1.10:8000 - DRONE_GITLAB_CLIENT_ID你的ApplicationID - DRONE_GITLAB_CLIENT_SECRET你的ApplicationSecret - DRONE_RPC_SECRET请用一个随机字符串 - DRONE_SERVER_HOST192.168.1.10:8080 - DRONE_SERVER_PROTOhttp - DRONE_USER_CREATEusername:你的GitLab用户名,admin:true - DRONE_LOGS_DEBUGtrue 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_PROTOhttp - DRONE_RPC_HOSTdrone-server - DRONE_RPC_SECRET和上面保持一致 - DRONE_RUNNER_NAMErunner-1 - DRONE_RUNNER_CAPACITY2 - DRONE_RUNNER_ENABLE_VOLUMEStrue几个关键参数逐个说明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.ymlWebhook 即使触发了也不会执行任何任务因为 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 --registryhttps://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 -DskipTestsfalse构建产物如果是 jar/war后续镜像构建步骤的dockerfile可以换成对应的多阶段 DockerfilePython Django 项目则用 python 镜像执行pip install -r requirements.txt再跑相关的测试命令。流水线的主体结构完全一样装依赖 - 跑检查 - 测试 - 构建容器 - 部署。这套模式一旦跑通换个语言只是改命令而已。3.4 部署凭据统一用 Secret 管理流水线里的镜像仓库账号、SSH 私钥、服务器地址这些敏感信息不能直接写在.drone.yml里否则等于把密码放在仓库里任何人都能看。在 Drone 项目页面进入 Settings - Secrets添加以下常见 SecretSecret 名称用途REGISTRY_USER私有镜像仓库账号REGISTRY_PASSWORD私有镜像仓库密码SSH_HOST部署服务器地址SSH_USER部署服务器 SSH 用户SSH_KEYSSH 私钥内容添加 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_VOLUMEStrue。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 按域名转发到对应端口。项目访问域名宿主机端口容器端口项目Ademo-a.example.com808180项目Bdemo-b.example.com808280项目Cdemo-c.example.com808380这样每个项目之间互相隔离部署时只需要替换对应容器不影响其他服务。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 天。同时开启 gzipgzip on; gzip_types text/plain text/css application/json application/javascript;这样哪怕代码没做太多优化首屏加载速度也能提升一截。5. 常见问题与排查技巧实录5.1 一句话速查表症状可能原因解决办法push 后不触发构建Webhook 没建成功GitLab 项目 Settings - Webhooks 查看记录点 Test 测试Runner 一直 offlineRPC Secret 不一致检查 Server 和 Runner 的DRONE_RPC_SECRET是否相同登录 Drone 报 login failedOAuth 配置错误检查 Application ID/Secret、Redirect URI 是否带/loginSSH 部署失败私钥格式损坏重新粘贴完整私钥不要改换行构建时 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 状态一直 offlineUI 上始终看不到在线节点。排查思路是先看 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、测试、生产环境限制这些环节。自动化这件事能早一天做就能早一天省心。