Software Teams

Deployment Automation for PHP Applications: Zero-Downtime Shipping

How to build automated deployment pipelines for PHP applications with zero-downtime releases, rollback strategies, and staging parity.

Manual Deployments Do Not Scale

If your deployment process involves SSH-ing into a server, running git pull, and hoping nothing breaks, you are one mistake away from a production outage. Manual deployments are slow, error-prone, and terrifying on Friday afternoons.

Automated deployment pipelines eliminate the human error factor and make deployments boring. Boring is exactly what you want.

Anatomy of a Deployment Pipeline

A production-grade deployment pipeline for a PHP application has four stages:

Stage 1: Build

Compile and prepare everything needed for deployment:

build:
  steps:
    - checkout code
    - composer install --no-dev --optimize-autoloader
    - npm ci && npm run build
    - php artisan event:cache
    - php artisan route:cache
    - php artisan config:cache
    - create deployment artifact (tarball or Docker image)

Key principle: Build once, deploy everywhere. The same artifact that passes staging tests gets deployed to production. Never run composer install on a production server.

Stage 2: Test

Run your full test suite against the build artifact:

test:
  steps:
    - php artisan test --parallel
    - phpstan analyse --level=8
    - npm run lint

If any test fails, the pipeline stops. The artifact never reaches production.

Stage 3: Stage

Deploy to a staging environment that mirrors production:

  • Same PHP version, same extensions
  • Same database engine and version
  • Same queue driver and cache driver
  • Same environment variables (with staging-specific values)

Run smoke tests against staging to verify the deployment works in a production-like environment.

Stage 4: Deploy to Production

With the artifact verified in staging, deploy to production using a zero-downtime strategy.

Zero-Downtime Deployment Strategies

Symlink-Based Deployments

The most common approach for PHP applications, used by tools like Deployer, Envoyer, and Ploi.

/var/www/
  releases/
    20260128-001/    # previous release
    20260128-002/    # current release
    20260128-003/    # new release being prepared
  current -> releases/20260128-002/   # symlink
  shared/
    storage/         # shared across releases
    .env             # shared configuration

How it works:

  1. Create a new release directory
  2. Upload or extract the build artifact
  3. Link shared directories (storage, .env)
  4. Run database migrations
  5. Swap the current symlink to the new release
  6. Reload PHP-FPM to pick up the new code
  7. Clean up old releases (keep the last 5 for rollback)

The symlink swap is atomic. Users are served from the old release until the exact moment the symlink changes, then immediately served from the new release.

Rollback: Swap the symlink back to the previous release directory. Takes seconds.

Docker-Based Deployments

For containerized applications:

deploy:
  steps:
    - docker build -t myapp:$GIT_SHA .
    - docker push registry.example.com/myapp:$GIT_SHA
    - kubectl set image deployment/myapp myapp=registry.example.com/myapp:$GIT_SHA

Kubernetes handles rolling updates automatically. New pods start with the new image, health checks pass, old pods terminate. Zero downtime by design.

Rollback: kubectl rollout undo deployment/myapp. Kubernetes rolls back to the previous image.

Database Migrations in Automated Pipelines

Database migrations are the trickiest part of zero-downtime deployments. The new code runs alongside the old code during the transition, so migrations must be backwards-compatible.

Safe Migration Patterns

Adding a column:

// Safe: old code ignores the new column
Schema::table('invoices', function (Blueprint $table) {
    $table->string('reference_number')->nullable()->after('invoice_number');
});

Removing a column (two-step process):

// Step 1: Deploy code that stops using the column
// Step 2: Deploy migration that drops the column
Schema::table('invoices', function (Blueprint $table) {
    $table->dropColumn('legacy_field');
});

Renaming a column (three-step process):

// Step 1: Add new column, write to both old and new
// Step 2: Migrate data from old to new, switch reads to new column
// Step 3: Drop old column

Migration Timing

Run migrations before the code swap, not after. If a migration fails, the old code is still running and unaffected. If it succeeds, the new code can use the updated schema immediately.

Environment Parity

"It works on staging" must mean "it will work on production." Environment drift causes deployment failures. Minimize it:

  • Infrastructure as Code. Define server configuration in Terraform, Ansible, or similar tools. Do not configure production servers manually.
  • Same versions everywhere. PHP 8.4 in development, staging, and production. MySQL 8.0 everywhere. Redis 7 everywhere.
  • Environment-specific configuration only. Database credentials, API keys, and domain names differ between environments. Application behavior should not.
  • Docker simplifies parity. When your application runs in the same Docker image everywhere, environment differences shrink to configuration values.

Deployment Safeguards

Health Checks

After deployment, verify the application is healthy:

// routes/web.php
Route::get('/health', function () {
    // Check database connectivity
    DB::connection()->getPdo();

    // Check Redis connectivity
    Cache::store('redis')->get('health-check');

    // Check queue connectivity
    Queue::size('default');

    return response()->json(['status' => 'healthy']);
});

Your deployment tool should hit this endpoint after deploy and roll back automatically if it returns an error.

Canary Deployments

Deploy to a subset of servers first. If the canary server shows elevated error rates, stop the rollout. If it is healthy after a defined observation period, continue to the remaining servers.

Deployment Notifications

Notify the team when deployments happen:

post_deploy:
  - notify slack channel with commit summary
  - update deployment tracking (who, when, what)
  - tag the release in error tracking (Sentry, Flare)

Tagging releases in your error tracking tool lets you instantly see if a deployment introduced new errors.

Deployment Frequency

The goal is to deploy frequently with confidence. Elite teams deploy multiple times per day. The key enablers are:

  • Automated testing that catches regressions
  • Zero-downtime deployment that eliminates risk
  • Feature flags that decouple deployment from release
  • Monitoring that detects problems within minutes

Each deployment should be small enough that if something goes wrong, the cause is obvious from the commit diff.

Let's talk about your software teams needs

Whether you're modernizing your infrastructure, navigating compliance, or building new software - we can help.

Book a 30-min Call