
September 07, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
You deploy a Laravel 13 application, run migrations, clear caches, and restart PHP-FPM—then repeat the same five SSH commands on three servers every release. Laravel Envoy for remote task automation replaces that copy-paste routine with a version-controlled task file you run from your laptop or CI runner. Envoy is not a full deployment platform like Deployer; it is a lightweight SSH task runner with Blade-style syntax that fits teams who want readable automation without standing up a pipeline first. This guide covers installation, real Envoy.blade.php patterns, production gotchas I've hit on client servers, and when Envoy beats—or loses to—Deployer and GitLab CI.
Envoy.blade.php file using @servers, @task, and @story directives—ideal for small teams that want version-controlled deploy scripts without a full CI/CD stack.What is Laravel Envoy and how does remote task automation work?
Envoy ships as a standalone Composer package. It reads a task definition file—conventionally Envoy.blade.php at your project root—and executes named tasks on remote hosts over SSH. Unlike Laravel scheduled tasks in production, which run inside the application on a cron schedule, Envoy runs from your local machine or a build server and pushes commands outward.
The mental model is simple: you declare servers, define tasks as shell blocks, and optionally chain tasks into stories. Envoy handles parallel execution across servers, confirmation prompts, and variable interpolation. For teams already comfortable with modern Laravel architecture, Envoy feels familiar because the syntax mirrors Blade.
Envoy sits between ad-hoc shell scripts and heavyweight orchestration. You get structured, named tasks without Kubernetes, without writing a custom bash wrapper, and without committing your entire release process to a SaaS CI vendor on day one. On projects where I also maintain build automation pipelines, Envoy often handles the "last mile" SSH steps that CI cannot reach without extra configuration.
Core directives you will use daily
- @servers — maps logical names to SSH connection strings (
user@host:port). - @setup — PHP block executed once before tasks; ideal for reading
.envvalues or setting branch names. - @task — a named unit of shell commands run on one or more servers.
- @story — chains multiple tasks in order, with optional confirmation between steps.
- @finished — hook that runs after a story completes, useful for Slack notifications.
Envoy is officially documented in the Laravel Envoy documentation. It works with any PHP project that has Composer—not only Laravel—though Laravel teams adopt it fastest because the Blade-like syntax matches their daily workflow.
How do you install and configure Laravel Envoy for production servers?
Install Envoy globally or per-project. For Laravel 13 on PHP 8.3 or higher, both approaches work; I prefer a project-level dev dependency so the whole team runs the same Envoy version via Composer 2.10.
Step 1: Install the package
composer require laravel/envoy --dev Or install globally:
composer global require laravel/envoy
export PATH="$PATH:$HOME/.composer/vendor/bin" Verify:
./vendor/bin/envoy --version Step 2: Create Envoy.blade.php
Place this file at your repository root alongside composer.json:
@servers(['web' => 'deploy@203.0.113.10', 'worker' => 'deploy@203.0.113.11'])
@setup
$repository = 'git@github.com:your-org/your-app.git';
$appDir = '/var/www/your-app';
$branch = $_ENV['DEPLOY_BRANCH'] ?? 'main';
@endsetup
@task('ping', ['on' => 'web'])
hostname && php -v
@endtask Run a smoke test before touching production:
./vendor/bin/envoy run ping Step 3: Configure SSH for unattended runs
Envoy uses your system's SSH client. Production automation fails when keys are missing, passphrases prompt interactively, or known_hosts rejects new fingerprints. Fix this before writing deploy tasks.
- Generate a deploy key:
ssh-keygen -t ed25519 -C "deploy@your-app" -f ~/.ssh/your_app_deploy - Add the public key to the server's
~deploy/.ssh/authorized_keys. - Create a host alias in
~/.ssh/configwithIdentityFilepointing to that key. - Pre-populate
known_hosts:ssh-keyscan -H 203.0.113.10 >> ~/.ssh/known_hosts
For CI runners, inject the private key as a masked variable and write it to disk at pipeline start—same pattern used in build pipeline automation best practices. On Ubuntu 22/24 servers I administer through Linux system administration services, I restrict the deploy user with sudo rules only for systemctl reload php8.3-fpm and supervisorctl restart, never full root.
How do you write Envoy tasks for common Laravel deployment workflows?
A realistic Laravel deploy story pulls code, installs dependencies, runs migrations, rebuilds caches, and reloads PHP-FPM so opcache picks up changes—a pattern I repeat across Laravel booking platforms and legal-tech portals. Here is a production-oriented Envoy.blade.php for a single-server symlink-free layout:
@servers(['web' => 'production'])
@setup
$appDir = '/var/www/myapp/current';
$php = '/usr/bin/php8.3';
@endsetup
@task('deploy', ['on' => 'web', 'confirm' => true])
cd {{ $appDir }}
git fetch origin
git reset --hard origin/{{ $branch }}
{{ $php }} /usr/local/bin/composer install \
--no-dev --prefer-dist --no-interaction --optimize-autoloader
{{ $php }} artisan migrate --force
{{ $php }} artisan config:cache
{{ $php }} artisan route:cache
{{ $php }} artisan view:cache
{{ $php }} artisan event:cache
sudo systemctl reload php8.3-fpm
@endtask
@story('release')
deploy
@endstory Run the full story:
DEPLOY_BRANCH=main ./vendor/bin/envoy run release Multi-server patterns
When web and queue workers live on separate boxes, target each role explicitly:
@task('deploy-app', ['on' => ['web-1', 'web-2']])
cd {{ $appDir }} && git pull origin {{ $branch }}
{{ $php }} artisan migrate --force
@endtask
@task('restart-workers', ['on' => 'worker'])
sudo supervisorctl restart laravel-worker:*
@endtask
@story('full-release')
deploy-app
restart-workers
@endstory Envoy runs deploy-app on web-1 and web-2 in parallel, then executes restart-workers on the worker host. For zero-downtime symlink releases—the approach I use with Deployer 7 on sister sites like Notary Kathmandu—point $appDir at the current symlink and keep shared storage/ and .env outside the release directory.
Variables, macros, and conditional tasks
The @setup block is plain PHP. Read environment variables, parse .env, or set defaults:
@setup
$branch = $_ENV['DEPLOY_BRANCH'] ?? 'main';
$runMigrations = filter_var(
$_ENV['RUN_MIGRATIONS'] ?? 'true',
FILTER_VALIDATE_BOOLEAN
);
@endsetup
@if ($runMigrations)
@task('migrate', ['on' => 'web'])
cd {{ $appDir }} && {{ $php }} artisan migrate --force
@endtask
@endif Use @macro for reusable PHP helpers—handy when you need to resolve the latest Git tag or format a Slack payload. Keep macros small; complex logic belongs in a dedicated Artisan command that Envoy invokes, which keeps tasks testable through php artisan locally.
Integrating with Vite and frontend assets
On servers without Node.js 26 LTS installed—a common constraint on budget hosting—I build assets in CI and commit the compiled public/build directory, then let Envoy pull and cache. If your pipeline runs npm run build via Vite 8.x, add a pre-deploy CI step rather than installing Node on production. See Vite config for Laravel projects for the build side; Envoy handles only the SSH transfer and cache warming on the server.
How does Laravel Envoy compare to Deployer and CI/CD pipelines?
Teams often ask whether Envoy replaces Deployer, GitLab CI, or GitHub Actions. It does not replace all three—it occupies a specific slot. Envoy is a task runner; Deployer is a deployment framework with release management, rollbacks, and shared-directory conventions built in; CI/CD orchestrates tests, builds, and gated promotions across environments.
| Criteria | Laravel Envoy | Deployer 7 | GitLab CI / GitHub Actions |
|---|---|---|---|
| Primary role | SSH task runner with Blade syntax | Zero-downtime deployment framework | Full pipeline: test, build, deploy, notify |
| Rollback support | Manual—write your own task | Built-in dep rollback | Depends on deploy strategy |
| Release directories | You implement symlinks yourself | Native symlinked releases | Varies by recipe/script |
| Learning curve | Low—one file, few directives | Medium—recipe PHP + host config | Medium—YAML + runner setup |
| Best fit | Solo devs, small teams, maintenance tasks | Production Laravel apps needing atomic deploys | Teams needing automated tests before deploy |
| Runs from | Local machine or CI SSH step | Local or CI via dep deploy | Cloud runner with repo access |
In practice I combine them. GitLab CI runs PHPUnit, PHPStan, and npm run build; on success it calls dep deploy production for sites that need atomic releases. For smaller WordPress or Laravel sites on shared hosting—common among Laravel development projects in Nepal—Envoy alone is enough: one envoy run deploy from a trusted laptop after manual QA passes.
Deployer's documentation at deployer.org covers rollback and shared directories in depth. If your app serves paying customers and downtime costs money, Deployer or a similar release manager earns its place. If you need to restart queue workers across three servers at 2 AM after a config change, Envoy is the faster tool to write and read.
What are common Laravel Envoy mistakes on real production servers?
Envoy fails quietly or loudly depending on the mistake. These are the issues I troubleshoot most often during support and maintenance engagements.
Wrong PHP binary or stale opcache
A server with PHP 8.3 and 8.5 side-by-side will run the wrong version if you call bare php artisan. Always use the full path—/usr/bin/php8.3—matching your FPM pool. After deploy, reload PHP-FPM; otherwise opcache serves old bytecode and your "successful" deploy shows stale code. This is the same class of problem covered in replacing messy crontabs with Laravel scheduling, where the cron entry points at the wrong binary.
Running migrations on every web node
When deploy-app targets multiple web servers, running artisan migrate --force on each causes race conditions. Restrict migrations to one host:
@task('deploy-code', ['on' => ['web-1', 'web-2']])
cd {{ $appDir }} && git pull origin {{ $branch }}
{{ $php }} /usr/local/bin/composer install --no-dev -o
@endtask
@task('migrate', ['on' => 'web-1'])
cd {{ $appDir }} && {{ $php }} artisan migrate --force
@endtask Missing confirmation on destructive tasks
Add 'confirm' => true to production deploy tasks so Envoy prompts before execution. Skip confirmation only in CI by passing --no-interaction when you intend unattended runs. Destructive one-offs—artisan db:wipe, mass cache flushes during peak hours—deserve their own task name and a human gate.
Secrets in Envoy.blade.php
Never hardcode API keys or database passwords in the task file. Read from the server's existing .env or inject via CI variables at runtime. If you need to validate JSON config during deploy, pipe output through a local JSON formatter tool for debugging—do not echo secrets into Slack notifications from @finished hooks.
Forgetting file permissions after git pull
git pull as the deploy user can leave storage/ and bootstrap/cache/ owned incorrectly, breaking uploads and cache writes. Add an explicit fix:
chmod -R ug+rwx storage bootstrap/cache
chgrp -R www-data storage bootstrap/cache On legal-tech portals like Court Marriage In Nepal, document upload failures from bad permissions are a support ticket I can prevent with one Envoy line.
No deployment lock
Envoy has no built-in mutex. Two developers running envoy run release simultaneously can interleave git operations. Use a simple flock on the server or restrict deploys to CI only:
@task('deploy', ['on' => 'web'])
flock -n /tmp/deploy.lock -c '
cd {{ $appDir }} && git pull origin {{ $branch }}
' || exit 1
@endtask Key Takeaways
- Install Envoy per-project with
composer require laravel/envoy --devso the team shares one version via Composer 2.10. - Configure SSH keys, host aliases, and
known_hostsbefore writing deploy tasks—most Envoy failures are SSH problems, not Laravel problems. - Use
@storyto chain git pull, Composer, migrate, cache, and FPM reload; run migrations on a single server when you have multiple web nodes. - Call PHP by full path (
/usr/bin/php8.3) on servers with multiple PHP versions installed side-by-side. - Pair Envoy with CI for tests and asset builds; use Deployer when you need atomic releases and one-command rollbacks.
- Add confirmation prompts and flock locks so two operators cannot deploy over each other.
People Also Ask
Does Laravel Envoy work with Laravel 13?
Yes. Envoy is a standalone Composer package and is not tied to a specific Laravel framework version. It works alongside Laravel 13 on PHP 8.3 or higher. Your Envoy.blade.php lives at the project root and invokes standard artisan commands regardless of framework minor version.
Can Laravel Envoy replace GitHub Actions or GitLab CI?
Not entirely. Envoy executes SSH tasks; it does not run your test suite, lint code, or build frontend assets unless you shell out to those commands manually. The practical setup runs tests and Vite builds in CI, then triggers Envoy or Deployer for the SSH deploy step. Envoy replaces repetitive manual SSH sessions, not a full pipeline.
How do you run Laravel Envoy from GitLab CI?
Add a deploy stage that loads an SSH private key, installs Envoy via Composer, and runs vendor/bin/envoy run release --no-interaction. Store the key as a masked CI variable, run ssh-keyscan in before_script, and restrict the deploy key to the target server only. This mirrors the GitLab CI plus Deployer pattern I use on production sites.
Is Laravel Envoy the same as Envoy Proxy?
No. Laravel Envoy is a PHP task runner for SSH automation. Envoy Proxy is a CNCF service mesh and edge proxy for Kubernetes and microservices—they share a name only. If you need service mesh routing, that is a different tool entirely; see infrastructure documentation at envoyproxy.io for the proxy product.
Ship reliable deploys without reinventing your workflow
Laravel Envoy for remote task automation earns its place in any team that still SSHs into servers for releases. Start with a ping task, grow into a full release story, and graduate to Deployer plus CI when atomic rollbacks and automated tests become non-negotiable. The goal is not the fanciest toolchain—it is deploys you can repeat at midnight without guessing which PHP binary or cache command you forgot.
If you want help wiring Envoy, Deployer 7, or GitLab CI into a production Laravel stack—with correct permissions, opcache handling, and SEO-safe deploy practices—web development services cover the full path from task file to live server. For ongoing ops after launch, hosting and domain setup and custom software development keep the pipeline maintainable. Learn more about my background, review Laravel client portal work, or contact us to audit your current deploy process.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

