小服务器自动部署

小规格服务器不是不能做自动化部署,而是要先承认资源边界:内存要兜底,SSH 链路要闭环,依赖安装要可重复,生产收尾要可检查。

这篇文章将一份 Laravel 部署笔记改写为公开版:所有真实服务器地址、仓库、目录、用户名和业务路径都已抽象,只保留适合复用的部署原则。它关注的不是某一台机器的命令清单,而是小规格服务器上自动部署最容易断裂的几条链路:资源、SSH、依赖、容器路径和生产收尾。

部署拓扑:把登录链、拉代码链和容器链拆开

Laradock + GitHub Actions 的典型拓扑是:代码托管在 GitHub,生产服务器上有项目目录和 Laradock 目录,Actions 在 push 后通过 SSH 进入服务器,再由服务器拉取目标分支,最后在 Laradock 的 workspacephp-fpm 容器里执行 Composer 与 Laravel 命令。

开发者 push → GitHub Actions Runner → SSH 登录生产服务器
生产服务器 → SSH 拉取 Git 仓库 → Laradock workspace 执行 composer install
Laradock php-fpm / workspace → 重建缓存、修复 storage 权限、完成上线检查

这条链路里有三个边界必须明确:Actions 不直接持有代码仓库写权限之外的业务秘密;服务器不依赖人工进入容器执行发布;Composer 安装必须读取锁文件,避免线上临时解析依赖树。

组件职责需要准备失败信号
GitHub Actions触发部署、登录服务器、输出日志Secrets、workflow、分支触发规则SSH 握手失败或 secret 缺失
生产服务器保存项目目录、执行 Git 拉取部署用户、SSH key、Swap、目录权限publickeynot a git repository
Laradock提供 PHP CLI 与运行环境workspace、php-fpm、volume 映射容器路径不存在或 TTY 报错
Laravel 项目依赖安装、缓存、日志、上传目录composer.lockstorage 可写依赖漂移或运行时 Permission denied

前置检查:小服务器先补资源安全网

中小规格服务器运行 Composer 时最常见的问题不是 YAML 写错,而是内存被耗尽。Swap 不是性能优化,它只是让内核在高峰时有一个缓冲区,避免 Composer 解析依赖或生成 autoloader 时直接被 OOM killer 结束。机器越小,越应该减少线上计算量:能复用锁文件就不重新求解,能跳过无变化依赖就不重复安装。

  • 确认服务器内存、磁盘、Swap 均可用,Swap 大小按业务负载设置为兜底值。
  • 确认部署目录、Laradock 目录和容器内挂载路径一一对应。
  • 确认生产分支策略清晰,例如只允许主干或发布分支触发自动部署。
  • 确认 composer.jsoncomposer.lock 同步提交,线上不临时 update。
  • 确认 Laravel 的 storage、缓存目录、日志目录最终归运行用户可写。

预检阶段还要明确“谁会改服务器上的文件”。如果部署脚本使用强制同步远端分支,那么服务器上的手工修改会被覆盖;这恰恰是自动化部署需要的确定性,但必须提前约定好,避免把生产热修当成常态。

自动化部署不是把命令塞进 YAML,而是把机器限制也写进流程。

SSH 信任链:Actions 登录服务器,服务器拉取代码

一条完整的部署链路通常包含两个方向。第一条是 GitHub Actions Runner 能登录生产服务器:私钥保存在 Actions Secrets,服务器的 authorized_keys 接受对应公钥。第二条是生产服务器能拉取 GitHub 仓库:服务器公钥登记为仓库 Deploy Key,服务器使用对应私钥执行 git fetch

链路 A:Actions Secret 中的部署私钥 → 服务器 authorized_keys → 允许 Runner 登录
链路 B:服务器上的部署公钥 → GitHub Deploy Keys → 允许服务器拉取仓库
位置保存内容用途安全建议
Actions SecretsREMOTE_HOSTREMOTE_USERSSH_PRIVATE_KEY让 Runner 登录服务器只用泛化变量名,不在 workflow 写真实值
服务器 authorized_keys部署公钥允许对应私钥登录权限收紧到 700/600 级别
GitHub Deploy Keys服务器公钥允许服务器拉取仓库默认只读;确需写入再评估写权限
workflow引用 secret 的占位符组织部署步骤不打印私钥、不输出真实主机和仓库名

部署密钥应专用,不复用个人常用登录密钥。若服务器只负责拉取代码,Deploy Key 从只读权限开始即可;只有脚本确实需要从服务器向仓库写入内容时,才评估写权限。公开文章中也不要出现真实主机、真实仓库、真实用户或完整私钥变量值。

