Table Of Contents
- Introduction
- Understanding Laravel Pipeline Structure
- Setting Up GitHub Actions
- Running Tests in CI
- Deployment Strategies
- Adding Notifications
- Best Practices and Troubleshooting
- Final Thoughts
Introduction
After 15 years of manually deploying applications and dealing with "it works on my machine" syndrome, I can confidently say that implementing CI/CD pipelines is one of the best investments you can make as a Laravel developer.
Continuous Integration and Continuous Deployment (CI/CD) automates your testing and deployment processes, ensuring that every code change is validated before reaching production. This not only reduces bugs but also gives you the confidence to deploy multiple times per day without breaking your application.
In this comprehensive guide, I'll walk you through setting up a robust CI/CD pipeline for Laravel using GitHub Actions. We'll cover everything from basic test automation to deploying across different hosting environments, complete with notifications to keep your team informed.
Understanding Laravel Pipeline Structure
Before diving into implementation, let's understand what a typical Laravel CI/CD pipeline should accomplish:
Pipeline Stages
- Code Quality Checks: Static analysis, linting, and code style validation
- Dependency Installation: Composer and npm package installation
- Database Setup: Creating test databases and running migrations
- Test Execution: Unit tests, feature tests, and browser tests
- Build Assets: Compiling CSS/JS for production
- Deployment: Pushing code to staging/production environments
- Notifications: Informing team members about deployment status
Why This Structure Matters
Each stage serves a specific purpose and should fail fast if issues are detected. For example, there's no point in running expensive browser tests if your code doesn't pass basic linting rules. This approach saves both time and compute resources.
Setting Up GitHub Actions
GitHub Actions provides a powerful platform for CI/CD directly integrated with your repository. The workflow files live in .github/workflows/
directory and use YAML syntax.
Basic Workflow Structure
Create .github/workflows/ci.yml
in your Laravel project:
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
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:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: xdebug
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Generate key
run: php artisan key:generate
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Create Database
run: |
mkdir -p database
touch database/database.sqlite
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: vendor/bin/phpunit --coverage-text
Running Tests in CI
Test Configuration
Your Laravel application should have a dedicated testing environment configuration. Update your phpunit.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
<php>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>
Advanced Testing Workflow
For larger applications, I recommend splitting tests into separate jobs for faster execution:
name: Advanced CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
code-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: phpstan, phpcs
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run PHPStan
run: ./vendor/bin/phpstan analyse
- name: Run PHP CS Fixer
run: ./vendor/bin/php-cs-fixer fix --dry-run --diff
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run Unit Tests
run: ./vendor/bin/phpunit --testsuite=Unit
feature-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:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Generate key
run: php artisan key:generate
- name: Run Feature Tests
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
run: |
php artisan migrate --force
./vendor/bin/phpunit --testsuite=Feature
Deployment Strategies
Deploying to Shared Hosting
Shared hosting deployment typically involves FTP/SFTP file transfer. Here's a workflow for cPanel-style hosting:
deploy-shared:
runs-on: ubuntu-latest
needs: [code-quality, unit-tests, feature-tests]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Build assets
run: |
npm ci
npm run build
- name: Create deployment archive
run: |
tar -czf deploy.tar.gz \
--exclude='.git' \
--exclude='.github' \
--exclude='node_modules' \
--exclude='tests' \
--exclude='.env.example' \
.
- name: Deploy to cPanel
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with:
server: ${{ secrets.FTP_HOST }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: ./
exclude: |
.git*
.github/
node_modules/
tests/
.env.example
Deploying to VPS
For VPS deployment, I prefer using SSH with rsync for efficient file transfers:
deploy-vps:
runs-on: ubuntu-latest
needs: [code-quality, unit-tests, feature-tests]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Deploy to VPS
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} '
cd /var/www/your-app &&
git pull origin main &&
composer install --no-dev --optimize-autoloader &&
npm ci && npm run build &&
php artisan migrate --force &&
php artisan config:cache &&
php artisan route:cache &&
php artisan view:cache &&
sudo systemctl reload php8.2-fpm &&
sudo systemctl reload nginx
'
Deploying to Laravel Forge
Laravel Forge simplifies deployment significantly. You can trigger deployments via webhook:
deploy-forge:
runs-on: ubuntu-latest
needs: [code-quality, unit-tests, feature-tests]
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to Laravel Forge
run: |
curl -X POST "${{ secrets.FORGE_DEPLOY_WEBHOOK }}"
- name: Wait for deployment
run: sleep 30
- name: Health check
run: |
curl -f ${{ secrets.APP_URL }}/health || exit 1
Adding Notifications
Slack Notifications
Keep your team informed with Slack notifications:
notify-slack:
runs-on: ubuntu-latest
needs: [deploy-vps]
if: always()
steps:
- name: Slack Notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
text: |
${{ github.repository }} - ${{ github.ref }}
Deployment Status: ${{ job.status }}
Commit: ${{ github.sha }}
Author: ${{ github.actor }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Email Notifications
For critical deployments, email notifications provide an additional layer of communication:
notify-email:
runs-on: ubuntu-latest
needs: [deploy-vps]
if: failure()
steps:
- name: Send email notification
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 587
username: ${{ secrets.SMTP_USERNAME }}
password: ${{ secrets.SMTP_PASSWORD }}
subject: "🚨 Deployment Failed: ${{ github.repository }}"
body: |
Deployment failed for ${{ github.repository }}
Branch: ${{ github.ref }}
Commit: ${{ github.sha }}
Author: ${{ github.actor }}
Please check the GitHub Actions logs for details.
to: dev-team@yourcompany.com
from: ci-cd@yourcompany.com
Best Practices and Troubleshooting
Environment Variables and Secrets
Store sensitive information in GitHub Secrets:
DB_PASSWORD
: Database passwordsSSH_PRIVATE_KEY
: SSH keys for VPS deploymentSLACK_WEBHOOK_URL
: Slack integration webhookFORGE_DEPLOY_WEBHOOK
: Laravel Forge deployment webhook
Performance Optimization
- Use caching: Cache composer dependencies and npm packages
- Parallel jobs: Run independent tests in parallel
- Fail fast: Put quick checks before expensive operations
- Selective deployment: Only deploy when tests pass
Common Issues and Solutions
Issue: Tests fail due to missing PHP extensions Solution: Specify required extensions in the setup-php action
Issue: Database connection errors Solution: Ensure database service is healthy before running tests
Issue: File permission errors Solution: Set proper permissions for storage and bootstrap/cache directories
Issue: Memory limits during testing Solution: Increase PHP memory limit or optimize test data
Complete Production-Ready Workflow
Here's a comprehensive workflow that incorporates all the concepts we've discussed:
name: Production CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
PHP_VERSION: '8.2'
NODE_VERSION: '18'
jobs:
code-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
tools: phpstan, phpcs, php-cs-fixer
- name: Cache Composer packages
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run PHPStan
run: ./vendor/bin/phpstan analyse
- name: Check code style
run: ./vendor/bin/php-cs-fixer fix --dry-run --diff
test:
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
redis:
image: redis:alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: xdebug
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Cache Composer packages
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
- name: Install PHP dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Install Node dependencies
run: npm ci
- name: Build assets
run: npm run build
- name: Generate application key
run: php artisan key:generate
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Run tests
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
REDIS_HOST: 127.0.0.1
REDIS_PORT: 6379
run: |
php artisan migrate --force
./vendor/bin/phpunit --coverage-clover coverage.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
deploy:
runs-on: ubuntu-latest
needs: [code-quality, test]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: |
composer install --no-dev --optimize-autoloader
npm ci
- name: Build production assets
run: npm run build
- name: Deploy to production
run: |
# Your deployment script here
echo "Deploying to production..."
notify:
runs-on: ubuntu-latest
needs: [deploy]
if: always()
steps:
- name: Slack Notification
uses: 8398a7/action-slack@v3
with:
status: ${{ needs.deploy.result }}
channel: '#deployments'
text: |
Deployment Status: ${{ needs.deploy.result }}
Repository: ${{ github.repository }}
Branch: ${{ github.ref }}
Commit: ${{ github.sha }}
Author: ${{ github.actor }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Final Thoughts
Implementing CI/CD for your Laravel applications transforms how you develop and deploy software. The initial setup investment pays dividends through reduced bugs, faster deployment cycles, and increased confidence in your releases.
Start with a basic pipeline covering tests and simple deployment, then gradually add more sophisticated features like parallel testing, multiple environments, and comprehensive notifications. Remember, the best CI/CD pipeline is one that your team actually uses and trusts.
The key is to make your pipeline fast, reliable, and informative. Every minute spent waiting for a slow pipeline is a minute not spent building features your users need.
Ready to level up your Laravel development workflow? Check out my guide on Laravel Testing Best Practices to ensure your CI/CD pipeline catches issues before they reach production. And if you're looking to optimize your application performance, don't miss Laravel Performance Optimization Techniques for advanced strategies that complement your automated deployment process.
Add Comment
No comments yet. Be the first to comment!