Advanced Laravel

Advanced Deployment Strategies

18 min Lesson 30 of 40

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.