干我们这行最怕的不是需求多,而是发版靠手工。项目一多,ssh 上去装依赖、打包、再传服务器,一套流程重复 N 遍,中间只要手一抖,线上就多一个事故。所以我一直想把“代码 push 完 → 自动构建 → 自动部署到主机”这条链路彻底打通。这篇文章就是我在实际项目中落地的一套方案:用 Arbess 作为自动化流水线平台,GitLab 做代码托管和事件触发,目标是让一个 React.js 项目从提交代码到部署到 Linux 主机,全程不需要人工介入。如果你正在为“每次发版都战战兢兢”而头疼,或者想从 Jenkins 全家桶里跳出来换一种更轻量的玩法,这篇内容应该能给你一条可以直接照着走的路。
1. 这套组合解决什么问题:为什么是 Arbess + GitLab
1.1 从“手动发版”到“推送即部署”的链路设计
先把整条链路在脑子里画出来:开发者在本地把代码推到 GitLab 仓库,GitLab 那边产生一个 push 事件,Arbess 通过 webhook 收到这个事件,自动从仓库拉取最新代码,然后在一台构建机上安装依赖、执行构建命令,把 React 项目打成静态文件,最后通过 SSH 把产物分发到目标主机,并远程执行部署脚本,让 Nginx 切换到新版本。整个过程里,开发者只做一件事:push。
这个链路里,GitLab 负责的是“代码仓库 + 事件源 + 权限控制”,Arbess 负责的是“流水线编排 + 构建执行 + 部署动作”。两者各管一段,职责清晰。相比把构建部署逻辑全塞进 GitLab CI 里,这种拆分的好处是:流水线的执行引擎和代码仓库解耦,以后你就算把仓库迁到别的 Git 服务,流水线也可以原样保留,不用推倒重来。
另外,主机部署这个场景很多人容易忽略——不是所有项目都上了 Kubernetes,大量内部系统、管理后台、公司官网还跑在普通云主机上。这类项目用 K8s 是大炮打蚊子,但纯手动部署又太低效。Arbess + GitLab + SSH 主机部署这套组合,恰好卡在“轻量”和“自动化”的平衡点上。
1.2 为什么不用 Jenkins 全家桶:选型背后的逻辑
每次聊自动化构建,总有人问“为什么不用 Jenkins”。不是 Jenkins 不好,而是它太重了。Jenkins 的插件生态确实丰富,但随之而来的是维护成本:插件版本兼容、Master-Slave 节点管理、Groovy 脚本调试、权限模型配置……这些对于一个小团队或者一个中小型项目的 DevOps 体系建设来说,投入产出比并不高。
Arbess 这类新一代流水线平台,核心思路是“流水线即代码”。你不用在网页上拖一堆构建任务,而是把构建、部署的步骤写成一个 YAML 文件,跟项目代码放在一起。这样有两个非常实际的好处:第一,流水线配置跟着代码走,每次改动都有 Git 历史可查,出了问题可以直接回滚到上一个版本的流水线配置;第二,新成员上手时不用去学一套复杂的 Jenkins 操作,打开 YAML 文件就能看懂这个项目是怎么构建、怎么部署的。
从触发方式上说,GitLab webhook 比 Jenkins 定时轮询更高效。代码一 push,webhook 立刻通知 Arbess,不用等轮询周期,构建启动更快,服务器压力也更小。当然,GitLab 自带的 CI/CD 也能做这些事,但如果你要统一管理多个 Git 仓库源、想在主机部署之外还保留后续扩展空间,Arbess 的独立平台模式会更灵活。这套选型本质上是在“够用”和“可控”之间做一个偏向工程效率的选择。
2. 环境准备:GitLab、Arbess、目标主机三端就绪
2.1 用 Docker 装一个 GitLab 社区版并做好基础设置
GitLab 社区版的功能对中小团队完全够用。我习惯用 Docker 方式部署,省去手动处理 Ruby 依赖和系统包的麻烦。一个典型的启动命令是这样的:
export GITLAB_HOME=/srv/gitlab docker run --detach \ --hostname gitlab.example.com \ --publish 443:443 \ --publish 80:80 \ --publish 22:22 \ --name gitlab \ --restart always \ --volume $GITLAB_HOME/config:/etc/gitlab \ --volume $GITLAB_HOME/logs:/var/log/gitlab \ --volume $GITLAB_HOME/data:/var/opt/gitlab \ gitlab/gitlab-ce:latest这里有几个点需要特别提醒。--hostname一定要设置成你实际访问 GitLab 的域名或 IP,因为 GitLab 会把仓库的 Clone 地址和 Webhook 地址都基于这个 hostname 生成,改起来很麻烦。另外,GitLab 是内存大户,官方建议至少 4G 内存,亲测 2G 内存跑起来会频繁卡顿,构建任务一多直接 OOM。如果服务器资源紧张,可以适当调低unicorn和sidekiq的并发数,但别调太狠,否则 API 响应会慢到让人崩溃。
装好之后,第一次访问会让你设置 root 初始密码。紧接着有件重要的事:进入Admin Area → Settings → Sign-up Restrictions,关掉“Sign-up enabled”,不然公网服务器上任何人都能注册账号,这是最基本的安全习惯。同时检查一下Admin Area → Settings → Visibility and access control,把项目默认可见性改成 Private,避免代码裸奔。
2.2 部署 Arbess 平台并打通 GitLab 连接
Arbess 的部署方式也支持容器化。我这边是直接在一台独立的构建服务器上跑了一个 Docker 容器,数据目录挂载到宿主机,方便升级和备份:
mkdir -p /opt/arbess && chmod 755 /opt/arbess docker run -d \ --name arbess \ --restart always \ -p 8080:8080 \ -v /opt/arbess:/data \ arbess/arbess:latest启动完成后,浏览器访问http://构建机IP:8080,按引导创建管理员账号。接下来最关键的一步是让 Arbess 能访问 GitLab。我通常的做法是在 GitLab 里创建一个专门给自动化平台使用的账号,而不是直接用 root。这是最小权限原则——就算这台构建机被攻破,攻击者拿到的也只是流水线执行权限,而不是 GitLab 管理员权限。
在 GitLab 中进入User Settings → Access Tokens,给这个专用账号创建一个 Personal Access Token,权限范围勾选api、read_repository、write_repository,然后把这个 Token 填到 Arbess 的 GitLab 连接配置里。创建 Token 时记得设置过期时间,这是很多团队忽略的点。Token 一旦泄露,如果没有过期时间,等于给攻击者留了一扇永久后门。
打通连接之后,去 GitLab 项目里配置 Webhook:Settings → Webhooks,URL 填 Arbess 的接收地址,Secret Token 填一串随机字符串,Trigger 勾选Push events。这样每次 push 代码,GitLab 就会把事件推送到 Arbess 触发流水线。添加 Webhook 之后,可以先用 GitLab 自带的“Test”按钮发一个测试事件,确认接收正常。
2.3 目标主机侧准备:部署用户、目录结构和 SSH 免密
目标主机就是最终跑 Nginx 和服务的那台机器。在动手写流水线之前,先把主机侧收拾利索。
我习惯为每个应用创建独立的操作系统用户,比如deploy,权限只限定在应用目录内,不要给 root。目录结构是这套部署方案的核心,我建议这样规划:
/app/react-app/ ├── releases/ │ ├── 20250110143000/ │ ├── 20250111101500/ │ └── 20250112162000/ ├── current -> /app/react-app/releases/20250112162000 └── deploy.shreleases目录下每次部署生成一个带时间戳的版本目录,current是一个软链接,始终指向当前线上版本。这样做的最大好处是回滚极其简单——只要把current指回上一个版本目录,然后重载 Nginx 就完成了,不需要重新构建,不需要解压覆盖。
SSH 免密这步要特别注意。Arbess 构建机需要能免密 SSH 到目标主机,但不是用 root,而是用刚才创建的deploy用户。在构建机上生成密钥对:
ssh-keygen -t ed25519 -C "arbess-deploy"然后把公钥追加到目标主机的/home/deploy/.ssh/authorized_keys里,私钥配置到 Arbess 的凭证管理里。这里有个我从实践中踩出来的经验:私钥文件权限必须是 600,目录权限必须是 700,否则 SSH 会直接拒绝使用这个密钥。很多新手在这个问题上耗半天,其实就是一个权限位的事。
3. 落地实操:React.js 项目流水线配置与主机部署全流程
3.1 React.js 项目接入前的仓库梳理
流水线要跑得顺,项目仓库得先整理利索。React.js 项目本身不复杂,但有几个细节会影响构建稳定性和部署结果。
首先,.gitignore必须把node_modules、dist、.env.local这些本地文件排除掉,避免仓库里混入依赖包和本地环境配置。其次,我强烈建议在项目根目录放一个.nvmrc文件,写上18或者20。构建时如果用的 Node 版本跟本机开发环境不一致,轻则依赖安装报错,重则构建出来的产物在线上出现诡异的问题。Node.js 的大版本升级通常会引入一些破坏性变更,React 项目尤其要锁定主版本。
包管理器方面,现代 React 项目一般用 npm 或 pnpm。我这里用 npm 举例,但关键点是:安装依赖这一步不要用npm install,要用npm ci。npm ci会根据package-lock.json精确安装锁定的版本,不会自作主张升级某个依赖,构建环境才能保持可复现。如果你的项目还没有package-lock.json,跑一次npm install生成它,提交到仓库。
环境变量也要在接入流水线前想清楚。React 项目在构建时会把REACT_APP_*开头的环境变量编译进静态文件里,所以这些变量应该在流水线构建阶段注入,而不是在运行时读取。我一般会在仓库里维护一个.env.production示例,真正的内容放在 Arbess 的环境变量配置里,比如REACT_APP_API_BASE_URL=https://api.example.com。这样开发环境和生产环境之间的配置就彻底隔离了。
3.2 build 阶段:依赖安装与静态产物构建
Arbess 的流水线配置是 YAML 文件,放在项目仓库里统一管理。我这里给一个典型的 React.js 构建部署配置,写法上按通用 YAML 流程展示,你们落地时根据自己 Arbess 版本的字段规范微调即可:
version: 1.0 stages: - build - deploy build: stage: build image: node:20 script: - npm ci - npm run build cache: - node_modules artifacts: - dist/ deploy: stage: deploy script: - ssh deploy@10.0.0.8 "mkdir -p /app/react-app/releases/$(date +%Y%m%d%H%M%S)" - rsync -az --delete dist/ deploy@10.0.0.8:/app/react-app/releases/$(date +%Y%m%d%H%M%S)/ - ssh deploy@10.0.0.8 "bash /app/react-app/deploy.sh $(date +%Y%m%d%H%M%S)"第一次跑流水线时,npm ci会完整下载所有依赖,耗时可能比较长,这是正常的。后续构建一定要配置缓存,把node_modules缓存起来,用 package-lock.json 的哈希作为缓存 key,只要锁文件没变就直接复用缓存,构建速度能缩短一大半。我在实测中,一个中等规模的 React 项目,首次构建用了三分多钟,缓存命中之后基本稳定在一分钟以内。
构建产物dist目录要作为 artifacts 传给 deploy 阶段。这一步看似不起眼,但很多人踩过坑:如果在 deploy 阶段重新拉代码再构建一次,不仅浪费资源,还可能因为构建环境不一致导致部署的产物跟测试的不是同一份。在 build 阶段把产物固定下来,deploy 阶段只负责传输和切换,这才是“可追溯的部署”。
3.3 deploy 阶段:rsync 分发与远程脚本执行
rsync 是整个部署流程里最顺手的分发工具。相比 scp,rsync 支持增量传输,二次部署时只传变更文件,速度优势非常明显。我常用的参数是:
rsync -az --delete dist/ deploy@10.0.0.8:/app/react-app/releases/20250112162000/-a保留文件权限和时间戳,-z传输时压缩,--delete确保远端目录跟本地dist完全一致,不会残留旧的静态文件。需要注意,dist/最后的斜杠不能丢,它表示把dist目录下的内容同步到目标目录,而不是把dist这个目录本身嵌套进去。
rsync 传完之后,接着用 SSH 远程执行目标主机上的部署脚本。这里我刻意把“文件传输”和“服务切换”拆成两步——传输失败不影响线上版本,切换失败跟传输无关,排查问题时边界清晰。
实际部署时建议给 deploy 阶段加上超时限制和失败重试。SSH 连接偶尔会因为网络抖动失败一次,重试机制非常实用。另外,流水线里部署主机 IP 这类信息不要硬编码在 YAML 里,应该配置成变量,方便以后加一台新机器时不用改仓库代码。
3.4 版本化部署脚本与 Nginx 配置细节
目标主机上的deploy.sh是整个部署动作的收口。这个脚本不复杂,但逻辑必须严谨:
#!/usr/bin/env bash set -e APP_NAME="react-app" BASE_DIR="/app/${APP_NAME}" TARGET_VERSION="$1" if [ -z "$TARGET_VERSION" ]; then echo "usage: $0 <release_version>" exit 1 fi RELEASE_DIR="${BASE_DIR}/releases/${TARGET_VERSION}" if [ ! -d "$RELEASE_DIR" ]; then echo "release dir not found: ${RELEASE_DIR}" exit 1 fi ln -sfn "$RELEASE_DIR" "${BASE_DIR}/current" # 健康检查,确认 nginx 能正常返回 sleep 2 if curl -sfI "http://127.0.0.1/healthz" >/dev/null 2>&1; then echo "deploy ok: ${RELEASE_DIR}" else echo "health check failed, rollback..." # 切回上一个版本 PREV_RELEASE=$(ls -1t "${BASE_DIR}/releases" | sed -n '2p') ln -sfn "${BASE_DIR}/releases/${PREV_RELEASE}" "${BASE_DIR}/current" exit 1 fiset -e让脚本在任意一步出错时立刻退出,避免带着半截状态继续执行。软链切换本身是原子操作,部署过程中不会出现用户访问到“半新半旧”文件的情况。健康检查那一步是我后来补上的——最初部署只做软链切换,结果有一次构建出来的静态文件缺了资源,线上直接白屏,人还在开心地说“部署完了”。加上健康检查之后,有问题会在切换后的两秒内被发现并自动回滚。
Nginx 配置也要跟这套目录结构配合。如果 React 项目用了 BrowserRouter 这类前端路由,try_files那行不能省:
server { listen 80; server_name react.example.com; root /app/react-app/current; index index.html; location / { try_files $uri $uri/ /index.html; } access_log /var/log/nginx/react-app.access.log; }关键是root指向的是current软链接,而不是某个具体的版本目录。这样每次部署切换软链,Nginx 会自动生效,只需要systemctl reload nginx,不需要重启进程,线上请求不会中断。reload和restart的区别就在这——reload是平滑重载配置,正在处理的请求不会断。
4. 常见问题与排查实录
4.1 连接 GitLab 时的登录与令牌问题
先说说运营中最常见的“登录失败”问题。如果你在用 IntelliJ IDEA 或者 PyCharm 连接 GitLab,登录时提示gitlab versions older than 14.0 are not supported. log in,大概率是两件事之一:要么你的 GitLab 版本太老,要么你还在用账号密码方式登录。新版的 JetBrains 系列 IDE 默认走 GitLab API,14.0 之前的 API 兼容性已经不维护了。
这种情况我的建议是双管齐下:先升级 GitLab 社区版到较新的稳定版本,然后把 IDE 登录方式切换成 Personal Access Token。在 GitLab 里进入User Settings → Access Tokens,创建一个 token,权限勾选api和read_repository,拿到 token 后在 IDE 的 GitLab 登录面板选择 Token 方式粘贴进去。这里有个容易摸不着头脑的点:Token 可能需要填在“密码”那一栏而不是用户名那一栏,不同 IDE 版本入口不一样,以提示为准。
还有一种更隐蔽的情况:提示login failed. check api token or gitlab version. log in via git if the version。这里的关键是 token 的 scope 不够。GitLab 的 API token 权限是分级的,如果你创建的 token 只有read_repository,那登录能成功,但某些需要写权限的操作会被拒。排插顺序建议是:先确认 GitLab 版本,再确认 token scope,最后检查系统时间是否准确——别笑,token 校验是依赖时间戳的,服务器时间漂移几分钟就会导致鉴权失败。
4.2 构建阶段常见的翻车点
构建阶段的问题,大部分集中在依赖和内存上。
依赖下载慢是每个网络环境都会遇到的痛点。npm ci默认走官方 registry,国内访问速度很不稳定,一次构建卡在下载阶段十几分钟不是新鲜事。处理方式是配置 registry 镜像源,在流水线里显式指定:
script: - npm config set registry https://registry.npmmirror.com - npm ci但要注意,构建机的这个配置只对该次构建有效,不要直接改到全局,避免其他项目被同一份配置影响。
另一个高频问题是 Node 构建内存溢出,报错往往带着JavaScript heap out of memory。React 项目体积大了之后,webpack 或 vite 在压缩代码阶段确实可能吃掉几个 G 内存。处理方式是给 Node 显式设置堆内存上限:
export NODE_OPTIONS="--max-old-space-size=4096"设置之后要注意,这只是把内存上限提高了,如果构建机物理内存本来就不够,该 OOM 还是会 OOM,只是时间往后拖延了一点。所以还是要在构建机资源上留足余量。
缓存失效也是个容易踩的坑。配置了node_modules缓存之后,如果依赖没有变化但构建突然失败,先怀疑缓存是不是被污染的。这时候清掉缓存重跑一次,如果好了,说明缓存里混入了脏数据。我现在的做法是缓存 key 绑定package-lock.json的哈希,锁文件一变,缓存自动失效,从根上避免这个问题。
4.3 部署阶段 SSH、权限与 Nginx 的坑
部署阶段的问题,九成出在 SSH 和权限上。
SSH 连不上的时候,先不要急着怀疑网络。按这个顺序查:目标主机的sshd服务有没有在跑、防火墙有没有放行 22 端口、deploy用户的公钥有没有正确写进authorized_keys、私钥文件的权限是不是 600。我见过最多的就是私钥权限给了 644,SSH 直接以“permissions are too open”为由拒绝使用。
还有个容易忽略的点:Arbess 构建机首次 SSH 连接目标主机时,会遇到host key verification failed。因为目标主机的指纹不在构建机的known_hosts里。处理方式是在建主机时往~/.ssh/known_hosts里提前写入目标机器的公钥指纹,而不是在流水线脚本里临时加StrictHostKeyChecking=no——后者虽然省事,但等于关闭了 SSH 的防中间人攻击保护,生产环境不建议这么干。
rsync 传输时也要注意排除项。如果直接把项目整个目录 rsync 过去,node_modules和.git会一起传过去,不仅慢,而且没有意义。在 deploy 命令里显式排除:
rsync -az --delete --exclude 'node_modules' --exclude '.git' --exclude '*.map' dist/ deploy@10.0.0.8:/app/react-app/releases/xxx/Nginx 层面的坑主要是 404。如果你部署的 React 项目刷新页面就 404,基本就是try_files没配。try_files $uri $uri/ /index.html的含义是:先找真实文件,没有就尝试目录,都没有就统一回退到index.html,由前端路由接管。少了最后那个/index.html,刷新一个子路由页面就会直接白屏。
4.4 GitLab 安全加固:从漏洞修复到最小权限
GitLab 这些年公布过不少高危漏洞,其中相当一部分涉及未授权访问和 token 泄露。我的态度很明确:不要迷信某个版本的“绝对安全”,要建立一套可持续的安全操作习惯。
“高危漏洞修复方案”落到实操上,核心就是及时升级。GitLab 社区版的发布节奏很快,安全修复会同步进入新的小版本。你应该订阅官方安全公告,或者至少养成每隔一两个月主动升级一次的习惯。升级之前,先完整备份/etc/gitlab/gitlab-secrets.json和数据库,用gitlab-backup create做一个备份验证恢复流程——备份如果没验证过,等于没备份。
账号层面,除了前面说的关闭注册、开启 2FA,还要注意服务账号的权限收敛。创建 Personal Access Token 时,坚持最小权限原则:只给流水线需要的api、read_repository,不要顺手把admin_mode也勾上。Token 一定要有过期时间,我建议最长不超过 90 天,并配置到期的提醒机制。团队管理规范里也应该写清楚:root 账号只能由运维负责人使用,日常操作一律通过普通账号 + token 完成。
5. 实战经验总结与后续扩展方向
5.1 我个人在实战中坚持的几条原则
这套方案跑通不难,但要跑得稳,有几个原则我是踩了坑之后才真正坚定的。
第一,流水线配置和项目代码必须一起管理。流水线 YAML 放仓库里,不是放在 Arbess 平台网页上。这样一次代码回滚,连流水线配置一起回滚,不会出现“代码回滚了但构建配置还是新版”的错位情况。
第二,部署过程一定要分成“传输”和“切换”两步。传输只是把文件放到目标机,切换才是真正影响线上的动作。两步之间隔着一次健康检查,能挡掉大部分部署事故。回滚操作也应该单独做成脚本,跟部署脚本分开,防止部署脚本里回滚逻辑出问题。
第三,任何自动化流程都要有“人可干预”的出口。我保留了一个手动触发按钮,可以让运维在必要时直接选择某个历史版本重新部署。自动化不是把人的介入全部拿掉,而是让人不需要在重复劳动中介入,只在异常场景中介入。
5.2 下一步可以加的料:健康检查、通知与灰度
这套流水线现在能跑,但离“舒服”还有一段距离。我接下来打算做的扩展有三个方向。
一是把健康检查做得更完整。现在的健康检查只确认 Nginx 返回 200,但页面内容是否正常渲染、接口是否能通,这些单靠curl状态码是看不出来的。下一步我会在部署脚本里加一个更细的检查,比如请求首页 HTML 后确认包含预期的<div id="root"></div>标记,至少能确认产物不是空壳。
二是接入通知。流水线跑完,成果要让人知道,失败更要第一时间让人知道。我打算接一个钉钉或飞书群机器人 webhook,在 deploy 阶段的成功和失败节点分别发送消息,带上版本号、部署时间和当前线上健康状态。这样团队就不用每天追着问“发了吗”“好了吗”,机器人直接报结果。
三是灰度发布。现在的软链切换是瞬时全量切换,对内部系统够用,但要给线上用户做项目,还是得有灰度能力。思路是在current之外再加一个canary目录,用 Nginx 的split_clients模块把一定比例的流量引到新版本上,观察一段时间再决定是全量切换还是回滚。这套玩法可以在不引入网关的情况下实现基本灰度,很适合主机部署场景。
从 GitLab 收到 webhook 那一刻起,到 Nginx 切换软链完成,一条自动化部署链路不算复杂,但它解决的是一个非常实际的问题:让 React.js 项目的发布不再依赖人工记忆和手动操作。我真心建议你把这套流程搭起来,哪怕先从最简单的 build + rsync 开始,也比每天手动 ssh 上去发版强得多。