Laravel Forge Deployment Automation: From Development to Production
Laravel Forge revolutionizes how we deploy and manage Laravel applications. By automating server provisioning, deployments, and maintenance tasks, Forge allows developers to focus on building features rather than wrestling with server configurations. Let's explore how to leverage Forge for professional deployment workflows.
Understanding Laravel Forge Architecture
Server Provisioning and Setup
Forge simplifies server management across multiple cloud providers:
# Forge automatically sets up servers with:
# - PHP 8.2+ with required extensions
# - Nginx web server
# - MySQL/PostgreSQL databases
# - Redis for caching and queues
# - Node.js for asset compilation
# - SSL certificates via Let's Encrypt
# - Firewall configuration
# - Process monitoring with Supervisor
Site Configuration Best Practices
When creating sites in Forge, optimize for production:
# Forge generates optimized Nginx configuration
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name yourdomain.com;
root /home/forge/yourdomain.com/public;
# SSL Configuration (managed by Forge)
ssl_certificate /etc/nginx/ssl/yourdomain.com/server.crt;
ssl_certificate_key /etc/nginx/ssl/yourdomain.com/server.key;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Laravel-specific configuration
index index.php;
charset utf-8;
# Handle Laravel routes
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP handling
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
# Production optimizations
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
}
# Asset optimization
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Block sensitive files
location ~ /\.(?!well-known).* {
deny all;
}
}
Advanced Deployment Strategies
Zero-Downtime Deployment Scripts
Create sophisticated deployment scripts for production environments:
#!/bin/bash
# Custom Forge deployment script for zero-downtime deployments
set -e
# Configuration
FORGE_SITE_PATH="/home/forge/yourdomain.com"
BACKUP_PATH="/home/forge/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
echo "๐ Starting deployment at $(date)"
# Create backup directory if it doesn't exist
mkdir -p $BACKUP_PATH
cd $FORGE_SITE_PATH
# 1. Backup current release
echo "๐ฆ Creating backup..."
if [ -d "current" ]; then
cp -r current $BACKUP_PATH/backup_$TIMESTAMP
fi
# 2. Clone fresh copy to new directory
echo "โฌ๏ธ Pulling latest code..."
RELEASE_DIR="releases/release_$TIMESTAMP"
mkdir -p releases
git clone --depth 1 --branch production $FORGE_SITE_PATH $RELEASE_DIR
cd $RELEASE_DIR
# 3. Install PHP dependencies
echo "๐ Installing Composer dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist
# 4. Install and build Node.js assets
echo "๐จ Building assets..."
npm ci --production=false
npm run production
# 5. Copy environment file
echo "โ๏ธ Configuring environment..."
cp $FORGE_SITE_PATH/.env .env
# 6. Generate application key if needed
if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
php artisan key:generate --force
fi
# 7. Run database migrations
echo "๐๏ธ Running migrations..."
php artisan migrate --force
# 8. Cache optimization
echo "๐ง Optimizing caches..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
# 9. Queue optimization
echo "โก Optimizing queues..."
php artisan queue:restart
# 10. Warm up application
echo "๐ฅ Warming up application..."
curl -s -o /dev/null -w "%{http_code}" "https://yourdomain.com" || true
# 11. Create symbolic link (atomic swap)
echo "๐ Switching to new release..."
cd $FORGE_SITE_PATH
ln -nfs $RELEASE_DIR current-temp
mv current-temp current
# 12. Reload PHP-FPM
echo "๐ Reloading PHP-FPM..."
sudo service php8.2-fpm reload
# 13. Clean up old releases (keep last 5)
echo "๐งน Cleaning old releases..."
cd releases
ls -t | tail -n +6 | xargs rm -rf
echo "โ
Deployment completed successfully at $(date)"
# 14. Health check
echo "๐ฉบ Running health check..."
if curl -f -s "https://yourdomain.com/up" > /dev/null; then
echo "โ
Health check passed"
else
echo "โ Health check failed - consider rollback"
fi
# 15. Send deployment notification
echo "๐ข Sending deployment notification..."
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"๐ Deployment completed for yourdomain.com at $(date)\"}" \
$SLACK_WEBHOOK_URL
Environment-Specific Configurations
Set up different deployment strategies per environment:
# Staging deployment script
#!/bin/bash
set -e
cd $FORGE_SITE_PATH
# Staging allows more verbose output and testing
echo "๐งช Staging deployment started"
# Pull code
git pull origin staging
# Install all dependencies (including dev)
composer install --optimize-autoloader
# Install Node dependencies with dev tools
npm ci
# Build assets for staging (with source maps)
npm run dev
# Run migrations with seeding for testing data
php artisan migrate:fresh --seed --force
# Clear all caches for development
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
# Run tests to ensure deployment is working
echo "๐งช Running tests..."
php artisan test --coverage --parallel
# Queue setup for staging
php artisan queue:restart
php artisan horizon:terminate
echo "โ
Staging deployment completed"
Database Migration Strategies
Implement safe migration practices:
// app/Console/Commands/SafeMigrate.php
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class SafeMigrate extends Command
{
protected $signature = 'migrate:safe {--backup} {--check}';
protected $description = 'Run migrations with safety checks and backup option';
public function handle()
{
if ($this->option('check')) {
return $this->checkMigrations();
}
if ($this->option('backup')) {
$this->createBackup();
}
return $this->runMigrations();
}
private function checkMigrations()
{
$this->info('๐ Checking pending migrations...');
$pending = $this->getPendingMigrations();
if (empty($pending)) {
$this->info('โ
No pending migrations');
return 0;
}
$this->warn('โ ๏ธ Found ' . count($pending) . ' pending migrations:');
foreach ($pending as $migration) {
$this->line(" - {$migration}");
}
// Check for potentially destructive migrations
$destructive = $this->checkForDestructiveOperations($pending);
if (!empty($destructive)) {
$this->error('๐จ Found potentially destructive migrations:');
foreach ($destructive as $migration => $operations) {
$this->error(" {$migration}: " . implode(', ', $operations));
}
return 1;
}
return 0;
}
private function createBackup()
{
$this->info('๐พ Creating database backup...');
$database = config('database.connections.mysql.database');
$timestamp = date('Y-m-d_H-i-s');
$backupFile = storage_path("backups/db_backup_{$timestamp}.sql");
// Ensure backup directory exists
if (!is_dir(dirname($backupFile))) {
mkdir(dirname($backupFile), 0755, true);
}
// Create mysqldump backup
$command = sprintf(
'mysqldump -h%s -u%s -p%s %s > %s',
config('database.connections.mysql.host'),
config('database.connections.mysql.username'),
config('database.connections.mysql.password'),
$database,
$backupFile
);
exec($command, $output, $returnCode);
if ($returnCode === 0) {
$this->info("โ
Backup created: {$backupFile}");
} else {
$this->error('โ Backup failed');
return 1;
}
}
private function runMigrations()
{
$this->info('๐๏ธ Running migrations...');
try {
$this->call('migrate', ['--force' => true]);
$this->info('โ
Migrations completed successfully');
return 0;
} catch (\Exception $e) {
$this->error('โ Migration failed: ' . $e->getMessage());
return 1;
}
}
private function getPendingMigrations(): array
{
$migrationFiles = glob(database_path('migrations/*.php'));
$ranMigrations = DB::table('migrations')->pluck('migration')->toArray();
$pending = [];
foreach ($migrationFiles as $file) {
$migrationName = basename($file, '.php');
if (!in_array($migrationName, $ranMigrations)) {
$pending[] = $migrationName;
}
}
return $pending;
}
private function checkForDestructiveOperations(array $migrations): array
{
$destructive = [];
foreach ($migrations as $migration) {
$filePath = database_path("migrations/{$migration}.php");
if (file_exists($filePath)) {
$content = file_get_contents($filePath);
$operations = [];
if (strpos($content, '->drop') !== false) {
$operations[] = 'DROP operations detected';
}
if (strpos($content, 'Schema::dropIfExists') !== false) {
$operations[] = 'DROP TABLE operations detected';
}
if (strpos($content, '->dropColumn') !== false) {
$operations[] = 'DROP COLUMN operations detected';
}
if (!empty($operations)) {
$destructive[$migration] = $operations;
}
}
}
return $destructive;
}
}
CI/CD Integration
GitHub Actions Integration
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ production ]
jobs:
tests:
name: Run Tests
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, dom, fileinfo, mysql
coverage: xdebug
- name: Install Composer dependencies
run: composer install --prefer-dist --no-interaction
- name: Copy environment file
run: cp .env.example .env
- name: Generate application key
run: php artisan key:generate
- name: Run migrations
run: php artisan migrate --force
- name: Install Node dependencies
run: npm ci
- name: Build assets
run: npm run production
- name: Run tests
run: php artisan test --coverage-clover coverage.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install dependencies
run: composer install --prefer-dist --no-interaction
- name: Run security scan
run: composer audit
deploy:
name: Deploy to Forge
runs-on: ubuntu-latest
needs: [tests, security]
if: github.ref == 'refs/heads/production'
steps:
- name: Deploy to server
uses: jbrooksuk/laravel-forge-action@v1.0.0
with:
trigger_url: ${{ secrets.FORGE_TRIGGER_URL }}
- name: Verify deployment
run: |
sleep 30
response=$(curl -s -o /dev/null -w "%{http_code}" https://yourdomain.com/up)
if [ $response -eq 200 ]; then
echo "โ
Deployment successful"
else
echo "โ Deployment failed with status: $response"
exit 1
fi
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()
Rollback Strategies
# Rollback script for Forge
#!/bin/bash
set -e
FORGE_SITE_PATH="/home/forge/yourdomain.com"
BACKUP_PATH="/home/forge/backups"
echo "๐ Starting rollback process..."
cd $FORGE_SITE_PATH
# Find the latest backup
LATEST_BACKUP=$(ls -t $BACKUP_PATH/backup_* | head -n 1)
if [ -z "$LATEST_BACKUP" ]; then
echo "โ No backup found for rollback"
exit 1
fi
echo "๐ฆ Rolling back to: $LATEST_BACKUP"
# Stop services that might be using the application
echo "โน๏ธ Stopping services..."
sudo supervisorctl stop all
# Backup current state (in case rollback fails)
ROLLBACK_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
cp -r current $BACKUP_PATH/pre_rollback_$ROLLBACK_TIMESTAMP
# Replace current with backup
rm -rf current
cp -r $LATEST_BACKUP current
# Restore database from backup if available
DB_BACKUP="${LATEST_BACKUP}_database.sql"
if [ -f "$DB_BACKUP" ]; then
echo "๐๏ธ Restoring database..."
mysql -u$DB_USERNAME -p$DB_PASSWORD $DB_DATABASE < $DB_BACKUP
fi
cd current
# Reinstall dependencies (in case of version mismatches)
echo "๐ Reinstalling dependencies..."
composer install --no-dev --optimize-autoloader
# Clear caches
echo "๐งน Clearing caches..."
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
# Restart services
echo "๐ Restarting services..."
php artisan queue:restart
sudo supervisorctl start all
sudo service php8.2-fpm reload
# Health check
echo "๐ฉบ Running health check..."
if curl -f -s "https://yourdomain.com/up" > /dev/null; then
echo "โ
Rollback completed successfully"
else
echo "โ Rollback health check failed"
exit 1
fi
echo "โ
Rollback completed at $(date)"
Performance Optimization
Caching Strategies
# Production optimization script
#!/bin/bash
echo "๐ Optimizing for production..."
# Optimize Composer autoloader
composer install --no-dev --optimize-autoloader
# Cache Laravel configurations
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Optimize OPcache settings
sudo tee /etc/php/8.2/fpm/conf.d/99-opcache.ini << EOF
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.fast_shutdown=1
EOF
# Restart PHP-FPM to apply changes
sudo service php8.2-fpm restart
echo "โ
Production optimization complete"
Database Optimization
// Schedule database maintenance
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
// Optimize database tables weekly
$schedule->call(function () {
DB::statement('OPTIMIZE TABLE users, posts, comments');
})->weekly()->sundays()->at('02:00');
// Clean up old logs and temporary data
$schedule->call(function () {
DB::table('telescope_entries')
->where('created_at', '<', now()->subDays(7))
->delete();
})->daily()->at('03:00');
// Backup database before maintenance
$schedule->command('backup:database')
->weekly()
->sundays()
->at('01:30');
}
Monitoring and Alerting
Server Monitoring Setup
# Install server monitoring (runs automatically via Forge)
# Monitor disk space
df -h | awk '$5 > 80 {print "Disk space warning: " $1 " is " $5 " full"}'
# Monitor memory usage
free | awk 'NR==2{printf "Memory Usage: %.2f%\n", $3*100/$2}'
# Monitor PHP-FPM processes
ps aux | grep php-fpm | wc -l
# Monitor queue workers
sudo supervisorctl status | grep laravel-worker
Application Health Monitoring
// app/Http/Controllers/HealthController.php
class HealthController extends Controller
{
public function check()
{
$checks = [
'database' => $this->checkDatabase(),
'redis' => $this->checkRedis(),
'queue' => $this->checkQueue(),
'storage' => $this->checkStorage(),
];
$healthy = collect($checks)->every(fn($check) => $check['status'] === 'ok');
return response()->json([
'status' => $healthy ? 'ok' : 'error',
'checks' => $checks,
'timestamp' => now()->toISOString(),
], $healthy ? 200 : 503);
}
private function checkDatabase(): array
{
try {
DB::connection()->getPdo();
return ['status' => 'ok', 'message' => 'Database connected'];
} catch (Exception $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
private function checkRedis(): array
{
try {
Redis::ping();
return ['status' => 'ok', 'message' => 'Redis connected'];
} catch (Exception $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
private function checkQueue(): array
{
try {
$size = Queue::size();
return [
'status' => $size < 1000 ? 'ok' : 'warning',
'message' => "Queue size: {$size}",
'queue_size' => $size
];
} catch (Exception $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
private function checkStorage(): array
{
try {
$diskFree = disk_free_space(storage_path());
$diskTotal = disk_total_space(storage_path());
$percentUsed = (($diskTotal - $diskFree) / $diskTotal) * 100;
return [
'status' => $percentUsed < 90 ? 'ok' : 'warning',
'message' => "Disk usage: " . round($percentUsed, 2) . "%",
'disk_usage_percent' => round($percentUsed, 2)
];
} catch (Exception $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
}
Conclusion
Laravel Forge transforms deployment from a complex, error-prone process into a streamlined, automated workflow. By leveraging Forge's capabilities along with proper deployment scripts, CI/CD integration, and monitoring, you can achieve professional-grade deployment pipelines.
Key benefits of mastering Forge:
- Automated server management reduces operational overhead
- Zero-downtime deployments ensure continuous availability
- Integrated monitoring catches issues before they affect users
- Rollback capabilities provide safety nets for failed deployments
- CI/CD integration enables modern development workflows
Start with basic Forge functionality and gradually implement advanced features like custom deployment scripts, automated testing, and comprehensive monitoring. The investment in proper deployment automation pays dividends in reduced stress, faster iterations, and more reliable applications.