news 2026/5/5 5:07:48

告别 Shell 脚本:用 Laravel Envoy 实现干净可复用的部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
告别 Shell 脚本:用 Laravel Envoy 实现干净可复用的部署

告别 Shell 脚本:用 Laravel Envoy 实现干净可复用的部署

如果你部署代码有段时间了,很可能某个地方有个叫deploy.sh的文件。

也许一开始只有几行:

gitpull origin main php artisan migrate --force

一两年后,它变成了一堵 bash 墙:各种条件判断、环境标志、几行注释掉的旧代码(来自某次事故),还有一个神秘的sleep 5,没人敢动。

它能跑。
但也很吓人。

Laravel Envoy 就是那种默默解决这个问题的工具:可重复、可读的部署自动化,如果你习惯 SSH 和 shell 命令,用起来还很熟悉。Envoy 让你用干净的、类 PHP 的方式组织部署流程,同时仍然通过 SSH 在真实服务器上运行真实命令,而不是维护又一个巨大的 shell 脚本。

本文将介绍:

  • Laravel Envoy 是什么(以及不是什么)
  • 它如何改进纯 shell 脚本
  • 如何将真实的deploy.sh迁移到Envoy.blade.php
  • 处理多环境(staging、QA、production)
  • 安全特性(确认、钩子、通知)
  • Envoy 如何融入 CI/CD

读完后,你应该能把现有部署流程转换成更干净、可复用、团队更容易理解的东西。

原文链接 告别 Shell 脚本:用 Laravel Envoy 实现干净可复用的部署

为什么纯 shell 脚本会变得痛苦

