How to build automated deployment pipelines for PHP applications with zero-downtime releases, rollback strategies, and staging parity.
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.
A production-grade deployment pipeline for a PHP application has four stages:
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.
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.
Deploy to a staging environment that mirrors production:
Run smoke tests against staging to verify the deployment works in a production-like environment.
With the artifact verified in staging, deploy to production using a zero-downtime strategy.
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:
current symlink to the new releaseThe 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.
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 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.
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
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.
"It works on staging" must mean "it will work on production." Environment drift causes deployment failures. Minimize it:
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.
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.
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.
The goal is to deploy frequently with confidence. Elite teams deploy multiple times per day. The key enablers are:
Each deployment should be small enough that if something goes wrong, the cause is obvious from the commit diff.
Whether you're modernizing your infrastructure, navigating compliance, or building new software - we can help.
Book a 30-min Call