DevOps & Deployment

Streamlining Laravel 12 Deployments with GitHub Actions and Envoy

Automate Laravel 12 deployments with GitHub Actions and Laravel Envoy. Learn CI/CD pipelines, zero-downtime deployments, and production-ready automation strategies.

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

|
03-Nov-2025
10 min read
Streamlining Laravel 12 Deployments with GitHub Actions and Envoy

Introduction

Manual deployments are error-prone, time-consuming, and inconsistent. Modern Laravel development demands automated, reliable deployment pipelines that ensure code quality, run tests, and deploy seamlessly to production servers.

GitHub Actions provides powerful CI/CD automation directly integrated with your repository, while Laravel Envoy offers elegant, SSH-based deployment scripting. Together, they create a robust deployment pipeline that reduces human error, speeds up releases, and enables zero-downtime deployments.

In this comprehensive guide, we'll build a complete deployment pipeline for Laravel 12 using GitHub Actions and Envoy, covering automated testing, deployment strategies, environment management, rollback procedures, and production best practices.


Understanding the Deployment Pipeline

Complete CI/CD Workflow

  1. Trigger – Push to main branch or create release tag
  2. Build – Install dependencies, compile assets
  3. Test – Run unit, feature, and integration tests
  4. Quality Check – Static analysis, code formatting
  5. Deploy – Copy files, run migrations, cache configs
  6. Verify – Health checks, smoke tests
  7. Notify – Team notifications on success/failure

Deployment Strategies

  • Basic Deployment – Direct file replacement (downtime)
  • Symlink Deployment – Atomic switching (zero downtime)
  • Blue-Green Deployment – Parallel environments
  • Rolling Deployment – Gradual server updates

Setting Up Laravel Envoy

1. Installing Envoy

composer require laravel/envoy --dev

2. Basic Envoy Configuration

Create Envoy.blade.php in your project root:

@servers(['production' => 'user@your-server.com'])

@setup
    $repository = 'git@github.com:username/repository.git';
    $baseDir = '/var/www/html';
    $releasesDir = $baseDir . '/releases';
    $currentDir = $baseDir . '/current';
    $newReleaseName = date('Y-m-d_H-i-s');
    $newReleaseDir = $releasesDir . '/' . $newReleaseName;
    $user = 'www-data';
@endsetup

@story('deploy')
    clone_repository
    run_composer
    update_symlinks
    run_migrations
    optimize_laravel
    reload_php_fpm
    cleanup_old_releases
@endstory

@task('clone_repository')
    echo "🚀 Cloning repository..."
    [ -d {{ $releasesDir }} ] || mkdir -p {{ $releasesDir }}
    git clone --depth 1 {{ $repository }} {{ $newReleaseDir }}
    cd {{ $newReleaseDir }}
    git checkout {{ $commit ?? 'main' }}
@endtask

@task('run_composer')
    echo "📦 Installing dependencies..."
    cd {{ $newReleaseDir }}
    composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
@endtask

@task('update_symlinks')
    echo "🔗 Updating symlinks..."
    # Link .env file
    ln -nfs {{ $baseDir }}/.env {{ $newReleaseDir }}/.env
    
    # Link storage directory
    ln -nfs {{ $baseDir }}/storage {{ $newReleaseDir }}/storage
    
    # Update current release symlink
    ln -nfs {{ $newReleaseDir }} {{ $currentDir }}
    
    chown -R {{ $user }}:{{ $user }} {{ $newReleaseDir }}
@endtask

@task('run_migrations')
    echo "🗄️ Running migrations..."
    cd {{ $newReleaseDir }}
    php artisan migrate --force
@endtask

@task('optimize_laravel')
    echo "⚡ Optimizing Laravel..."
    cd {{ $newReleaseDir }}
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
    php artisan event:cache
@endtask

@task('reload_php_fpm')
    echo "🔄 Reloading PHP-FPM..."
    sudo systemctl reload php8.3-fpm