Shell 脚本很适合快速搞定事情。它们:

  • 无处不在(每台服务器都有/bin/sh
  • 上手简单
  • 适合做胶水工作

问题是:部署往往会膨胀。一个典型的 Laravel 部署脚本经常变成这样:

#!/usr/bin/env bashset-eAPP_DIR=/var/www/myappBRANCH=${1:-main}echo"Deploying branch$BRANCHto$APP_DIR"cd"$APP_DIR"echo"Pulling latest code..."gitfetch origin"$BRANCH"gitreset --hard"origin/$BRANCH"echo"Installing composer dependencies..."COMPOSER_ALLOW_SUPERUSER=1composerinstall--no-dev --prefer-dist --no-interaction --optimize-autoloaderecho"Running migrations..."php artisan migrate --forceecho"Caching config & routes..."php artisan config:cache php artisan route:cacheecho"Restarting queues..."php artisan queue:restartecho"Done!"

这看起来还算温和,但随着需求增长,你开始堆叠:

  • 多环境(staging vs production)
  • 不同环境用不同分支
  • 资源、Horizon、队列、缓存预热的额外步骤
  • 条件判断(“只在生产环境运行这个”、“没有变更就跳过迁移”)
  • 通知(Slack、邮件)
  • 用符号链接和releases/20251208000000文件夹实现零停机发布

你可以继续用 bash 做这些,但最终会重新实现其他工具已经提供的结构:分组、钩子、可复用的片段、任务组合。

这就是 Envoy 的用武之地。

Laravel Envoy 是什么?

Laravel Envoy 是一个小型的 PHP SSH 任务运行器。你在一个叫Envoy.blade.php的文件中用类 Blade 语法定义任务,Envoy 通过 SSH 在远程服务器上执行这些任务。

几个要点:

  • 它不限于 Laravel 应用。你可以用它部署任何能通过 shell 命令管理的东西:Node 应用、静态站点、后台 worker 等。
  • 它在本地(或 CI 中)运行,通过 SSH 连接服务器,就像你在终端里做的一样。
  • 官方支持 macOS 和 Linux;在 Windows 上通常通过 WSL2 使用。

Envoy 提供的不是一个巨大的脚本,而是:

  • @servers— 命名的 SSH 目标
  • @task— 在这些目标上运行的 shell 命令块
  • @story— 组成部署流水线的任务序列
  • @setup— 用于变量、分支逻辑、配置的 PHP 代码
  • 钩子(@before@after@error@success@finished)用于日志和通知等横切关注点
  • 额外功能如确认、并行执行和 Slack 通知

你仍然用你已经熟悉的部署语言写核心逻辑:shell 命令。Envoy 只是给它们加上结构。

在项目中安装 Envoy

你有两个主要选择:全局安装或按项目安装。现在按项目安装通常更干净、更可复现。

1. 通过 Composer 安装 Envoy

在 Laravel 项目根目录:

composerrequire laravel/envoy --dev

这会把 Envoy 添加为开发依赖,并在vendor/bin/envoy暴露其二进制文件。

确认安装成功:

php vendor/bin/envoy --version

2. 创建 Envoy.blade.php

在项目根目录:

touchEnvoy.blade.php

这个文件是所有 Envoy 配置和任务的所在地。Envoy 默认查找这个文件。

添加一个最小示例:

@servers(['web' => 'deploy@your-server']) @task('hello', ['on' => 'web']) echo "Hello from {{ gethostname() }}"; @endtask

运行它:

php vendor/bin/envoy run hello

Envoy 会以 deploy 用户 SSH 到你的服务器并远程执行 echo 命令。

从这里开始,你可以扩展这个文件来匹配你的部署流程。

从 deploy.sh 到 Envoy.blade.php 的逐步迁移

让我们把一个典型的 Laravel 部署脚本转换成 Envoy。

原始 bash 部署脚本

我们从这样的脚本开始:

#!/usr/bin/env bashset-eAPP_DIR=/var/www/myappBRANCH=${1:-main}echo"Deploying branch$BRANCHto$APP_DIR"cd"$APP_DIR"gitfetch origin"$BRANCH"gitreset --hard"origin/$BRANCH"COMPOSER_ALLOW_SUPERUSER=1composerinstall--no-dev --prefer-dist --no-interaction --optimize-autoloader php artisan migrate --force php artisan config:cache php artisan route:cache php artisan queue:restart

它能用,但所有东西都纠缠在一起。没有简单的方法只重新运行其中一部分(比如只运行迁移),或者在不复制修改脚本的情况下在环境间共享片段。

步骤 1:定义服务器

开始我们的Envoy.blade.php

@servers(['web' => 'deploy@your-server'])

你可以定义任意多个命名服务器(如 staging、prod-1、prod-2、workers)。

步骤 2:设置共享变量

Envoy 允许你在任务执行前在@setup块中运行 PHP 代码。这非常适合配置、路径和分支。

@setup $appDir = '/var/www/myapp'; // 如果没传分支则默认为 main:--branch=feature/something $branch = isset($branch) ? $branch : 'main'; @endsetup

注意:运行 Envoy 时可以传递选项如--branch=develop,它们会作为$branch在 Blade 模板中可用。

步骤 3:把部署拆分成任务

不是一个长脚本,我们创建几个专注的任务:

@task('git', ['on' => 'web']) echo "Deploying branch {{ $branch }}"; cd {{ $appDir }}; git fetch origin {{ $branch }}; git reset --hard origin/{{ $branch }}; @endtask @task('composer', ['on' => 'web']) cd {{ $appDir }}; echo "Installing composer dependencies..."; COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader; @endtask @task('migrate', ['on' => 'web']) cd {{ $appDir }}; echo "Running migrations..."; php artisan migrate --force; @endtask @task('optimize', ['on' => 'web']) cd {{ $appDir }}; echo "Caching config & routes..."; php artisan config:cache; php artisan route:cache; @endtask @task('restart-queues', ['on' => 'web']) cd {{ $appDir }}; echo "Restarting queues..."; php artisan queue:restart; @endtask

每个@task就是 bash,但分组和命名得很好。

步骤 4:把任务组合成 “story”

story 是按顺序运行的任务序列。这是定义部署流水线的地方。

@story('deploy') git composer migrate optimize restart-queues @endstory

现在完整部署只需要:

php vendor/bin/envoy run deploy --branch=main

如果你需要只运行其中一部分(比如只运行迁移):

php vendor/bin/envoy run migrate

到这里,我们已经复现了原始脚本——但它更干净、可组合、更容易修改。

干净地支持多环境

真实部署几乎总是涉及至少 staging 和 production。让我们扩展 Envoy 设置来支持这一点。

1. 声明多个服务器

@servers([ 'staging' => 'deploy@staging.example.com', 'prod' => 'deploy@prod.example.com', ])

2. 用 --server 选项选择环境

我们在@setup中添加一些 PHP 逻辑来要求--server参数,并为每个环境选择正确的分支:

@setup if (! isset($server)) { throw new Exception("Please pass --server=staging or --server=prod"); } $appDir = '/var/www/myapp'; // 环境与分支的映射 $branches = [ 'staging' => 'develop', 'prod' => 'main', ]; if (! array_key_exists($server, $branches)) { throw new Exception("Unknown server '{$server}'. Allowed: staging, prod."); } $branch = isset($branch) ? $branch : $branches[$server]; @endsetup

Envoy 会把 CLI 选项如--server=staging传入$server,所以我们可以在任务和配置中使用它。

3. 让任务感知环境

我们现在可以定义on参数是动态的任务:

@task('deploy-code', ['on' => $server]) echo "Deploying {{ $branch }} to {{ $server }}"; cd {{ $appDir }}; git fetch origin {{ $branch }}; git reset --hard origin/{{ $branch }}; @endtask @task('run-migrations', ['on' => $server]) cd {{ $appDir }}; php artisan migrate --force; @endtask @task('optimize', ['on' => $server]) cd {{ $appDir }}; php artisan optimize:clear; @endtask @story('deploy') deploy-code run-migrations optimize @endstory

同一个 Envoy story 现在适用于任何环境:

# 部署到 staging(使用 develop 分支)php vendor/bin/envoy run deploy --server=staging# 部署到 prod(使用 main 分支)php vendor/bin/envoy run deploy --server=prod# 如果确实需要,可以覆盖分支php vendor/bin/envoy run deploy --server=staging --branch=feature/checkout-redesign

注意我们没有重复任务。只有配置(哪个服务器、哪个分支)根据输入变化。

添加安全措施:确认、钩子和通知

Envoy 自带一些有用的"护栏",用纯 shell 脚本很难做得这么好。

1. 确认危险任务

对于可能搞坏东西的任务(如部署到生产环境),你可以让 Envoy 提示确认:

@task('deploy-prod', ['on' => 'prod', 'confirm' => true]) cd {{ $appDir }}; git fetch origin {{ $branch }}; git reset --hard origin/{{ $branch }}; php artisan migrate --force; @endtask

使用'confirm' => true,Envoy 会在运行任务前显示"确定吗?"风格的提示。

2. 钩子:@before、@after、@error、@success、@finished

钩子让你在每个任务周围插入行为,而不用重复自己。Envoy 在本地以 PHP 执行这些,不是在服务器上。

例如,简单的日志:

@before echo "About to run task: {$task}"; @endbefore @after echo "Finished task: {$task}"; @endafter @error echo "Task {$task} failed!"; @enderror @success echo "All tasks completed successfully!"; @endsuccess

或者在部署完成后发送 Slack 通知:

@finished if ($exitCode === 0) { @slack('https://hooks.slack.com/services/XXX/YYY/ZZZ', '#deployments') } @endfinished

Envoy 的@slack指令通过 webhook 向你选择的频道或用户发送消息,非常适合"deployments"或"ops"频道。

对比用纯 bash 做同样的事情,尤其是当你想在每个任务周围都加上这些而不只是在最后。

部署到多台服务器(可选并行)

如果你的应用在负载均衡器后面的多台 web 服务器上运行,你可能需要部署到所有服务器。

Envoy 让这变得非常简单:

@servers([ 'web-1' => 'deploy@web1.example.com', 'web-2' => 'deploy@web2.example.com', ]) @setup $appDir = '/var/www/myapp'; $branch = isset($branch) ? $branch : 'main'; @endsetup @task('deploy-code', ['on' => ['web-1', 'web-2']]) cd {{ $appDir }}; git fetch origin {{ $branch }}; git reset --hard origin/{{ $branch }}; @endtask

默认情况下,Envoy 串行运行任务:完成 web-1,然后转到 web-2。如果你想加快速度,并且确信部署可以安全地并发运行,可以启用并行执行:

@task('deploy-code', ['on' => ['web-1', 'web-2'], 'parallel' => true]) cd {{ $appDir }}; git fetch origin {{ $branch }}; git reset --hard origin/{{ $branch }}; @endtask

只需改一行就能从串行变成跨多台服务器的并行部署——这在 bash 中当然也能做到,但远没有这么方便。

迈向零停机:releases 和符号链接

对于很多应用,"原地 git pull"就够了。但一旦你关心零停机部署,通常会开始使用这样的模式:

  • /var/www/myapp/releases/20251208090000(新发布目录)
  • /var/www/myapp/current(指向活动发布的符号链接)
  • /var/www/myapp/shared(共享存储 .env、uploads 等)

Envoy 非常适合用可读的方式编码这种模式。

这是一个简化的例子:

@setup $appDir = '/var/www/myapp'; $releasesDir = $appDir . '/releases'; $currentDir = $appDir . '/current'; $branch = isset($branch) ? $branch : 'main'; date_default_timezone_set('UTC'); $release = date('YmdHis'); $newReleaseDir = $releasesDir . '/' . $release; @endsetup @story('deploy-zero-downtime') prepare-release install-dependencies migrate optimize switch-symlink restart-queues @endstory @task('prepare-release', ['on' => 'prod']) echo "Creating new release: {{ $newReleaseDir }}"; mkdir -p {{ $newReleaseDir }}; cd {{ $newReleaseDir }}; git clone --depth=1 --branch={{ $branch }} git@github.com:your-org/your-repo.git .; @endtask @task('install-dependencies', ['on' => 'prod']) cd {{ $newReleaseDir }}; COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader; @endtask @task('migrate', ['on' => 'prod']) cd {{ $newReleaseDir }}; php artisan migrate --force; @endtask @task('optimize', ['on' => 'prod']) cd {{ $newReleaseDir }}; php artisan config:cache; php artisan route:cache; @endtask @task('switch-symlink', ['on' => 'prod']) echo "Switching symlink to {{ $newReleaseDir }}"; ln -nfs {{ $newReleaseDir }} {{ $currentDir }}; @endtask @task('restart-queues', ['on' => 'prod']) cd {{ $currentDir }}; php artisan queue:restart; @endtask

这基本上就是用 Envoy 写的经典"Capistrano 风格"发布流程。有些细节你可能想完善(共享存储、.env 文件符号链接、清理旧发布),但结构读起来像一个故事:

  1. 准备发布目录
  2. 安装依赖
  3. 运行迁移
  4. 优化
  5. 把 current 符号链接指向新发布
  6. 重启队列

因为每个步骤都是独立的任务,如果需要调试,你也可以单独重新运行某个步骤。

将 Envoy 集成到 CI/CD

到目前为止,我们假设你从笔记本电脑运行 Envoy,这已经比临时 SSH 登录好很多了。但 Envoy 也能很好地配合 CI/CD 系统。

核心上,CI 只需要:

  1. 能访问你的 SSH 密钥(或某个部署密钥)
  2. 检出你的仓库
  3. 运行php vendor/bin/envoy run deploy ...

例如,一个非常简单的 GitHub Actions 工作流可能是这样:

name:Deployon:workflow_dispatch:push:branches:-mainjobs:deploy:runs-on:ubuntu-lateststeps:-name:Checkout codeuses:actions/checkout@v4-name:Setup PHPuses:shivammathur/setup-php@v2with:php-version:'8.3'-name:Install dependenciesrun:composer install--no-dev--prefer-dist--no-interaction-name:Configure SSHrun:|mkdir -p ~/.ssh echo "$DEPLOY_KEY" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan your-server.com >> ~/.ssh/known_hosts-name:Run Envoy deployrun:php vendor/bin/envoy run deploy--server=prodenv:DEPLOY_KEY:${{secrets.DEPLOY_KEY}}

如果你想要更干净的抽象,还有专门用于运行 Envoy stories 的社区 GitHub Action。

这把你的部署从"某人手动运行的 CLI 命令,可能忘了记录"变成"存在版本控制中的可重复工作流,可以在推送时或手动触发"。

什么时候不用 Envoy

Envoy 是一个"刚刚好"的工具:

  • 它比完整的 CI/CD 平台加单独的部署工具更简单
  • 它比一次性 shell 脚本或随机命令更有结构

但是,有些时候它可能不是正确的选择:

  • 你已经深度使用另一个部署系统(如 Laravel Forge、Envoyer、基于 Kubernetes 的部署,或平台特定的流水线)
  • 你的团队更喜欢用 PHP 以外的语言做运维工具,想要一切都用 Go 或 Python
  • 你的基础设施太复杂,需要超出 SSH 任务运行器的编排功能

但如果你处于很常见的情况——“我们 SSH 到几台 VPS 运行命令来部署"或"我们有个不太信任的 bash 脚本”——Envoy 是很自然的升级。

在真实项目中使用 Envoy 的实用技巧

最后,这里有一些通常效果不错的模式和习惯:

把 Envoy.blade.php 放在仓库里

把它当作应用代码。代码审查你的部署变更。如果部署流程变了,应该在 git 历史中可见。

用意图命名任务,而不是实现

使用deploywarm-cacherestart-workersrollback这样的名字,而不是task1step2等。这让你的 stories 读起来像真正的文字。

用 stories 作为你的"公共 API"

人们运行envoy run deployenvoy run rollback。内部,这些 stories 可以由更小的任务组成,你可以自由重组而不改变人们与部署交互的方式。

快速失败并大声报错

在合理的地方使用set -e风格的行为(Envoy 已经暴露退出码并支持@error钩子)。不要静默吞掉错误。如果迁移失败,你希望部署停止并报错。

明确环境

--server(或--env)成为必需的。如果没传环境,永远不要默认到生产环境。强制调用者明确意图。

从小处开始,然后重构

你不需要在第一天就有完美的零停机、多阶段、多服务器流水线。先把你当前的步骤包装成 Envoy 任务和一个 story。稳定后,再添加环境、钩子和更高级的流程。

总结

你不用抛弃所有关于部署的知识才能获得更干净的自动化。

Laravel Envoy 让你:

  • 继续写你已经在用的 shell 命令
  • 把它们包装在 Blade 风格的、PHP 驱动的配置文件中
  • 与应用代码一起分享和版本控制这个文件
  • 添加结构:命名任务、stories、钩子和环境
  • 安全地从"VPS 上的一台服务器"扩展到"多服务器、零停机部署"

用纯 bash 脚本做所有事情可以吗?当然可以。但随着项目——和团队——的增长,有一个读起来几乎像文档、同时又是可执行代码的部署流程是非常有价值的。

如果你有个让人紧张的deploy.sh,试试这个:

  1. 在项目中安装 Envoy
  2. 把现有命令包装成@task
  3. 把它们组合成@story('deploy')
  4. 添加一个环境标志和一个钩子

从那里开始,你可以逐步迭代成健壮、可读、可复用的东西——超越 shell 脚本,但不放弃你已经熟悉的工具。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 9:20:07

如何快速掌握计算机体系结构:量化研究方法的完整指南

如何快速掌握计算机体系结构:量化研究方法的完整指南 【免费下载链接】体系结构量化研究方法第六版电子书下载 《体系结构:量化研究方法》第六版是计算机体系结构领域的经典教材,由2018年图灵奖得主撰写,全面深入地介绍了计算机体…

作者头像 李华
网站建设 2026/5/3 12:47:32

如何快速构建高性能HTTP服务器:httpserver.h新手完整指南

如何快速构建高性能HTTP服务器:httpserver.h新手完整指南 【免费下载链接】httpserver.h httpserver.h - 一个单头文件C库,用于构建事件驱动的非阻塞HTTP服务器。 项目地址: https://gitcode.com/gh_mirrors/ht/httpserver.h 想要在C语言项目中快…

作者头像 李华
网站建设 2026/5/3 18:23:04

现代C++工程实践:简单的IniParser3——改进我们的split

目录 前言 下面这个改进对吗 关键问题: substr() 返回的是新的 std::string 第二版:问题是如何被修复的? 修复的核心点:使用原始 src 构造 string_view 作为根 1. substr() 变成了 "视图切片",不是 &qu…

作者头像 李华
网站建设 2026/5/4 18:09:09

重新定义个人知识管理:note-gen应用深度体验指南

重新定义个人知识管理:note-gen应用深度体验指南 【免费下载链接】note-gen 一款专注于记录和写作的跨端 AI 笔记应用。 项目地址: https://gitcode.com/GitHub_Trending/no/note-gen 在信息爆炸的时代,如何高效地收集、整理和创作知识成为每个现…

作者头像 李华
网站建设 2026/4/29 20:45:50

CANN Samples(十八):最佳实践与行业案例

1. 从“知道”到“做到”:探寻最佳实践的价值 在上一篇文章中,我们绘制了一幅从初级到高级的CANN开发成长地图。然而,地图只是指引,真正的风景需要用脚步去丈量。理论知识学得再多,如果不能应用到实际项目中&#xff0…

作者头像 李华