Workflow 设计原则:确定性、幂等、可回滚

workflow 的核心目标是把“手工 SSH 发布”拆成可重复步骤:拉取远端状态、判断依赖是否变化、同步代码、在容器里执行必要命令、做生产收尾。小服务器上尤其要避免每次 push 都全量计算依赖,因此可以检测 composer.jsoncomposer.lock 是否变化,再决定是否进入 Composer 步骤。

  1. 触发条件只绑定明确分支,避免任意实验分支误发布。
  2. 登录服务器后进入抽象项目根目录 <app-root>,不存在 Git 元数据时初始化或提前人工 clone。
  3. git fetch 获取远端状态,再比较依赖文件变化,最后同步到目标提交。
  4. 依赖变化时进入 Laradock 目录,在 workspace 容器中执行 Composer。
  5. 根据变更类型选择是否重建 Laravel 配置、路由、视图缓存。
  6. 收尾检查权限、日志、健康页面和关键接口。
BASE_DIR="<app-root>"
LARADOCK_DIR="<laradock-root>"
PROJECTS=("<service-a>" "<service-b>")

git fetch --all
COMPOSER_CHANGED=$(git diff --name-only HEAD origin/<branch> | grep -E 'composer\.(json|lock)' || true)
git reset --hard origin/<branch>

docker-compose exec -T -e COMPOSER_MEMORY_LIMIT=-1 workspace composer install \
--working-dir=/var/www/<app>/<service> \
--no-dev --optimize-autoloader --no-interaction

docker-compose exec -T-T 很重要,因为 GitHub Actions 这类非交互环境没有可用 TTY。Composer 命令应加 --no-interaction,避免流程等待输入。多子项目场景下,用数组或配置列出需要安装依赖的目录,但对外文档只保留抽象路径。

Composer install,而不是线上 update

生产部署应使用 composer install,读取已经提交并测试过的 composer.lockcomposer update 会根据版本范围重新计算依赖树,既耗内存又可能引入未测试版本;这类动作应该在本地或 CI 的测试阶段完成,而不是在小规格生产机上临时执行。

命令做什么适合位置风险
composer install按 lock 文件安装确定版本生产部署、CI 构建lock 缺失或未同步会失败
composer update重新解析并更新依赖版本本地开发、受控升级分支依赖漂移、耗时、耗内存
--no-dev不安装开发依赖生产环境代码若误依赖 dev 包会暴露问题
--optimize-autoloader优化类加载映射生产环境需要在代码同步后执行

推荐闭环是:本地或独立构建环境执行依赖升级,测试通过后提交 composer.jsoncomposer.lock;服务器只做安装与优化。这样即使部署失败,也能根据 Git 提交和 lock 文件定位具体差异。

生产收尾与常见失败表

Laravel 发布的最后一步不是“代码拉完”,而是让运行时状态可用。配置、路由、视图缓存是否需要重建,要根据本次变更判断;storage、缓存和日志目录必须确保 PHP-FPM 运行用户可写。权限修复应收敛到项目目录,不要为了省事对过大的父目录递归授权。

docker-compose exec -T php-fpm php /var/www/<app>/<service>/artisan config:cache
docker-compose exec -T php-fpm php /var/www/<app>/<service>/artisan route:cache
docker-compose exec -T workspace chown -R <runtime-user>:<runtime-group> /var/www/<app>/<service>/storage
错误特征可能原因处理方式
ssh: handshake failedActions 私钥与服务器公钥不匹配,或 .ssh 权限错误核对 secret、authorized_keys 与目录权限
Permission denied (publickey)服务器拉仓库时没有 Deploy Key 权限把服务器公钥加入对应仓库 Deploy Keys
fatal: not a git repository项目目录没有 Git 元数据或路径错了确认 BASE_DIR,必要时重新 clone 或初始化 remote
Process KilledComposer 被 OOM killer 结束检查 Swap、减少 update、使用 install 与锁文件
The directory does not exist宿主机路径与容器内路径不一致核对 Laradock volumes 和 --working-dir
Laravel Permission denied日志、缓存或上传目录不可写修复 storage 和缓存目录归属
  • 服务器 Swap、磁盘、部署目录和 Laradock 容器均已预检。
  • Actions Secrets 只保存抽象变量,workflow 不硬编码敏感值。
  • Deploy Key 默认只读,密钥专用于部署,不复用个人密钥。
  • 生产使用 composer installcomposer.lock,不在线上执行 composer update
  • 所有容器命令带 -T 和非交互参数,路径使用容器内路径。
  • 发布后完成 Laravel 缓存、权限、日志和健康检查。