
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Dropped requests during deployment are unacceptable for production PHP applications, yet traditional in-place updates frequently cause brief outages or error spikes. Implementing blue-green deploys for a PHP app eliminates this risk by maintaining two identical production environments and switching traffic atomically at the reverse proxy layer. This guide details the exact Nginx configuration, filesystem layout, and database migration strategies required to make this pattern work reliably for Laravel or Symfony projects in 2026.
How do you configure Nginx for blue-green deploys for a PHP app?
The core mechanism relies on Nginx's upstream directive and the ability to reload configuration without dropping connections. Unlike Kubernetes services that handle this via label selectors, bare-metal or VM-based PHP apps require explicit proxy management. For teams familiar with blue-green and canary deploys on Kubernetes, think of the Nginx upstream block as your manual service mesh router.
Define Separate Upstream Blocks
Create distinct upstreams for each environment. Each points to a separate PHP-FPM socket or TCP port. Using Unix sockets is generally preferred for performance on a single host, while TCP ports work better across multiple servers.
# /etc/nginx/conf.d/php-upstreams.conf
upstream php_blue {
server unix:/run/php/php8.4-fpm-blue.sock;
# Or TCP: server 127.0.0.1:9001;
}
upstream php_green {
server unix:/run/php/php8.4-fpm-green.sock;
# Or TCP: server 127.0.0.1:9002;
}
# The active upstream used by the vhost
upstream php_active {
server unix:/run/php/php8.4-fpm-green.sock;
} Atomic Switching Script
Never edit the main Nginx config manually during a deploy. Use a script that swaps the symlink or updates the upstream file, tests the config, and reloads. The nginx -t check is mandatory; a syntax error during reload will leave your old config intact, but a bad upstream path causes 502 errors.
#!/bin/bash
# /opt/deploy/scripts/switch-upstream.sh
TARGET=$1 # 'blue' or 'green'
if [[ "$TARGET" != "blue" && "$TARGET" != "green" ]]; then
echo "Usage: $0 {blue|green}"
exit 1
fi
# Update the active upstream pointer
sed -i "s|server unix:/run/php/php8.4-fpm-[a-z]*.sock;|server unix:/run/php/php8.4-fpm-${TARGET}.sock;|" \
/etc/nginx/conf.d/php-active-upstream.conf
# Validate before reloading
if nginx -t 2>/dev/null; then
systemctl reload nginx
echo "Switched to ${TARGET} successfully"
else
echo "ERROR: Nginx config test failed. No changes applied."
exit 1
fi What is the correct directory structure for PHP blue-green deployments?
A common mistake is trying to reuse the same vendor directory or storage folder across both environments. True isolation prevents dependency conflicts and file permission race conditions. When implementing blue-green deploys for a PHP app, treat each color as a completely independent installation that happens to share a database and cache layer.
- /var/www/app-blue/current/ — Complete application release including vendor/
- /var/www/app-green/current/ — Independent copy with its own autoloader
- /var/www/app-shared/storage/ — Symlinked logs, sessions, and uploads
- /var/www/app-shared/.env — Shared environment config (symlinked into both)
- /run/php/php8.4-fpm-blue.sock — Dedicated FPM socket per color
This structure mirrors what mature tools like Deployer use internally. If you are managing infrastructure manually, adhering to this convention makes it easier to migrate to automated tooling later. Ensure the www-data user owns both trees and that shared directories have correct ACLs to prevent cross-contamination during concurrent writes.
How do you handle database migrations safely in blue-green PHP deploys?
Database schema changes are the most frequent failure point. During the transition period, both Blue and Green versions may be serving requests simultaneously for a few seconds. Your database must remain compatible with both codebases at all times. This requires backward-compatible migrations, a discipline I emphasize when discussing zero-downtime Laravel database migrations.
The Expand-and-Contract Pattern
- Expand: Add new columns as nullable or with defaults. Do not remove old columns.
- Deploy Green: New code writes to both old and new columns. Reads prefer new, fallback to old.
- Switch: Route traffic to Green. Both old and new data paths are valid.
- Contract: In a subsequent deploy, backfill historical data, update reads to use only new column, then drop the old column.
This multi-step approach feels slow compared to running php artisan migrate inline, but it is the only way to guarantee zero downtime. Skipping this discipline is why many teams abandon blue-green entirely after their first botched schema change.
How does blue-green compare to rolling and canary deploys for PHP?
Choosing the right strategy depends on your team size, traffic volume, and tolerance for complexity. While blue-green deploys for a PHP app offer the fastest rollback, they double your compute costs temporarily. Understanding these trade-offs helps avoid over-engineering.
| Criteria | Blue-Green | Rolling Update | Canary |
|---|---|---|---|
| Downtime Risk | Near zero (atomic switch) | Brief errors possible | Near zero |
| Rollback Speed | Instant (flip switch) | Slow (redeploy previous) | Fast (shift weight) |
| Resource Cost | 2× during deploy | 1× + buffer | 1× + small % |
| DB Migration Safety | Requires expand-contract | Same requirement | Same requirement |
| Complexity | Moderate (proxy mgmt) | Low | High (traffic splitting) |
| Best For | Critical apps, fast rollback | Internal tools, low traffic | High-risk feature validation |
For most PHP e-commerce or SaaS platforms serving Nepal and global audiences, blue-green provides the best balance of safety and operational simplicity. Canary releases add significant observability overhead that smaller teams often cannot sustain effectively.
How do you automate health checks before switching traffic?
Never switch traffic based solely on deployment script success. Automated health verification catches issues that static checks miss: misconfigured caches, failed queue workers, or database connection pool exhaustion. Integrate this gate directly into your CI/CD pipeline or deployment script.
#!/bin/bash
# Verify the STANDBY environment before promotion
STANDBY_URL="http://127.0.0.1:8081/healthz"
MAX_RETRIES=10
RETRY_INTERVAL=3
for i in $(seq 1 $MAX_RETRIES); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$STANDBY_URL")
if [ "$STATUS" == "200" ]; then
echo "Health check passed on attempt $i"
exit 0
fi
echo "Attempt $i/$MAX_RETRIES: HTTP $STATUS. Waiting..."
sleep $RETRY_INTERVAL
done
echo "FAILED: Standby environment unhealthy after $MAX_RETRIES attempts"
exit 1 Your /healthz endpoint must verify actual dependencies, not just return a static 200. Check database connectivity, Redis availability, and critical service bindings. A shallow health check gives false confidence. For comprehensive monitoring setup, refer to Laravel health checks and uptime monitoring to build endpoints that reflect true application readiness.
Implementing Blue-Green Deploys for a PHP App Reliably
Adopting blue-green deploys for a PHP app transforms your release process from a source of anxiety into a predictable, reversible operation. Start by setting up isolated FPM pools and Nginx upstreams on a staging server to practice the workflow without production pressure. Automate the health check gate early; manual verification does not scale and introduces human error during high-stress moments. Remember that database compatibility is the hardest constraint—invest time in expand-contract migration patterns before attempting your first live switch. If your team needs guidance on architecting this for Laravel, legacy PHP, or compliance-regulated environments, reach out to discuss your deployment strategy.