Advanced Laravel
Advanced Deployment Strategies
Advanced Deployment Strategies
Modern Laravel deployment requires strategies that minimize downtime, ensure reliability, and scale efficiently. This lesson covers zero-downtime deployments, automation tools, serverless architectures, and containerization approaches for production Laravel applications.
Production Goals: Zero downtime during deployments, automated rollback on failures, easy scaling, consistent environments, and fast deployment cycles. These strategies ensure your application remains available and performant.
Zero-Downtime Deployment Basics
Traditional deployments cause downtime when files are replaced. Zero-downtime deployment uses symbolic links and atomic directory switching:
<?php
// Directory structure for zero-downtime
/var/www/myapp/
├── current → releases/20240214120000 (symlink)
├── releases/
│ ├── 20240214120000/ (latest)
│ ├── 20240214110000/
│ └── 20240214100000/
├── shared/
│ ├── storage/
│ │ ├── app/
│ │ ├── logs/
│ │ └── framework/
│ └── .env
└── repo/ (git repository)
// Deployment steps:
// 1. Clone/pull code to new release directory
// 2. Install dependencies (composer, npm)
// 3. Create symlinks to shared resources
// 4. Run migrations and optimizations
// 5. Atomically switch 'current' symlink to new release
// 6. Reload PHP-FPM/restart queue workers
// 7. Keep last 3-5 releases for quick rollback
// Example bash deployment script
#!/bin/bash
DEPLOY_PATH="/var/www/myapp"
RELEASE_NAME=$(date +%Y%m%d%H%M%S)
RELEASE_PATH="$DEPLOY_PATH/releases/$RELEASE_NAME"
# Clone repository
git clone --depth 1 https://github.com/user/repo.git $RELEASE_PATH
# Install dependencies
cd $RELEASE_PATH
composer install --no-dev --optimize-autoloader
npm ci && npm run build
# Create symlinks to shared resources
ln -nfs $DEPLOY_PATH/shared/.env $RELEASE_PATH/.env
ln -nfs $DEPLOY_PATH/shared/storage $RELEASE_PATH/storage
# Run migrations and optimizations
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Atomic switch to new release
ln -nfs $RELEASE_PATH $DEPLOY_PATH/current
# Reload services
sudo systemctl reload php8.2-fpm
php artisan queue:restart
# Cleanup old releases (keep last 5)
cd $DEPLOY_PATH/releases
ls -t | tail -n +6 | xargs rm -rf
echo "Deployment completed: $RELEASE_NAME"
Atomic Symlink Switch: The `ln -nfs` command atomically replaces the symlink, ensuring users never see a half-deployed application. Web servers follow the symlink to serve the new version instantly.
Laravel Envoy - Elegant Deployment Automation
<?php
// composer require laravel/envoy --dev
// Envoy.blade.php
@servers(['web' => 'deploy@example.com'])
@setup
$repository = 'git@github.com:user/repo.git';
$app_dir = '/var/www/myapp';
$release = date('YmdHis');
$new_release_dir = $app_dir.'/releases/'.$release;
@endsetup
@story('deploy')
clone_repository
run_composer
run_npm
update_symlinks
migrate_database
optimize_application
switch_release
reload_services
cleanup_old_releases
@endstory
@task('clone_repository')
echo "Cloning repository..."
[ -d {{ $app_dir }}/repo ] || git clone {{ $repository }} {{ $app_dir }}/repo
git --git-dir={{ $app_dir }}/repo/.git --work-tree={{ $app_dir }}/repo pull origin main
mkdir -p {{ $new_release_dir }}
cp -r {{ $app_dir }}/repo/* {{ $new_release_dir }}
echo "Repository cloned"
@endtask
@task('run_composer')
echo "Running composer..."
cd {{ $new_release_dir }}
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
@endtask
@task('run_npm')
echo "Building assets..."
cd {{ $new_release_dir }}
npm ci
npm run build
@endtask
@task('update_symlinks')
echo "Linking storage and .env..."
ln -nfs {{ $app_dir }}/shared/.env {{ $new_release_dir }}/.env
ln -nfs {{ $app_dir }}/shared/storage {{ $new_release_dir }}/storage
# Ensure bootstrap/cache exists
mkdir -p {{ $new_release_dir }}/bootstrap/cache
chmod -R 775 {{ $new_release_dir }}/bootstrap/cache
@endtask
@task('migrate_database')
echo "Running migrations..."
cd {{ $new_release_dir }}
php artisan migrate --force
@endtask
@task('optimize_application')
echo "Optimizing application..."
cd {{ $new_release_dir }}
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
@endtask
@task('switch_release')
echo "Switching to new release..."
ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current
@endtask
@task('reload_services')
echo "Reloading services..."
sudo systemctl reload php8.2-fpm
cd {{ $app_dir }}/current
php artisan queue:restart
@endtask
@task('cleanup_old_releases')
echo "Cleaning up old releases..."
cd {{ $app_dir }}/releases
ls -t | tail -n +6 | xargs rm -rf
@endtask
@finished
echo "Deployment completed successfully!"
@endfinished
@error
echo "Deployment failed at task: {{ $task }}"
@enderror
// Deploy with: php vendor/bin/envoy run deploy
// Rollback: php vendor/bin/envoy run rollback
@story('rollback')
rollback_release
reload_services
@endstory
@task('rollback_release')
cd {{ $app_dir }}/releases
PREVIOUS=$(ls -t | sed -n 2p)
echo "Rolling back to: $PREVIOUS"
ln -nfs {{ $app_dir }}/releases/$PREVIOUS {{ $app_dir }}/current
@endtask
Laravel Forge - Managed Deployment
<?php
// Forge provides one-click deployment with:
// - Server provisioning (DigitalOcean, AWS, Linode)
// - SSL certificate management (Let's Encrypt)
// - Database backups
// - Queue worker management
// - Scheduled task management
// - Zero-downtime deployments
// - One-click rollback
// Deploy script example (auto-generated by Forge)
cd /home/forge/example.com
git pull origin main
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
php artisan migrate --force
npm ci
npm run build
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
if [ -f artisan ]; then
php artisan horizon:terminate
fi
// Quick Deployment via Forge API
use Laravel\Forge\Forge;
$forge = new Forge(env('FORGE_API_TOKEN'));
// Deploy site
$forge->deploySite($serverId, $siteId);
// Check deployment status
$deployment = $forge->siteDeploymentHistory($serverId, $siteId);
// Enable quick deploy (auto-deploy on git push)
$forge->enableQuickDeploy($serverId, $siteId);
Deployment Testing: Always test deployments on staging environments first. Use feature flags to gradually roll out changes. Monitor error rates and performance metrics immediately after deployment.
Laravel Vapor - Serverless Deployment
<?php
// composer require laravel/vapor-cli --global
// vapor login
// vapor.yml configuration
id: 12345
name: my-app
environments:
production:
memory: 1024
cli-memory: 512
runtime: 'php-8.2:al2'
build:
- 'composer install --no-dev --classmap-authoritative'
- 'php artisan event:cache'
deploy:
- 'php artisan migrate --force'
staging:
memory: 512
cli-memory: 512
runtime: 'php-8.2:al2'
database: my-staging-db
cache: my-staging-cache
// Deploy to production
// vapor deploy production
// Key Vapor features:
// - Automatic scaling (0 to infinity)
// - Pay only for actual usage
// - AWS Lambda integration
// - CloudFront CDN
// - RDS database management
// - SQS queue integration
// - S3 asset storage
// - Environment variables management
// - Custom domains and SSL
// Vapor-specific code adjustments
// Storage disk configuration
'disks' => [
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'visibility' => 'public',
],
],
// Queue configuration for Vapor
'connections' => [
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX'),
'queue' => env('SQS_QUEUE'),
'region' => env('AWS_DEFAULT_REGION'),
],
],
Docker and Kubernetes Deployment
<?php
// Dockerfile for Laravel
FROM php:8.2-fpm
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip
# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www
# Copy application
COPY . /var/www
# Install dependencies
RUN composer install --no-dev --optimize-autoloader
# Build assets
RUN npm ci && npm run build
# Set permissions
RUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache
EXPOSE 9000
CMD ["php-fpm"]
// docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
image: myapp
container_name: myapp-app
restart: unless-stopped
working_dir: /var/www
volumes:
- ./:/var/www
networks:
- myapp-network
nginx:
image: nginx:alpine
container_name: myapp-nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./:/var/www
- ./docker/nginx:/etc/nginx/conf.d
networks:
- myapp-network
mysql:
image: mysql:8.0
container_name: myapp-mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
networks:
- myapp-network
redis:
image: redis:alpine
container_name: myapp-redis
restart: unless-stopped
networks:
- myapp-network
networks:
myapp-network:
driver: bridge
volumes:
mysql-data:
// Kubernetes deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app
spec:
replicas: 3
selector:
matchLabels:
app: laravel
template:
metadata:
labels:
app: laravel
spec:
containers:
- name: laravel
image: myapp:latest
ports:
- containerPort: 9000
env:
- name: APP_ENV
value: "production"
- name: DB_HOST
value: "mysql-service"
volumeMounts:
- name: storage
mountPath: /var/www/storage
volumes:
- name: storage
persistentVolumeClaim:
claimName: laravel-storage
Blue-Green Deployment Strategy
<?php
// Blue-Green deployment maintains two identical environments
// Only one (blue or green) serves production traffic at a time
// Nginx configuration for blue-green switching
upstream blue {
server blue.myapp.internal:80;
}
upstream green {
server green.myapp.internal:80;
}
# Current active environment
upstream backend {
server blue.myapp.internal:80; # Switch to green when deploying
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
// Deployment process:
// 1. Deploy new version to green (currently inactive)
// 2. Run tests on green environment
// 3. Warm up green environment (cache, database connections)
// 4. Switch nginx upstream from blue to green
// 5. Monitor green for issues
// 6. If issues occur, switch back to blue (instant rollback)
// 7. If stable, keep green active and update blue for next deployment
// Automated blue-green switch script
#!/bin/bash
CURRENT=$(cat /etc/nginx/current_env)
NEW_ENV="green"
if [ "$CURRENT" == "green" ]; then
NEW_ENV="blue"
fi
# Update nginx config
sed -i "s/server $CURRENT.myapp.internal/server $NEW_ENV.myapp.internal/" /etc/nginx/sites-enabled/default
# Reload nginx
nginx -t && systemctl reload nginx
# Update current environment marker
echo $NEW_ENV > /etc/nginx/current_env
echo "Switched from $CURRENT to $NEW_ENV"
Exercise 1: Create a complete zero-downtime deployment script using Envoy. Include tasks for cloning code, installing dependencies, running migrations, optimizing, and atomic symlink switching. Test rollback functionality.
Exercise 2: Set up a Docker-based Laravel development and production environment. Create Dockerfile, docker-compose.yml, and deployment scripts. Include services for app, nginx, MySQL, Redis, and queue workers.
Exercise 3: Implement a blue-green deployment strategy for a Laravel application. Set up two identical environments, create automated switching scripts, and test rollback scenarios. Document the deployment process and failover procedures.