@endtask

@task('cleanup_old_releases')
    echo "🧹 Cleaning up old releases..."
    # Keep only the 5 most recent releases
    cd {{ $releasesDir }}
    ls -dt */ | tail -n +6 | xargs rm -rf
@endtask

@finished
    echo "✅ Deployment completed successfully!"
@endfinished

@error
    echo "❌ Deployment failed!"
@enderror

3. Running Envoy

# Deploy to production
vendor/bin/envoy run deploy --commit=main

# Deploy specific commit
vendor/bin/envoy run deploy --commit=abc123

# Run specific task
vendor/bin/envoy run run_migrations

Advanced Envoy Configuration

1. Zero-Downtime Deployment

@servers(['production' => ['user@server1.com', 'user@server2.com']])

@setup
    $repository = 'git@github.com:username/repo.git';
    $baseDir = '/var/www/html';
    $sharedDir = $baseDir . '/shared';
    $releasesDir = $baseDir . '/releases';
    $currentDir = $baseDir . '/current';
    $release = date('YmdHis');
    $releaseDir = $releasesDir . '/' . $release;
@endsetup

@story('deploy')
    deployment_started
    clone_repository
    create_shared_directories
    link_shared_files
    install_composer_dependencies
    compile_assets
    update_permissions
    run_migrations
    optimize_application
    switch_current_release
    reload_services
    cleanup_old_releases
    deployment_finished
@endstory

@task('deployment_started')
    echo "🚀 Starting deployment..."
@endtask

@task('create_shared_directories')
    echo "📁 Creating shared directories..."
    [ -d {{ $sharedDir }}/storage ] || mkdir -p {{ $sharedDir }}/storage
    [ -d {{ $sharedDir }}/storage/app ] || mkdir -p {{ $sharedDir }}/storage/app
    [ -d {{ $sharedDir }}/storage/framework ] || mkdir -p {{ $sharedDir }}/storage/framework
    [ -d {{ $sharedDir }}/storage/logs ] || mkdir -p {{ $sharedDir }}/storage/logs
@endtask

@task('link_shared_files')
    echo "🔗 Linking shared files..."
    rm -rf {{ $releaseDir }}/storage
    ln -nfs {{ $sharedDir }}/storage {{ $releaseDir }}/storage
    ln -nfs {{ $sharedDir }}/.env {{ $releaseDir }}/.env
@endtask

@task('compile_assets')
    echo "🎨 Compiling assets..."
    cd {{ $releaseDir }}
    npm ci
    npm run build
@endtask

@task('switch_current_release')
    echo "🔄 Switching to new release..."
    ln -nfs {{ $releaseDir }} {{ $currentDir }}
@endtask

@task('reload_services')
    echo "⚙️ Reloading services..."
    sudo systemctl reload php8.3-fpm
    sudo systemctl reload nginx
@endtask

@task('deployment_finished')
    echo "✅ Deployment completed: {{ $release }}"
@endtask

2. Rollback Configuration

@task('rollback')
    echo "⏪ Rolling back deployment..."
    cd {{ $releasesDir }}
    
    # Get previous release
    $previous = $(ls -t | sed -n 2p)
    
    if [ -z "$previous" ]; then
        echo "No previous release found"
        exit 1
    fi
    
    echo "Rolling back to: $previous"
    
    # Switch symlink to previous release
    ln -nfs {{ $releasesDir }}/$previous {{ $currentDir }}
    
    # Reload services
    sudo systemctl reload php8.3-fpm
    
    echo "✅ Rolled back to: $previous"
@endtask

3. Health Checks

