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
- Trigger – Push to main branch or create release tag
- Build – Install dependencies, compile assets
- Test – Run unit, feature, and integration tests
- Quality Check – Static analysis, code formatting
- Deploy – Copy files, run migrations, cache configs
- Verify – Health checks, smoke tests
- 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: