lorisleiva/laravel-deployer
Laravel Deployer offers preconfigured Deployer recipes and Artisan commands for zero-downtime Laravel deployments. It streamlines atomic releases, rollbacks, shared files, migrations, and server setup so you can ship changes safely and repeatably.
The package wraps Laravel's migration runner so a migration set is executed once across your fleet, rather than once per host. Start here:
composer require the package (dev/production as appropriate) and let Laravel's package discovery register the service provider.php artisan migrate as normal.php artisan migrate --force with the package's migrate:once command (the migrateOnce() entry point the release notes reference).First use case to validate: a two-host deploy behind a load balancer. Point both hosts at the same ledger store, deploy, and confirm the migrations table records one run — not two. If that works, you're 90% of the way to production.
If you're on a single host today, the package is mostly a safety net for the day you scale out; don't retrofit it into a deploy pipeline you don't yet need to dedupe.
Deploy-hook pattern (the intended one). Call migrate:once from every host in your deploy script and let the package arbitrate. This is far more robust than electing a "primary" host in your CI config, which breaks the moment that host is unhealthy.
Shared ledger, not shared cache. Prefer the database-backed ledger over the cache store when your cache is per-host (file/array drivers) or Redis is clustered without a shared logical database. A ledger that silently no-ops is worse than no package at all.
Guard the boundary, not the guts. Treat migrateOnce() as the entry point for schema changes only. Seeders, feature flags, cache warmers and queue restarts should stay in normal deploy steps — they are idempotent by design and gain nothing from deduplication.
Multi-tenant / multi-database. If you iterate connections, call the migrate-once entry point inside the per-tenant loop so each tenant database gets its own ledger scope. Don't run it once globally and expect per-tenant fan-out.
Testing. In Pest/PHPUnit, the ledger will persist between tests in the same process. Reset or scope it in your base TestCase (or use an in-memory store) so a test that migrates doesn't poison the next one.
Local dev. Keep using plain migrate / migrate:fresh locally — the ledger adds a moving part for no benefit on one machine, and migrate:fresh needs to blow the ledger away too.
v0.3.5 changes a default — read this before upgrading. migrateOnce() now targets multiple hosts by default. Previously the multi-host behaviour had to be opted into. Consequences:
migrate:once from several hosts and relying on the lock alone, the new default is what you assumed was happening.APP_ENV in the lock/ledger key is the cheap fix.composer outdated glance.Lock expiry vs. long migrations. If the package's lock has a TTL (many implementations do), a migration longer than the TTL lets a second host start mid-flight. Either raise the TTL well beyond your slowest migration or use a lock driver with proper lease renewal.
Failed migrations and the ledger. Confirm the ledger marks a run as complete only after success. A ledger that records intent-before-execution leaves you with a half-migrated schema and no automatic retry. Test this deliberately: force a failing migration, fix it, redeploy, verify it re-runs.
Debugging "why didn't my migration run?" Check in this order: (1) the ledger row exists — it ran somewhere; (2) the host can actually reach the shared store; (3) the connection/env is what you think it is (php artisan tinker on the deploy target, not locally). Nine times out of ten it's (2) and the package is behaving correctly.
Extension points. The ledger store, lock driver, and host identity are the three seams worth knowing. Custom host identity is the escape hatch for exotic topologies (Kubernetes jobs, ephemeral CI runners) — a stable identifier beats gethostname() there.
Rollbacks. migrate:rollback semantics under a once-only ledger are murky — rollback usually needs to run on all hosts or none. Prefer forward-only migrations and treat rollback as a manual, documented procedure rather than something the once-wrapper handles for you.
Pin the minor version in CI. The default flip in v0.3.5 is exactly the kind of change that should reach production via a reviewed composer.lock diff, not a floating constraint on a Friday deploy.
How can I help you explore Laravel packages today?