Deployment
Deploying Laravel applications with Forge, Vapor, and general server deployment strategies
You are an expert in deploying Laravel applications with Forge, Vapor, and related infrastructure. You approach deployment as a repeatable, automated process where every step is scripted, every environment is explicitly configured, and every release can be rolled back within minutes. ## Key Points * * * * * cd /home/forge/example.com && php artisan schedule:run >> /dev/null 2>&1 - Never set `APP_DEBUG=true` in production; it exposes environment variables and stack traces. - Run `php artisan optimize` on every deploy to cache config, routes, and views. - Use `--no-dev` with `composer install` in production to exclude development dependencies. - Set up health check endpoints for load balancers and uptime monitors. - Use queue worker process managers (Supervisor on Forge, native on Vapor) rather than running `queue:work` manually. - Store sessions and cache in Redis for horizontal scalability across multiple servers. - Use `php artisan down --secret="bypass-token"` during maintenance to allow testing before going live. - Automate deployments via CI/CD (GitHub Actions, GitLab CI) rather than manual SSH. - **Forgetting `php artisan queue:restart`**: After deployment, old worker processes still run the old code. They must be gracefully restarted. - **Running `migrate` without `--force`**: In production, Laravel prompts for confirmation interactively. `--force` is required for non-interactive deploys. - **Not caching config**: Every call to `config()` reads from disk without `config:cache`. This is a significant performance penalty. ## Quick Example ```bash # crontab (Forge manages this automatically) * * * * * cd /home/forge/example.com && php artisan schedule:run >> /dev/null 2>&1 # Vapor handles scheduling via CloudWatch Events # No cron setup needed for Vapor deployments ```
skilldb get php-laravel-skills/DeploymentFull skill: 357 linesDeployment — PHP/Laravel
You are an expert in deploying Laravel applications with Forge, Vapor, and related infrastructure. You approach deployment as a repeatable, automated process where every step is scripted, every environment is explicitly configured, and every release can be rolled back within minutes.
Core Philosophy
Deployment is not the final step of development -- it is a continuous discipline that shapes how the application is built. Every architectural decision has deployment implications: database migrations must be backward-compatible for zero-downtime deploys, configuration must be externalized for environment portability, and assets must be compiled reproducibly. Teams that treat deployment as a manual, infrequent ceremony accumulate deployment debt: long, error-prone runbooks, untested migration sequences, and "works on my machine" configuration gaps. Automating deployment from the first release eliminates this debt before it compounds.
Environment parity is a safety requirement, not a convenience. The difference between a successful deploy and a production incident is often a single misconfigured environment variable, a missing PHP extension, or a database version mismatch. Every environment -- development, staging, production -- should be configured explicitly and independently, with no assumption that one environment's settings will "just work" in another. Laravel's profile system, .env files, and config caching are designed to make environment-specific configuration straightforward. Using them correctly means a deploy to production is as predictable as a deploy to staging.
Rollback capability is the measure of deployment maturity. If a release introduces a regression, the team must be able to revert to the previous version in minutes, not hours. This requires that every deployment is atomic (old code serves traffic until the new release is verified), that database migrations are backward-compatible (the old code can still run against the new schema), and that assets are versioned (the old frontend does not load the new backend's resources). Forge, Vapor, and Envoyer each provide rollback mechanisms, but they only work when the application is designed for reversibility.
Overview
Laravel offers first-party deployment tools: Forge for provisioning and managing traditional servers (AWS EC2, DigitalOcean, Hetzner, etc.) and Vapor for serverless deployment on AWS Lambda. Understanding deployment pipelines, environment configuration, and production optimization is critical for reliable Laravel applications.
Core Concepts
Production Optimization Commands
# Cache configuration (merges all config files into one cached file)
php artisan config:cache
# Cache routes (skips route registration on every request)
php artisan route:cache
# Cache views (pre-compiles all Blade templates)
php artisan view:cache
# Cache events (pre-discovers event/listener mappings)
php artisan event:cache
# Optimize autoloader
composer install --optimize-autoloader --no-dev
# Run all optimizations at once
php artisan optimize
Environment Configuration
# .env.production (never committed to version control)
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
LOG_CHANNEL=stack
LOG_LEVEL=warning
DB_CONNECTION=mysql
DB_HOST=db-production.internal
DB_DATABASE=app_production
DB_USERNAME=app_user
DB_PASSWORD=secure-generated-password
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=redis-production.internal
MAIL_MAILER=ses
AWS_DEFAULT_REGION=us-east-1
Deployment Script (General)
#!/bin/bash
set -e
echo "Deploying application..."
# Enter maintenance mode
php artisan down --retry=60
# Pull latest code
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader --no-interaction
# Run migrations
php artisan migrate --force
# Clear and rebuild caches
php artisan optimize:clear
php artisan optimize
php artisan view:cache
# Install and build frontend assets
npm ci
npm run build
# Restart queue workers
php artisan queue:restart
# Exit maintenance mode
php artisan up
echo "Deployment complete."
Implementation Patterns
Laravel Forge
# Forge provisions servers and provides:
# - Automated SSL via Let's Encrypt
# - Queue worker management (Supervisor)
# - Scheduled task (cron) management
# - Database management
# - Deployment scripts triggered by Git push
# Typical Forge deployment script
cd /home/forge/example.com
git pull origin $FORGE_SITE_BRANCH
$FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader
( flock -w 10 9 || exit 1
echo 'Restarting FPM...'
sudo -S service $FORGE_PHP_FPM reload
) 9>/tmp/fpmrestart.lock
if [ -f artisan ]; then
$FORGE_PHP artisan migrate --force
$FORGE_PHP artisan config:cache
$FORGE_PHP artisan route:cache
$FORGE_PHP artisan view:cache
$FORGE_PHP artisan queue:restart
fi
npm ci
npm run build
Forge Queue Worker Configuration
# Managed via Forge UI -> Server -> Sites -> Queue
# Forge creates Supervisor config:
[program:worker-example-com]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/example.com/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=2
redirect_stderr=true
stdout_logfile=/home/forge/.forge/worker-example-com.log
stopwaitsecs=3600
Laravel Vapor (Serverless)
# vapor.yml
id: 12345
name: my-app
environments:
production:
memory: 1024
cli-memory: 512
runtime: php-8.3:al2
build:
- 'COMPOSER_MIRROR_PATH_REPOS=1 composer install --no-dev'
- 'php artisan event:cache'
- 'npm ci && npm run build && rm -rf node_modules'
deploy:
- 'php artisan migrate --force'
storage: my-app-production
database: my-app-db
cache: my-app-cache
gateway: http
domain: example.com
queues:
- default
- emails
staging:
memory: 512
runtime: php-8.3:al2
build:
- 'COMPOSER_MIRROR_PATH_REPOS=1 composer install --no-dev'
- 'npm ci && npm run build && rm -rf node_modules'
deploy:
- 'php artisan migrate --force'
database: my-app-staging-db
cache: my-app-staging-cache
domain: staging.example.com
# Deploy with Vapor CLI
vendor/bin/vapor deploy production
vendor/bin/vapor deploy staging
# Rollback
vendor/bin/vapor rollback production
# Run artisan commands on Lambda
vendor/bin/vapor command production --command="artisan tinker"
# View logs
vendor/bin/vapor tail production
Zero-Downtime Deployment with Envoyer
// Envoyer provides:
// - Symlink-based deployments (current -> releases/20240101120000)
// - Health checks before activating new release
// - One-click rollback
// - Deployment hooks (before/after each phase)
// Directory structure on server:
// /home/forge/example.com/
// current -> releases/20240315_143022
// releases/
// 20240315_143022/
// 20240314_120000/
// storage/ (shared, symlinked into each release)
// .env (shared, symlinked into each release)
Health Check Endpoint
// routes/web.php
Route::get('/health', function () {
try {
DB::connection()->getPdo();
Cache::store()->get('health-check');
// Add more checks as needed
return response()->json([
'status' => 'healthy',
'time' => now()->toIso8601String(),
]);
} catch (\Throwable $e) {
return response()->json([
'status' => 'unhealthy',
'error' => $e->getMessage(),
], 503);
}
});
Database Migration Safety
// For zero-downtime deploys, migrations must be backward-compatible.
// Bad: renaming a column while old code still reads the old name.
// Good: add new column, deploy code that writes to both, migrate data, then remove old column.
// Safe column addition
return new class extends Migration {
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('display_name')->nullable()->after('name');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('display_name');
});
}
};
Scheduled Tasks in Production
# crontab (Forge manages this automatically)
* * * * * cd /home/forge/example.com && php artisan schedule:run >> /dev/null 2>&1
# Vapor handles scheduling via CloudWatch Events
# No cron setup needed for Vapor deployments
Logging and Monitoring
// config/logging.php - production stack
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
'ignore_exceptions' => false,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'warning',
'days' => 14,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'level' => 'critical',
],
],
Best Practices
- Never set
APP_DEBUG=truein production; it exposes environment variables and stack traces. - Run
php artisan optimizeon every deploy to cache config, routes, and views. - Use
--no-devwithcomposer installin production to exclude development dependencies. - Set up health check endpoints for load balancers and uptime monitors.
- Use queue worker process managers (Supervisor on Forge, native on Vapor) rather than running
queue:workmanually. - Store sessions and cache in Redis for horizontal scalability across multiple servers.
- Use
php artisan down --secret="bypass-token"during maintenance to allow testing before going live. - Automate deployments via CI/CD (GitHub Actions, GitLab CI) rather than manual SSH.
Common Pitfalls
- Forgetting
php artisan queue:restart: After deployment, old worker processes still run the old code. They must be gracefully restarted. - Running
migratewithout--force: In production, Laravel prompts for confirmation interactively.--forceis required for non-interactive deploys. - Not caching config: Every call to
config()reads from disk withoutconfig:cache. This is a significant performance penalty. - Deploying with
APP_ENV=local: Accidentally deploying local config exposes debug info and may use wrong database/mail credentials. - Ignoring disk space for logs: Without log rotation (
dailydriver orlogrotate), logs can fill the disk. - Breaking migrations in zero-downtime deploys: Column renames or drops break the old code still running on other servers. Use a multi-step migration approach.
- Missing
.envon fresh servers: The deploy script assumes.envexists. Forge and Envoyer manage this, but manual setups often forget.
Anti-Patterns
-
The SSH-and-pray deploy — manually SSH-ing into the production server, running
git pull, and hoping nothing breaks. Manual deployments are unrepeatable, unauditable, and prone to forgotten steps. Every deploy should be triggered by a script or CI/CD pipeline, never by a human typing commands on a production server. -
Destructive migrations in zero-downtime deploys — renaming or dropping columns while the old code is still serving traffic on other servers. The old code reads from the old column name and crashes. Use a multi-step migration approach: add the new column, deploy code that writes to both, migrate data, then remove the old column in a subsequent release.
-
Debug mode in production — deploying with
APP_DEBUG=truebecause it "helps see errors." Debug mode exposes environment variables, database credentials, and full stack traces to anyone who triggers an error. SetAPP_DEBUG=falseand use structured logging and error tracking services (Sentry, Flare) instead. -
Skipping cache warmup — deploying without running
php artisan optimize, causing every request to re-parse config files, re-register routes, and re-compile Blade templates. The first users after a deploy experience significantly slower responses. Cache warmup should be a mandatory step in every deployment script. -
Queue workers running old code — deploying new code but forgetting
php artisan queue:restart, leaving workers processing jobs with the previous version's logic. This causes subtle bugs where some jobs use new code and others use old code. Always restart queue workers after every deploy.
Install this skill directly: skilldb add php-laravel-skills
Related Skills
API Resources
Laravel API resources and transformers for building consistent, well-structured JSON API responses
Authentication
Laravel authentication using Sanctum for API tokens and SPAs, and Fortify for web-based auth flows
Blade Livewire
Blade templating engine and Livewire for building dynamic server-rendered UI in Laravel applications
Eloquent ORM
Eloquent ORM patterns for models, relationships, query scoping, and database interactions in Laravel
Queues Jobs
Laravel queues, jobs, and background task processing with Redis, SQS, and Horizon
Routing Middleware
Laravel routing, route groups, resource controllers, and middleware for request filtering and authentication