exolnet/laravel-envoy
Exolnet Laravel Envoy integrates Laravel Envoy into your application, making it easier to organize and run remote server tasks and deployment stories from a familiar Laravel-style setup. Simplify repeatable server workflows with convenient, framework-integrated configuration.
This package is a Capistrano-style deployment template for Laravel Envoy — it doesn't add runtime code to your app, it gives you a battle-tested Envoy.blade.php that implements the release/symlink/shared-folder deployment model.
Minimal setup:
Install Envoy globally (local machine only — it drives SSH):
composer global require laravel/envoy
Pull in the template package as a dev dependency:
composer require --dev exolnet/laravel-envoy
Publish (or copy) the Envoy.blade.php to your project root. Depending on the tag the package exposes, this is usually:
php artisan vendor:publish --provider="Exolnet\Envoy\EnvoyServiceProvider"
Check vendor/exolnet/laravel-envoy/ — if there's no service provider, just copy its Envoy.blade.php into your repo root and commit it.
Open Envoy.blade.php and edit the @setup block first — this is the only place you should need to touch initially:
$repository — git URL the remote will clone$branch — main / master$servers — host + SSH user$deployPath — e.g. /var/www/myapp$keepReleases — how many releases to retain (default ~5)Prepare the server once (not automated by Envoy):
mkdir -p /var/www/myapp/{releases,shared}
# seed shared/.env and shared/storage before the first deploy
First deployment:
envoy run deploy
First useful scenario: a single-box staging deploy where current is a symlink into releases/ and shared/.env + shared/storage are symlinked into every release. That gives you atomic-ish cutover and instant rollback.
The composite deploy task. The template mirrors Capistrano's pipeline, so day-to-day you'll add custom tasks between the built-in steps rather than rewriting the flow:
deploy:prepare → deploy:update_code → deploy:shared → deploy:vendors
→ deploy:migrate → deploy:symlink → deploy:cleanup
Hook in by editing the deploy composite task to call your own tasks (e.g. deploy:queue:restart, deploy:horizon:terminate, deploy:assets).
Shared-file linking. Anything that must survive releases belongs in shared/ and gets symlinked into the release. Typical additions beyond .env/storage:
public/uploadspublic/storagebootstrap/cache (careful — permissions)Add symlinks in the deploy:shared section using the same ln -nfs pattern the template uses.
Multi-environment deploys. Envoy has no native --env flag. The pragmatic pattern is one file per environment that @includes a base task file, or duplicate Envoy.staging.blade.php / Envoy.production.blade.php and run envoy run deploy --conf=Envoy.production.blade.php.
Multi-server fan-out. Put multiple hosts in the @servers block. Deploy code to all, but run migrations and artisan commands only against the first host ($servers['app'][0]) to avoid duplicate migrations.
Rollback. Keep it as a first-class task:
envoy run rollback
It should re-point current to the previous release in releases/. It does not reverse migrations — plan for that separately (see Gotchas).
CI integration. The cleanest day-to-day workflow: build assets and run tests in CI, then invoke envoy run deploy as the final stage. Do not compile assets on the server.
Custom artisan tasks. Use Envoy's @task decorator for things like:
@task('deploy:cache', ['on' => $servers['app']])
cd {{ $currentReleasePath }}
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
@endtask
Envoy runs locally, artisan runs remotely. Your local PHP version is irrelevant to the deployed app; make sure the remote php binary is the right one. If the server defaults to PHP 7.4 but your app needs 8.2, every artisan step fails silently-ish. Use an explicit path (/usr/bin/php8.2) in tasks.
Blade vs shell variable collision. {{ $var }} is rendered locally by Envoy before the command is sent. If a shell command needs a literal $ (e.g. $USER, ${PATH}), escape it (\$USER) or the Blade renderer will either blank it or throw.
.env must pre-exist. The deploy:shared step symlinks shared/.env into the release. On a fresh server, if shared/.env is missing, the app errors on the first request after deploy — not during the deploy. Seed it manually as part of server bootstrap.
Permissions drift. Symlinked storage and bootstrap/cache are owned by the deploy user but need to be writable by www-data/nginx. Set setfacl or chown -R once on shared/; releases inherit via symlink.
Composer binary path. composer is often not on the non-interactive SSH PATH. Use the absolute path or php /usr/local/bin/composer.phar install --no-dev --optimize-autoloader --no-interaction.
Migrations are the sharp edge. artisan migrate --force between update_code and symlink can block the deploy and cannot be cleanly rolled back. For anything non-trivial, prefer a separate envoy run deploy:migrate step gated behind manual approval, or make migrations strictly additive/backward-compatible.
Not truly zero-downtime. The symlink swap is near-instant, but opcache/FPM may hold the old release. Add a step that reloads PHP-FPM or calls optimize:clear after deploy:symlink, otherwise you'll see stale code until the opcache TTL expires.
Debugging. Run envoy run deploy -v for verbose SSH output. Inspect {deploy_path}/.dep/ (the template typically stores revision metadata and release logs there) and {deploy_path}/releases/ to see what actually landed. When a step fails halfway, the release directory is left behind — clean it up or the next deploy:cleanup may misbehave.
Prune releases. $keepReleases in @setup controls pruning. Default of 5 is fine for most apps, but storage-heavy uploads in releases (rather than shared/) will silently fill the disk — always push user content into shared/.
Package staleness. At 23 stars and with the last release tagged 2026, treat this as a template, not a dependency you upgrade. Fork it into your repo, own the Envoy.blade.php, and pin your own changes rather than expecting upstream fixes. Its Capistrano layout is the real value; the code itself is small enough to adapt.
How can I help you explore Laravel packages today?