The Problem With Naive Deployments
The simplest Laravel deployment — SSH to the server, git pull, php artisan migrate, php artisan config:cache — works until you have paying customers who expect 99.9% uptime. At that point, the 90-second window where your app is running old code but new database schema (or vice versa) starts to matter. Zero-downtime deployments are not optional for production SaaS — they are table stakes.
The Blue-Green Architecture on ECS
Blue-green deployments maintain two identical production environments (blue and green). Traffic is routed to blue; you deploy to green; once green passes health checks, you switch the load balancer to point at green. The old blue environment stays warm for 10–15 minutes as a rollback target. On AWS ECS with an Application Load Balancer, this is natively supported via the CodeDeploy ECS deployment type.
Step 1: Database Migrations Without Downtime
The hardest part of zero-downtime deployments is database schema changes. You cannot run a migration that drops a column while the old application version is still reading that column. The solution is expand-contract migrations: (1) Expand: add new columns/tables (backward-compatible). (2) Deploy new code that writes to both old and new columns. (3) Contract: remove old columns in a later deployment. This approach requires more deployment steps but eliminates the schema/code mismatch window entirely. Never run php artisan migrate in your entrypoint — use a separate migration task in your ECS task definition that runs before the new service version receives traffic.
Step 2: Queue Worker Drain and Restart
When you deploy new code, running queue workers still have the old code in memory. You need them to finish their current jobs (not abort mid-process) and then restart with new code. Laravel Horizon supports graceful termination via php artisan horizon:terminate — send SIGTERM, Horizon drains in-progress jobs and exits cleanly. Trigger this via a deploy hook after the new application containers are healthy but before deregistering the old task set.
Step 3: Cache Warming
After deployment, the config, route, and view caches are cold. Under high traffic, this causes a thundering herd on first requests. Warm caches proactively: php artisan config:cache, route:cache, and view:cache run as part of the Docker image build — not at startup. This ensures caches are populated before the container receives its first request.
Rollback in 30 Seconds
With blue-green, rollback is a single ALB listener rule change — shift traffic back to the old target group. On AWS CodeDeploy, this is a one-click operation (or a single CLI command). Combined with automated CloudWatch alarm triggers that roll back on P2+ error rate increase, you get self-healing deployments with no human intervention required for most failure modes.