@task('health_check')
    echo "🏥 Running health checks..."
    response=$(curl -s -o /dev/null -w "%{http_code}" https://your-app.com/health)
    
    if [ $response -eq 200 ]; then
        echo "✅ Health check passed"
    else
        echo "❌ Health check failed: HTTP $response"
        exit 1
    fi
@endtask

GitHub Actions Configuration

1. Basic CI/CD Workflow

Create .github/workflows/deploy.yml:

name: Deploy to Production

on:
  push:
    branches:
      - main
  workflow_dispatch:

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:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.3
          extensions: mbstring, pdo_mysql, zip
          coverage: none
      
      - name: Install Composer dependencies
        run: composer install --no-interaction --prefer-dist --optimize-autoloader
      
      - name: Copy environment file
        run: cp .env.example .env
      
      - name: Generate application key
        run: php artisan key:generate
      
      - name: Run database migrations
        run: php artisan migrate --force
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password
      
      - name: Run tests
        run: php artisan test
  
  quality:
    name: Code Quality
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.3
      
      - name: Install dependencies
        run: composer install --no-interaction
      
      - name: Run Pint
        run: vendor/bin/pint --test
      
      - name: Run PHPStan
        run: vendor/bin/phpstan analyse
  
  deploy:
    name: Deploy to Production
    needs: [tests, quality]
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.3
      
      - name: Install Composer dependencies
        run: composer install --no-interaction --no-dev
      
      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.8.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
      
      - name: Add server to known hosts
        run: ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
      
      - name: Deploy with Envoy
        run: vendor/bin/envoy run deploy --commit=${{ github.sha }}
        env:
          SERVERS: ${{ secrets.PRODUCTION_SERVER }}
      
      - name: Notify deployment
        if: success()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: '🚀 Deployment successful!'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

2. Multi-Environment Deployment

name: Deploy Multi-Environment

on:
  push:
    branches:
      - main
      - staging
      - develop

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/staging'
    runs-on: ubuntu-latest
    environment: staging
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to Staging
        run: vendor/bin/envoy run deploy
        env:
          SERVERS: ${{ secrets.STAGING_SERVER }}
  
  deploy-production:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to Production
        run: vendor/bin/envoy run deploy
        env:
          SERVERS: ${{ secrets.PRODUCTION_SERVER }}

3. Deployment with Manual Approval

name: Deploy with Approval

on:
  push:
    branches:
      - main

jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: php artisan test
  
  request-approval:
    needs: tests
    runs-on: ubuntu-latest
    environment: production-approval
    
    steps:
      - name: Wait for approval
        run: echo "Waiting for manual approval..."
  
  deploy:
    needs: request-approval
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: vendor/bin/envoy run deploy

Production Best Practices

1. Secrets Management

Store sensitive data in GitHub Secrets:

# Access secrets in workflow
env:
  DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
  AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
  STRIPE_SECRET: ${{ secrets.STRIPE_SECRET }}

2. Deployment Notifications

- name: Notify on Slack
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    fields: repo,message,commit,author,action,eventName,ref,workflow
    webhook_url: ${{ secrets.SLACK_WEBHOOK }}
  if: always()

- name: Notify on Discord
  uses: sarisia/actions-status-discord@v1
  if: always()
  with:
    webhook: ${{ secrets.DISCORD_WEBHOOK }}
    title: "Deployment"
    description: "Build and deploy to production"

3. Database Backup Before Deployment

@task('backup_database')
    echo "💾 Backing up database..."
    cd {{ $currentDir }}
    php artisan backup:run --only-db
@endtask

@story('deploy')
    backup_database
    clone_repository
    # ... rest of deployment
@endstory

4. Maintenance Mode

@task('enable_maintenance')
    echo "🚧 Enabling maintenance mode..."
    cd {{ $currentDir }}
    php artisan down --retry=60 --secret="deployment-secret"
@endtask

@task('disable_maintenance')
    echo "✅ Disabling maintenance mode..."
    cd {{ $newReleaseDir }}
    php artisan up
@endtask

@story('deploy')
    enable_maintenance
    # ... deployment tasks
    disable_maintenance
@endstory

5. Cache Warming

@task('warm_cache')
    echo "🔥 Warming cache..."
    cd {{ $newReleaseDir }}
    
    # Warm route cache
    php artisan route:cache
    
    # Warm config cache
    php artisan config:cache
    
    # Prime application cache
    php artisan cache:warm
@endtask

Troubleshooting Common Issues

1. Permission Issues

# Fix storage permissions
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache

2. Failed Migrations

@task('run_migrations')
    echo "🗄️ Running migrations..."
    cd {{ $newReleaseDir }}
    
    # Test migrations first
    php artisan migrate --pretend
    
    # Run with error handling
    if ! php artisan migrate --force; then
        echo "❌ Migration failed!"
        exit 1
    fi
@endtask

3. Asset Compilation Failures

- name: Build assets with retry
  uses: nick-invision/retry@v2
  with:
    timeout_minutes: 10
    max_attempts: 3
    command: npm run build

Monitoring Deployments

1. Deployment Metrics

@task('send_metrics')
    echo "📊 Sending deployment metrics..."
    
    curl -X POST https://metrics.example.com/deployments \
        -H "Content-Type: application/json" \
        -d '{
            "app": "laravel-app",
            "version": "{{ $release }}",
            "environment": "production",
            "deployed_at": "{{ date('c') }}",
            "deployed_by": "{{ $deployedBy ?? 'GitHub Actions' }}"
        }'
