如何在 Ubuntu 上为 Laravel + Vite 项目实现原子发布?
结论
可以让新版本在独立目录中构建,完成检查后再切换 current 软链接。GitHub Actions 仍可调用 production.sh,由脚本完成部署和切换。
新版本构建期间,旧版本可以继续提供服务,通常不必为了执行 Vite 构建而开启维护模式。目录切换只能保证文件发布的原子性;要减少用户可见的错误,还需要处理数据库变更、静态资源和常驻进程。
为什么现有流程可能导致网站报错?
如果直接在正在提供服务的目录中执行 git pull、安装依赖和构建 Vite,请求可能读到尚未准备完整的新版本。例如,代码已经更新,对应的资源或依赖却还没有生成。
原问题没有提供错误日志,因此无法确定具体原因。可以检查 Laravel 日志,以及浏览器中失败的 JS、CSS 请求,确认是否存在 manifest 缺失、资源 404 或依赖加载错误。
1. 将每次发布放入独立目录
可以采用下面的结构:
/srv/myapp/
├── current -> releases/release-002
├── releases/
│ ├── release-001/
│ └── release-002/
└── shared/
├── .env
└── storage/
每个 release 都包含自己的应用代码、vendor 和前端构建产物。生产配置和需要持久保存的数据放在 shared 中,通过软链接接入版本目录。
持久数据和生成缓存需要区别处理:上传文件、日志、文件型 session 等需要跨版本保留;bootstrap/cache 应由各版本独立管理。如果共享整个 storage,还要检查 Blade 编译缓存的配置,避免准备新版本时清理或覆盖旧版本仍在使用的缓存。
部署前需要准备好目录和权限。Laravel 运行时需要能够写入 storage 和 bootstrap/cache,具体要求可参考 Laravel 部署文档。
2. 在新目录内完成构建
建议让 GitHub Actions 传递本次部署的 commit SHA,使后端代码、依赖和前端资源都对应同一次提交,便于追踪和回滚。
新 release 包含目标代码,并完成 .env、持久存储和权限配置后,可以执行:
# 在新 release 目录中运行
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
npm ci
npm run build
php artisan config:cache
这里假设项目使用 npm,并已提交 package-lock.json。如果使用其他包管理器,应改成对应的锁文件安装命令。
npm run build 用于生成生产资源,Laravel 的 Vite 集成会加载构建后的文件,详见 Laravel Vite 文档。生产产物不应带入开发服务器使用的 hot 文件。
也可以在 GitHub Actions 中完成构建,再把产物上传到服务器。构建环境需要满足项目要求,构建时还要提供必要的 VITE_* 配置。这些变量可能进入浏览器代码,不能包含服务端秘密。
切换前至少检查:
- 应用能够启动,必要的配置和依赖齐全。
- Vite manifest 存在,其中引用的资源均已生成;检查路径以项目配置为准。
- 新版本通过项目现有的测试或预发布检查。
任何准备步骤失败时,都应终止部署,让 current 继续指向旧版本。
3. 原子切换 current
下面的 Bash 示例只负责切换到已经准备好的 release,不包含构建、数据库迁移或健康检查。假设服务器使用 Ubuntu 常见的 GNU 工具,current 已经是软链接,临时链接与它位于同一个文件系统中。
#!/usr/bin/env bash
set -euo pipefail
BASE=/srv/myapp
RELEASE=$(readlink -f -- "${1:?Usage: switch-release.sh RELEASE_DIR}")
# 防止多个切换操作互相覆盖
exec 9>"$BASE/.switch.lock"
flock -n 9
case "$RELEASE" in
"$BASE"/releases/*) ;;
*) echo "Release must be under $BASE/releases" >&2; exit 1 ;;
esac
test -f "$RELEASE/public/index.php"
test -L "$BASE/current"
NEXT="$BASE/.current.$$"
trap 'rm -f -- "$NEXT"' EXIT
ln -s -- "$RELEASE" "$NEXT"
mv -Tf -- "$NEXT" "$BASE/current"
先创建临时软链接,再用同一文件系统内的 rename 操作替换 current。如果先删除 current 再重新创建,中间会出现入口不存在的时间窗口。
完整的 production.sh 还应对整个部署过程加锁,避免两次 GitHub Actions 部署同时构建、迁移或清理版本。示例中的锁仅保护切换操作。
如果使用 Nginx + PHP-FPM,站点根目录应指向:
root /srv/myapp/current/public;
还应检查 PHP location 中传给 PHP-FPM 的脚本路径。Laravel 官方 Nginx 示例使用:
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
这些是现有站点配置中的相关片段,完整配置及 PHP-FPM socket 路径仍需按服务器环境调整。可参考 Laravel 的 Nginx 配置示例。
4. 保留旧页面仍然需要的静态资源
原子切换后,浏览器可能还持有旧页面,或者稍后才请求旧版本的动态导入文件。如果旧资源 URL 只会访问新 release,而新目录没有对应文件,仍然会出现 404。
因此,除了保留旧 release 目录,还要确保旧资源 URL 继续可访问。常见做法包括:
- 将带内容 hash 的资源发布到持久静态资源目录或 CDN,新版本只追加资源,并延迟清理旧资源。
- 为不同 release 使用不同的资源 URL 前缀,让旧页面继续访问自己的资源版本。
无论采用哪种方式,都要先发布资源,再切换应用。manifest 应与对应的应用版本保持一致。不要把多个 release 的整个构建目录共用后,再让构建命令直接清空该目录。
5. 数据库、进程和回滚需要单独处理
数据库迁移应允许新旧代码短暂共存
如果发布需要执行迁移,应先确认旧代码能够在迁移后的数据库结构上继续运行。通常可以先增加新字段或新表,发布兼容代码并完成数据迁移,之后再单独删除旧结构。
不要把删除旧字段等破坏性迁移直接当作切换前的固定步骤。目录软链接无法回滚数据库;涉及不兼容变更时,仍可能需要维护窗口或其他发布方案。
常驻进程需要加载新代码
如果项目运行 queue worker、Horizon、Octane 或 SSR 服务,应在切换后按实际版本和进程管理方式执行平滑重启,并确认进程管理器会从新 release 启动进程。
PHP-FPM 是否需要 reload 取决于 OPcache 和路径缓存配置,应在预发布环境验证。Laravel 官方也提醒部署后需要处理仍驻留旧代码的长运行服务,见 服务重载说明。
切换后检查,失败时恢复旧版本
发布完成后,应检查实际 HTTP 请求、关键业务接口及静态资源,确认服务正常,而不只是判断脚本是否正常退出。切换前记录旧 release 的真实路径;检查失败时,可用相同的软链接替换方式恢复旧版本,并同步处理常驻进程。
保留一定数量的历史版本,等旧请求和任务结束后再清理。若数据库已经发生不兼容变更,仅恢复代码目录可能无法恢复服务。
对于现有流程,可以先让 production.sh 在新目录中完成准备工作,成功后再切换入口。随后补齐资源保留、兼容迁移和健康检查,处理实际访问过程中可能出现的问题。
备注:内容仅供参考。
