L
Laravel Deployment
LaravelDeploymentDevOpsProduction
Laravel Deployment: Zero-Downtime Production Deployments
Master Laravel deployments with zero-downtime strategies, environment configuration, database migrations, queue management, and monitoring setup.

Hoceine El Idrissi
Full Stack Developer
8 min read
Laravel Deployment: Zero-Downtime Production Deployments
Deploying Laravel applications requires careful planning to ensure zero downtime and smooth updates. This guide covers essential deployment strategies, from basic setups to enterprise-grade workflows.
Pre-Deployment Checklist
Environment Configuration
bash
# Verify environment
php artisan about
# Check configuration
php artisan config:show database
php artisan config:show cache
Production Optimizations
bash
# Cache configuration
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Or all at once
php artisan optimize
Database Migrations
bash
# Test migrations first
php artisan migrate --pretend
# Run migrations
php artisan migrate --force
Deployment Scripts
Basic Deployment Script
bash
#!/bin/bash
set -e
echo "Starting deployment..."
# Pull latest code
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Build frontend assets
npm ci
npm run build
# Run migrations
php artisan migrate --force
# Clear and cache
php artisan optimize:clear
php artisan optimize
# Restart services
php artisan queue:restart
echo "Deployment complete!"
Zero-Downtime Deployment
bash
#!/bin/bash
set -e
RELEASE_DIR="/var/www/releases/$(date +%Y%m%d%H%M%S)"
CURRENT_LINK="/var/www/current"
SHARED_DIR="/var/www/shared"
# Create release directory
mkdir -p $RELEASE_DIR
# Clone/copy code
git clone --depth 1 git@github.com:user/repo.git $RELEASE_DIR
# Link shared files
ln -sf $SHARED_DIR/.env $RELEASE_DIR/.env
ln -sf $SHARED_DIR/storage $RELEASE_DIR/storage
# Install dependencies
cd $RELEASE_DIR
composer install --no-dev --optimize-autoloader
# Build assets
npm ci && npm run build
# Run migrations
php artisan migrate --force
# Optimize
php artisan optimize
# Atomic symlink swap (zero downtime)
ln -sfn $RELEASE_DIR $CURRENT_LINK
# Reload PHP-FPM
sudo systemctl reload php8.3-fpm
# Restart queue workers
php artisan queue:restart
# Cleanup old releases (keep last 5)
cd /var/www/releases
ls -t | tail -n +6 | xargs -r rm -rf
echo "Deployed: $RELEASE_DIR"
Laravel Forge Deployment
Deploy Script
bash
cd /home/forge/myapp.com
git pull origin $FORGE_SITE_BRANCH
$FORGE_COMPOSER install --no-dev --optimize-autoloader
( flock -w 10 9 || exit 1
echo 'Restarting FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock
if [ -f artisan ]; then
$FORGE_PHP artisan migrate --force
$FORGE_PHP artisan optimize
$FORGE_PHP artisan queue:restart
fi
npm ci
npm run build
Environment Variables
php
// In .env.example (reference)
APP_NAME=MyApp
APP_ENV=production
APP_DEBUG=false
APP_URL=https://myapp.com
LOG_CHANNEL=stack
LOG_LEVEL=error
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=forge
DB_PASSWORD=
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
Database Migration Strategies
Safe Migration Patterns
php
// Always use nullable or default values for new columns
Schema::table('users', function (Blueprint $table) {
$table->string('timezone')->nullable()->after('email');
});
// Or with default
Schema::table('users', function (Blueprint $table) {
$table->string('status')->default('active')->after('email');
});
Large Table Migrations
php
// For large tables, use pt-online-schema-change or similar
public function up()
{
// Add index in separate migration
Schema::table('orders', function (Blueprint $table) {
$table->index(['user_id', 'created_at']);
});
}
Rollback Strategy
php
// Always define down() method
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('timezone');
});
}
Queue Worker Management
Supervisor Configuration
ini
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/var/www/shared/storage/logs/worker.log
stopwaitsecs=3600
Horizon for Advanced Queues
bash
# Install Horizon
composer require laravel/horizon
# Supervisor for Horizon
[program:horizon]
process_name=%(program_name)s
command=php /var/www/current/artisan horizon
autostart=true
autorestart=true
user=forge
redirect_stderr=true
stdout_logfile=/var/www/shared/storage/logs/horizon.log
stopwaitsecs=3600
Health Checks
Health Check Endpoint
php
// routes/web.php
Route::get('/health', function () {
$checks = [
'database' => false,
'cache' => false,
'queue' => false,
];
try {
DB::connection()->getPdo();
$checks['database'] = true;
} catch (\Exception $e) {}
try {
Cache::put('health_check', true, 10);
$checks['cache'] = Cache::get('health_check') === true;
} catch (\Exception $e) {}
try {
Queue::size('default');
$checks['queue'] = true;
} catch (\Exception $e) {}
$allHealthy = !in_array(false, $checks, true);
return response()->json([
'status' => $allHealthy ? 'healthy' : 'unhealthy',
'checks' => $checks,
'timestamp' => now()->toISOString(),
], $allHealthy ? 200 : 503);
});
Monitoring Integration
php
// app/Providers/AppServiceProvider.php
public function boot()
{
if (app()->isProduction()) {
// Log slow queries
DB::listen(function ($query) {
if ($query->time > 1000) {
Log::warning('Slow query', [
'sql' => $query->sql,
'time' => $query->time,
]);
}
});
// Report exceptions to external service
$this->app['log']->listen(function ($event) {
if ($event->level === 'error') {
// Send to Sentry, Flare, etc.
}
});
}
}
Rollback Procedures
Quick Rollback
bash
#!/bin/bash
# rollback.sh
CURRENT_LINK="/var/www/current"
RELEASES_DIR="/var/www/releases"
# Get previous release
PREVIOUS=$(ls -t $RELEASES_DIR | sed -n '2p')
if [ -z "$PREVIOUS" ]; then
echo "No previous release found!"
exit 1
fi
echo "Rolling back to: $PREVIOUS"
# Swap symlink
ln -sfn "$RELEASES_DIR/$PREVIOUS" $CURRENT_LINK
# Reload PHP-FPM
sudo systemctl reload php8.3-fpm
# Restart queues
php $CURRENT_LINK/artisan queue:restart
echo "Rollback complete!"
Database Rollback
bash
# Rollback last migration
php artisan migrate:rollback --step=1
# Rollback to specific batch
php artisan migrate:rollback --batch=5
SSL and Security
Force HTTPS
php
// app/Providers/AppServiceProvider.php
public function boot()
{
if (app()->isProduction()) {
URL::forceScheme('https');
}
}
Security Headers
php
// app/Http/Middleware/SecurityHeaders.php
public function handle($request, $next)
{
$response = $next($request);
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
if (app()->isProduction()) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}
return $response;
}
Deployment Checklist
Before Deployment
- All tests passing
- Code reviewed
- Dependencies updated
- Environment variables set
- Database backup taken
- Rollback plan ready
During Deployment
- Maintenance mode if needed
- Pull latest code
- Install dependencies
- Run migrations
- Clear caches
- Restart queue workers
After Deployment
- Health check passing
- Monitor error logs
- Verify critical features
- Check queue processing
- Monitor performance
Conclusion
Zero-downtime deployments require atomic symlink swaps, proper database migration strategies, and queue worker management. Always have a rollback plan and monitor your application after every deployment.