0

Zero-downtime deployment — deploying without a visible outage

Intermediate5 min read·eng-18-008
interview

Concept

Zero-downtime deployment — deploying new code while the application continues serving requests without interruption or error.

The problem: A naive deployment stops the server, overwrites files, restarts. During this window, users get errors. For high-traffic apps, even seconds of downtime is unacceptable.

What causes downtime during deployments:

  • Server restart or stop.
  • Database migrations that lock tables.
  • New code deployed before the server restarts (mixed old/new code in one request).
  • composer install running while web server is serving requests from incomplete vendor directory.

Atomic deployments with symlinks (the classic approach):

  1. Upload new code to a NEW directory (releases/20240612).
  2. Run composer install, artisan migrate, artisan optimize.
  3. Atomically swap the symlink: current/releases/20240612.
  4. PHP-FPM reads current/ — now serves new code with zero downtime.
  5. Keep last 5 releases, delete older ones.

Tools: Laravel Envoyer, Capistrano, Deployer. All implement the atomic symlink approach.

Database migrations in zero-downtime: The hardest part. The new code must work with the OLD schema (during the deployment window, old code still runs) AND with the NEW schema. Approach: multi-step migrations.

Blue-green deployments (separate concept): Run two identical environments. Zero downtime via load balancer switch. See eng-18-009.

Code Example

bash
# Atomic deployment script (simplified Envoyer/Deployer approach)
#!/bin/bash
RELEASE_DIR="/var/www/releases/$(date +%Y%m%d%H%M%S)"
CURRENT_LINK="/var/www/current"
SHARED_DIR="/var/www/shared"

# 1. Upload code to new release directory
rsync -az ./dist/ "$RELEASE_DIR/"

# 2. Link shared files (storage, .env)
ln -nfs "$SHARED_DIR/.env" "$RELEASE_DIR/.env"
ln -nfs "$SHARED_DIR/storage" "$RELEASE_DIR/storage"

# 3. Install dependencies in release directory
cd "$RELEASE_DIR"
composer install --no-dev --optimize-autoloader

# 4. Run migrations BEFORE swap (must be backward-compatible!)
php artisan migrate --force

# 5. Optimize (cache config, routes, views)
php artisan optimize

# 6. ATOMIC SWAP — this is the zero-downtime moment
ln -nfs "$RELEASE_DIR" "$CURRENT_LINK"  # atomic: one syscall

# 7. Reload PHP-FPM gracefully (drains existing requests, starts new workers)
php-fpm -t && kill -USR2 $(cat /var/run/php-fpm.pid)

# 8. Clean up old releases (keep last 5)
ls -t /var/www/releases/ | tail -n +6 | xargs -I {} rm -rf /var/www/releases/{}

echo "Deployed to: $RELEASE_DIR"
php
<?php
// SAFE MIGRATION for zero-downtime:
// Adding a NOT NULL column requires multiple deployments

// WRONG — blocks table during deploy, breaks old code
Schema::table('orders', function (Blueprint $table) {
    $table->string('reference_code')->notNull(); // old code doesn't send this!
});

// CORRECT — three deployments:
// Deploy 1: Add nullable column (backward-compatible)
Schema::table('orders', function (Blueprint $table) {
    $table->string('reference_code')->nullable(); // old code works fine
});

// Deploy 2: Backfill existing rows (queue job, no downtime)
// Deploy 3: Add NOT NULL constraint after all rows have values
Schema::table('orders', function (Blueprint $table) {
    $table->string('reference_code')->notNull()->change();
});