Zero Downtime Deployment
Also Known As
TL;DR
Explanation
Zero downtime deployment for PHP applications uses several techniques. Atomic symlink swap (Deployer, Envoyer, Capistrano): deploy to a new release/ directory, run migrations and composer install, then atomically switch a current/ symlink — PHP-FPM continues serving the old release until the symlink swaps. Blue/green: two identical environments; the load balancer is flipped between them after the new version passes health checks. Rolling: gradually replace old instances. Critical requirements: database migrations must be backward compatible with the old code (never rename or drop a column in the same deploy as the code that uses it — separate into two deploys); session storage must be external (Redis); file uploads must use shared storage (S3). Use a deployment tool like Deployer to orchestrate the release sequence.
Common Misconception
Why It Matters
Common Mistakes
- Restarting PHP-FPM abruptly — use reload (graceful) not restart to finish in-flight requests.
- Running destructive database migrations before deploying backward-compatible code.
- Not using atomic symlink swaps — file-by-file rsync creates a window where old and new files mix.
- Health check that passes immediately after deploy before the app has fully initialised.
Code Examples
# Non-zero-downtime deploy:
systemctl stop php-fpm # Drops all in-flight requests
rsync -avz ./app/ /var/www/ # Files updated one by one — mixed state
systemctl start php-fpm # Cold start — first requests slow
# Zero-downtime with symlinks:
rsync -avz ./app/ /var/www/releases/v2/ # Prep in background
ln -sfn /var/www/releases/v2 /var/www/current # Atomic swap
systemctl reload php-fpm # Graceful — finishes in-flight
# Zero-downtime deploy sequence for PHP apps
# 1. Run DB migrations that are backwards-compatible
# (add columns/tables — don't remove until next deploy)
$ php artisan migrate --force
# 2. Warm OPcache on new code
$ php artisan optimize
# 3. Rolling restart PHP-FPM workers (graceful — finishes current requests)
$ sudo kill -USR2 $(cat /var/run/php-fpm.pid)
# USR2 = graceful reload — new workers start, old ones finish then exit
# 4. Verify health endpoint returns 200
$ curl --fail https://yourapp.com/health
# Kubernetes approach:
# Set rollingUpdate: maxUnavailable=0, maxSurge=1
# New pods start, health check passes, old pods terminate
# Blue-green: both versions running — switch load balancer
# Canary: route 5% traffic to new version first