@endtask

2. Error Tracking

- name: Report to Sentry
  if: failure()
  run: |
    curl -X POST https://sentry.io/api/releases/ \
      -H "Authorization: Bearer ${{ secrets.SENTRY_TOKEN }}" \
      -H "Content-Type: application/json" \
      -d '{
        "version": "${{ github.sha }}",
        "projects": ["laravel-app"]
      }'

Advanced Deployment Strategies

1. Canary Deployment

@servers(['canary' => 'user@canary-server.com', 'production' => ['user@prod1.com', 'user@prod2.com']])

@story('canary_deploy')
    deploy_to_canary
    health_check_canary
    deploy_to_production
@endstory

@task('deploy_to_canary', ['on' => 'canary'])
    echo "🐤 Deploying to canary server..."
    # Deployment steps
@endtask

@task('health_check_canary')
    echo "🏥 Monitoring canary for 5 minutes..."
    sleep 300
    # Check error rates, response times
@endtask

2. Blue-Green Deployment

@servers(['blue' => 'user@blue-server.com', 'green' => 'user@green-server.com'])

@task('blue_green_deploy')
    # Deploy to inactive environment
    # Run tests
    # Switch load balancer
    # Keep old environment for rollback
@endtask

Conclusion

Automating Laravel 12 deployments with GitHub Actions and Envoy creates reliable, repeatable deployment pipelines that reduce errors and accelerate releases. By implementing zero-downtime strategies, automated testing, quality checks, and monitoring, you ensure production deployments are safe, fast, and reversible.

Key takeaways:

  • Use GitHub Actions for CI/CD automation
  • Implement Envoy for elegant deployment scripting
  • Enable zero-downtime deployments with symlinks
  • Add comprehensive health checks and rollback procedures
  • Secure deployments with proper secrets management
  • Monitor deployments with metrics and notifications
  • Test deployments in staging before production

Need help setting up automated deployments? NeedLaravelSite specializes in DevOps, CI/CD, and deployment automation. Contact us for expert Laravel development services.


Related Resources:


Article Tags

Laravel deployment Laravel 12 deployment GitHub Actions Laravel Laravel Envoy Laravel CI/CD Laravel automation Zero downtime deployment Laravel Laravel production deployment Continuous deployment Laravel Laravel deployment pipeline Automated Laravel deployment

About the Author

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

Founder & CEO at CentoSquare | Creator of NeedLaravelSite | Helping Businesses Grow with Cutting-Edge Web, Mobile & Marketing Solutions | Building Innovative Products