做DevOps这些年,我越来越认同一句话:持续集成不是炫技,是给团队省心。以前我待过的团队,发布一个Web项目靠的是“人肉部署”:本地build一下,scp到服务器,再手动reload,运气好一次成功,运气不好就得在群里艾特一圈人排查是谁改了配置。后来我把代码托管迁到GitLab,顺手接上了Drone CI,从此提交代码后,构建、打包、推送、部署全部自动完成。今天就用一个真实前端Web项目为例,把GitLab + Drone CI这条持续集成管线的搭建过程、配置细节和坑点,完整地记录一遍。
这篇文适合谁看?如果你已经在用或准备自建GitLab,如果你的项目是Node/Vue/React这类前端Web项目,或者Java、Go、Python后端项目,如果你想用一套“配置即代码”的轻量方案替代笨重的Jenkins,那接下来的内容可以直接照抄。
1. 方案设计与选型思考
1.1 持续集成到底解决了什么问题
先把需求讲清楚。我们最终要实现的,是这样一个效果:开发人员把代码push到GitLab的main分支,后面所有事情都不需要手动管了,Drone会自动拉代码、装依赖、构建、把产物发布到Nginx站点目录,最后reload Nginx。整个过程从push到生效,通常不超过两分钟。
那这件事为什么值得自动化?我举几个实际场景。场景一,多个前端项目同时维护,发布前要手工切换环境变量,有人忘了改,把测试环境的接口地址带到了生产。场景二,服务器上部署目录越来越大,旧文件没人清理,因为“怕删错”。场景三,凌晨上线,运维不在,开发不敢自己操作,等天亮再发,发布窗口被压缩。这些痛点,本质上都是手动操作带来的不确定性和不可追溯性,而持续集成和自动部署,就是用机器流程去消除这部分不确定性。
所以选型第一步不是选工具,而是明确你的流水线要承担哪些职责。我内部一直把发布流水线拆成“三段”:构建段、上传段、生效段。构建段解决“能不能编出来”的问题,上传段解决“产物怎么安全地过去”的问题,生效段解决“旧服务如何平滑切换到新产物”的问题。下面所有配置,都围绕这三段展开。
1.2 为什么是GitLab + Drone CI,而不是Jenkins
提到持续集成,很多人第一反应是Jenkins。这句话我承认,Jenkins的生态确实成熟,文档多、插件全、社区大。但正因为它什么都能干,维护它本身就是一门功课。插件兼容性、升级风险、构建节点管理、JVM内存参数调优……这些在只有两三个开发的小团队里,很难有专人投入精力去养它。我见过不少团队,Jenkins装好之后半年没人更新,插件版本老到连GitLab的新API都不认,连拉取代码都报错。
Drone走的是“云原生、容器化、配置即代码”的路线。它的核心配置文件.drone.yml直接放进项目仓库,跟代码一起版本化,你在GitLab上看到的每次提交,都能对应到一份确定的流水线配置。新员工加入,看一遍.drone.yml就明白发布的整个流程,这比去CI系统的网页上找历史构建记录,要直观太多了。
而且Drone和GitLab的集成是原生的OAuth模式。用户登录Drone,直接用GitLab账号授权,权限可以跟GitLab仓库权限联动。这一点对我们这种“代码和权限都在GitLab上”的团队非常友好,不用在Jenkins里单独维护一套账号和权限体系。
当然,我不是说Jenkins一无是处。如果你的流水线需要大量非容器的旧插件、需要跑Windows桌面端任务、需要复杂的Pipeline DSL逻辑,那Jenkins仍然是合理选择。但如果你面向的是Web开发团队,产物是Node静态文件、Docker镜像、或者是可以容器化的后端服务,Drone这套明显更轻。
1.3 整条流水线的工作逻辑与组件角色
在动手部署前,必须先把Drone的组件角色理解清楚。整套系统由三个部分组成:
第一部分是GitLab,它负责代码托管,同时向Drone Server推送Webhook事件。第二部分是Drone Server,它不直接干活,只负责接收事件、解析并校验.drone.yml、调度任务、展示流水线状态。第三部分是Drone Runner,它才是真正干活的,收到Server的指令后动态创建容器来执行构建步骤。
这里有一个新手最容易搞混的地方:Drone Server看起来像一个Web后台,你以为构建是在它上面执行的,其实不是。真正的构建发生在Runner机器上,而且因为Runner基于Docker,每一步都是全新容器,环境干净可复现。
整个事件流是这样的:开发push代码 -> GitLab触发Webhook -> Drone Server收到通知 -> Server从GitLab拉取.drone.yml -> 把任务分发给Runner -> Runner按yaml定义启动容器、执行命令 -> 构建产物被上传到目标服务器 -> Nginx reload -> 完成。这样一条链路,每部分职责单一,故障排查时也容易定位。
2. 基础环境搭建:GitLab与Drone的部署
2.1 用Docker Compose部署GitLab社区版
我假设你已经有一台Linux服务器,最好是Ubuntu 20.04以上,内存4GB起步。GitLab本身对资源有一定要求,内存低于4G会经常触发OOM,页面加载也会明显变慢。
部署我推荐用Docker Compose,方便升级和管理。先建一个项目目录,比如/data/cicd,在里面放docker-compose.yml:
version: '3' services: gitlab: image: 'gitlab/gitlab-ce:15.10.0-ce.0' container_name: gitlab restart: always hostname: 'gitlab.example.com' environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://gitlab.example.com' gitlab_rails['time_zone'] = 'Asia/Shanghai' gitlab_rails['gitlab_shell_ssh_port'] = 2222 gitlab_rails['smtp_enable'] = false ports: - '80:80' - '2222:22' volumes: - /data/cicd/gitlab/config:/etc/gitlab - /data/cicd/gitlab/logs:/var/log/gitlab - /data/cicd/gitlab/data:/var/opt/gitlab shm_size: '256m'启动命令一句:docker compose up -d gitlab。首次启动GitLab会做内部初始化,需要等个两三分钟。期间可以通过docker logs -f gitlab观察日志,看到“gitlab Reconfigured!”字样后,再访问浏览器。
有很多朋友会卡在“能进GitLab但是clone地址不对”。这里的关键是external_url,它不是随便填的,必须是你今后访问GitLab的完整地址。比如你准备用http://gitlab.example.com去访问,那external_url就必须是http://gitlab.example.com。如果以后上了HTTPS,这里也要同步改成https。
如果你不想用域名,直接用IP也可以,比如external_url 'http://192.168.1.10'。但是用IP有一个小问题,OAuth回调里如果带上端口,容易把地址搞复杂,所以我建议有条件还是先绑个域名或内网DNS解析。
2.2 部署Drone Server
GitLab容器稳定运行后,接着把Drone Server加进来。在docker-compose.yml里追加一个服务:
drone-server: image: drone/drone:2.17.0 container_name: drone-server restart: always ports: - '8080:80' environment: - DRONE_GITLAB_CLIENT_ID=这里填GitLab应用的Application ID - DRONE_GITLAB_CLIENT_SECRET=这里填GitLab应用的Secret - DRONE_RPC_SECRET=生成一段随机字符串 - DRONE_SERVER_HOST=drone.example.com - DRONE_SERVER_PROTO=http - DRONE_GITLAB_URL=http://gitlab.example.com - DRONE_USER_CREATE=username:admin,admin:false volumes: - /data/cicd/drone/server:/dataDRONE_GITLAB_CLIENT_ID和DRONE_GITLAB_CLIENT_SECRET要到第3章创建完OAuth应用才能拿到,所以你可以先把其他变量配好,再回来补这两个。如果你只是想先让服务跑起来,随便填一段,后面再改,重启容器即可。
DRONE_RPC_SECRET这个变量非常关键,它是Server和Runner之间通信的“接头暗号”,两边必须一致。生成方式可以这样:
openssl rand -hex 16然后把输出的32位字符串填入。不要用简单的123456这类弱密码,因为RPC端口一旦暴露,攻击者可能通过Server API接管构建任务。
DRONE_SERVER_HOST和DRONE_SERVER_PROTO决定了用户访问Drone的方式。如果Drone前面有Nginx代理并且域名是drone.example.com,那PROTO填https,HOST填drone.example.com;如果直接IP+端口访问,PROTO填http,HOST填192.168.1.10:8080。这个值会影响OAuth回调地址,填错会导致登录跳转后打不开页面。
DRONE_USER_CREATE这个变量是可选的,作用是自动创建一个用户账号。我这里创建了admin用户,方便后续管理。如果你不设置,第一次通过GitLab OAuth登录时,Drone也会根据GitLab账号自动创建用户。
配置完成,docker compose up -d drone-server,这时浏览器访问http://drone.example.com,应该能看到登录页面,点击继续会跳转到GitLab的授权页。如果这一步没走通,多半是client_id、secret、回调地址三者的关系没对,后面排查章节详细说。
2.3 部署Drone Runner
Runner我这里使用docker runner,因为对于Web项目,docker runner足够通用,而且环境隔离更干净。继续在docker-compose.yml里追加:
drone-runner: image: drone/drone-runner-docker:1.8.3 container_name: drone-runner restart: always environment: - DRONE_RPC_PROTO=http - DRONE_RPC_HOST=drone-server - DRONE_RPC_SECRET=和Server里填的保持一致 - DRONE_RUNNER_NAME=dev-runner-1 - DRONE_RUNNER_CAPACITY=2 volumes: - /var/run/docker.sock:/var/run/docker.sock depends_on: - drone-server由于我在同一个docker-compose网络里,DRONE_RPC_HOST直接填服务名drone-server即可。如果Runner部署在另一台机器,要把这里改成Drone Server的IP或域名,并确保RPC端口(默认80,我映射到宿主机8080)能被访问。
挂载/var/run/docker.sock是必须的。Runner要想动态创建构建容器,必须通过宿主机的Docker守护进程,它自己不需要内置docker引擎。挂载后,Runner创建出来的构建容器和Runner不在同一层,而是跑在宿主机上,这样资源调度更直接。
启动完成后,打开Drone后台,进入Settings -> Runner,能看到runner列表,状态应该是绿色的online。如果不是,执行docker logs -f drone-runner,重点看DRONE_RPC_SECRET有没有报错。
2.4 关键环境变量速查
这里把经常用到、容易混淆的drone-server环境变量整理成一个速查表,方便以后查错:
| 变量名 | 作用 | 常见错误 |
|---|---|---|
| DRONE_GITLAB_CLIENT_ID | GitLab OAuth应用ID | 填错导致跳转后403 |
| DRONE_GITLAB_CLIENT_SECRET | GitLab OAuth应用Secret | 泄露要积极吊销 |
| DRONE_RPC_SECRET | Server/Runner共享密钥 | 两边不一致,Runner离线 |
| DRONE_SERVER_HOST | Drone外部访问地址 | 带不带端口要看实际 |
| DRONE_SERVER_PROTO | Drone外部访问协议 | 误填https会出证书问题 |
| DRONE_GITLAB_URL | 私有GitLab地址 | 必填,否则连不上代码仓库 |
| DRONE_USER_CREATE | 预创建用户 | 可选,注意admin布尔值 |
这个表格在写配置时特别有用,我每次搭建新环境都会对照一遍,基本能避免80%的配置类问题。
3. Web项目接入与自动部署配置
3.1 创建GitLab OAuth应用
接下来就要把GitLab和Drone正式关联起来。使用管理员账号登录GitLab,点击头像进入Edit Profile,在左侧菜单找到Applications,点击New Application。
需要填写的内容是:
- Name:填Drone。
- Redirect URI:填http://drone.example.com/login。很多人会漏掉/login,或者漏掉端口,导致授权后无法跳回Drone。
- Confidential:选择Yes。
- Scopes:将api、read_user、openid、profile、email全部勾选。
这里Scopes我一开始只勾了read_user,能登录但后面无法同步仓库列表。如果你在Drone后台看到仓库列表一直是空的,先去GitLab更新OAuth应用的权限,再回来同步。
提交后,GitLab会生成Application ID和Secret。把这两个值复制到drone-server环境变量里,然后重启容器。重启后再次访问Drone,会重新走一次GitLab授权,这次授权后会进入Drone管理界面。
3.2 激活Drone并同步仓库
登录Drone后台后,左侧面板会展示可用的项目仓库列表。第一次进来,列表可能为空,点击右上角的SYNC按钮,Drone会调用GitLab API同步当前用户能看到的项目。
在列表中找到你的Web项目,点击Activate按钮。激活过程会在GitLab仓库里自动注册一个Webhook,同时把项目标记为启用状态。激活成功后,项目状态由灰色变为绿色。
此时可以去GitLab仓库的Settings -> Integrations里核对一下Webhook是否注册成功。如果Webhook存在但状态一直显示“Failed”,点进去能看到响应内容,最常见的响应异常是404或者Unauthorized。404通常是Drone Server地址填错,Unauthorized通常是Client Secret或Token不对。
我在实际项目中遇到过一种很隐蔽的情况:GitLab和Drone部署在不同的容器网络中,GitLab发Webhook请求到Drone的时候,用的地址是docker-compose服务名,而不是外部访问地址。解决办法是确保GitLab里Webhook的URL是Drone的外部地址,比如http://drone.example.com,而不是http://drone-server:80。Webhook填写错误的话,push代码后Drone没有任何反应,但是GitLab侧会显示请求失败。
3.3 编写.drone.yml:前端Web项目的部署
现在进入到最核心的部分。在项目根目录创建.drone.yml,把流水线定义交给Git托管。以Vue项目为例,完整内容如下:
kind: pipeline type: docker name: web-deploy trigger: branch: - main event: - push steps: - name: install-deps image: node:16-alpine commands: - node -v - npm -v - npm config set registry https://registry.npmmirror.com - npm ci - name: build image: node:16-alpine commands: - npm run build - name: upload-dist image: appleboy/drone-scp settings: host: 192.168.1.100 username: deploy key: from_secret: deploy_key port: 22 source: "dist/*" target: "/opt/www/html" strip_components: 1 - name: reload-nginx image: appleboy/drone-ssh settings: host: 192.168.1.100 username: deploy key: from_secret: deploy_key script: - sudo nginx -s reload关于这部分,我挑几个重点细说。
trigger这一段:branch写成main,event写成push,意味着只有main分支收到push时才会触发。如果你想tag发版也触发,可以再增加tag事件。如果你的仓库还在用master分支,注意改成master。
install-deps和build分开写,语义清晰,而且后续如果只需要build不需要重新install,可以单独调整。镜像用node:16-alpine,比较小,构建速度更快。注意npm ci必须要有package-lock.json,如果没有,改成npm install。
upload-dist这一步用的是appleboy/drone-scp插件,作用是把构建产物通过scp上传到服务器。source字段是dist/*,target是/opt/www/html,strip_components: 1是因为dist目录本身这里也有一层,去掉后目标目录里拿到的就是dist下的文件,而不是dist目录本身。
reload-nginx这一步用appleboy/drone-ssh插件,在服务器上执行nginx -s reload。如果服务器上的nginx配置路径或者用户不同,脚本改成你实际的命令。这里最大的坑是权限问题:deploy用户需要拥有对Nginx reload的sudo权限,并且ssh key要加入deploy用户的authorized_keys。
3.4 部署到Nginx与站点配置
项目部署目录设定为/opt/www/html,接下来要让Nginx能正确地服务这个目录。在部署服务器的Nginx配置目录下,增加一个站点配置,比如/etc/nginx/conf.d/web.conf:
server { listen 80; server_name demo.example.com; root /opt/www/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }写完配置,先执行nginx -t检查语法,没问题再nginx -s reload。这个顺序很重要,我见过有人省掉nginx -t,结果语法错误导致整个Nginx挂了,所有站点都打不开。
有几个细节对Web项目影响很大。第一个是try_files $uri $uri/ /index.html,这是给SPA用的,没有它,用户在浏览器里直接访问某个前端路由,刷新后会404。第二个是location /api/的反向代理,如果你前后端分离、接口路径统一带/api前缀,这一段正好把请求转发到后端服务。第三个是缓存策略,如果要控制静态资源的缓存时间,可以在location里加expires指令,这段配置里没有,但发布静态站点时建议根据自己的需求加上。
3.5 后端项目与容器化项目的部署变体
如果你的“Web项目”不只有静态文件,还包含后端服务,前面那种scp静态文件的方式就不够了。这里我给出一种较常用的方式:构建Docker镜像 -> 推送到镜像仓库 -> 服务器拉镜像 -> 重启容器。
在.drone.yml中,对应步骤可以这样写:
- name: build-image image: plugins/docker settings: registry: registry.example.com repo: registry.example.com/myapp/backend tags: ${DRONE_COMMIT_SHA:0:8} username: from_secret: reg_username password: from_secret: reg_password - name: deploy-container image: appleboy/drone-ssh settings: host: 192.168.1.100 username: deploy key: from_secret: deploy_key script: - docker login -u "$REG_USERNAME" -p "$REG_PASSWORD" registry.example.com - docker pull registry.example.com/myapp/backend:${DRONE_COMMIT_SHA:0:8} - docker rm -f app || true - docker run -d --name app -p 8080:8080 registry.example.com/myapp/backend:${DRONE_COMMIT_SHA:0:8}如果用from_secret引用了密钥,SSH脚本里可以直接以环境变量形式读取。这里有一点强调下:tag不要用latest。用commit短SHA的好处是,每次发布的版本和代码提交一一对应,查看线上容器时一眼就知道跑的是哪次提交;一旦发布出问题,回滚只需要把tag改回上一个SHA并再跑一次部署步骤。
4. 常见问题排查与迭代经验
4.1 OAuth登录失败怎么看日志
Drone和GitLab集成之后,最常遇到的第一个问题就是登录失败。如果你看到的是“login failed. check api token or gitlab version”这行提示,基本上可以断定是OAuth环节出了问题。
第一步,查看Drone Server的容器日志:
docker logs -f drone-server --tail 200日志里通常会有更详细的错误信息,比如GitLab API返回的401/403、回调URL不匹配等。
第二步,用curl手动测试GitLab API,确认当前OAuth应用的token是否有效:
curl -H "Authorization: Bearer <你的token>" http://gitlab.example.com/api/v4/user如果返回JSON里有用户信息,说明token没问题;如果返回401,说明token要么过期,要么scope不够。
第三步,检查GitLab和Drone的版本兼容性。GitLab版本太老,某些API路径或字段可能不被Drone支持。Drone官方文档里会列出对应的GitLab版本范围,安装前先确认。
4.2 Runner一直offline的问题
Runner离线是部署阶段出现频率第二高的问题。排查思路很明确:
先看Runner日志:
docker logs -f drone-runner --tail 100如果出现registration或permission denied关键字,先检查DRONE_RPC_SECRET是否一致。这个密钥在Server和Runner两端必须完全一样,包括空格都算。
如果日志显示connected但状态还是offline,检查DRONE_RPC_HOST和DRONE_RPC_PROTO。一个是协议,一个是地址,注意host不要带http://前缀。
还有防火墙因素。如果Server和Runner不在同机,需要保证Runner能访问Server的80端口(如果映射了8080就访问8080)。可以用telnet或者nc测试一下:
nc -vz 192.168.1.10 8080能通,问题基本就排除在网络层之外。
4.3 构建慢、依赖下载失败怎么办
Runner每次构建都会启动新容器,拉取镜像和下载依赖是耗时大户。如果你发现流水线经常卡在拉取node:16-alpine这一步,检查宿主机Docker的镜像源配置。
修改/etc/docker/daemon.json,类似这样:
{ "registry-mirrors": [ "https://docker.mirrors.ustc.edu.cn", "https://hub-mirror.c.163.com" ] }改完执行systemctl restart docker,再重新跑流水线,速度会有明显提升。如果你用的是Docker Desktop这类工具,在设置界面里也能直接配置registry-mirrors。
npm install慢,除了换registry源,还有一个技巧:把npm cache的volume挂载出来复用。不过Drone的docker runner默认不保留步骤间的容器文件系统,所以直接用npm ci时还是要忍一下缓存缺失的代价。如果依赖特别多,可以在node:16-alpine镜像基础上打一个自定义镜像,把node_modules预先装好,构建时再覆盖业务代码,能快很多。
4.4 部署后页面404或白屏怎么排查
部署流程跑通后,可能出现“流水线全绿,但访问页面是白的”这种诡异情况。遇到这种情况别慌,按顺序排查。
先确认产物目录里有内容没有。SSH到服务器,看/opt/www/html下是否有index.html,以及有没有dist那一层目录套多了的情况。这个问题很常见,因为scp的source和strip_components配置不当,会导致首页文件实际在/opt/www/html/dist/index.html,Nginx的root指向/opt/www/html,自然就404了。
然后看Nginx错误日志:
tail -f /var/log/nginx/error.log如果日志里出现Permission denied,说明deploy用户上传的文件,nginx的worker进程没有读权限。解决办法是调整目录权限,比如chown -R www-data:www-data /opt/www/html,或者让deploy用户umask设置成022,确保文件权限是644。
白屏的话,多半是前端资源加载失败。检查浏览器Network面板,看JS文件是否404,以及API请求是否被跨域或者代理配置挡住。很多时候是location /api/的proxy_pass少写了一个结尾路径,导致接口转发404。
这套GitLab + Drone CI我前后用了两三年,从最初只给前端项目做静态部署,到后来给多个后端服务做镜像构建、多环境发布,还在此基础上加了消息通知、人工确认等步骤。压过很多坑,也在凌晨处理过Runner连不上的事故,但总体上,它给我的回报远远大于维护成本。如果你现在还在人工发布,我建议不妨从一个小项目开始,先跑通一条最简单的前端部署流水线,用起来之后再慢慢扩展。最后送大家一个小技巧:把.drone.yml和部署脚本当成产品代码一样对待,命名清晰、注释充分、定期review,这套系统才能在团队里长期稳定地跑下去。真要说持续集成的核心,工具只是表象,可维护性和确定性才是灵魂。