Laravel CI/CD: Pipelines Every Laravel App Should Use

By Amas
Laravel CI/CD: Pipelines Every Laravel App Should Use

Continuous Integration and Continuous Deployment (CI/CD) have become essential practices in modern software development. For Laravel developers, setting up the right pipelines in your CI/CD process can make the difference between a stable, maintainable codebase and a chaotic, unreliable one.

In this article, we'll walk through the essential pipelines every Laravel project should include in its CI/CD workflow, from basic code quality checks to advanced deployment strategies.

In this article, we assume that you're hosting your code on Github, and you're going to use Github Actions to run your CI/CD pipelines.

At the end of the article I put together a full Github actions pipeline workflow file with all the pipelines that will be discussed here.

So, let's dive in👇

1. PHP Syntax and Linting Checks

Before running tests or deploying your app, the very first and most fundamental check should be: can PHP even parse your code? A syntax error in a single file, a missing semicolon, an unmatched bracket, or a typo in a class definition, can break the entire application.

This step is fast, lightweight, and incredibly valuable as a first line of defense in your CI pipeline. It ensures that all your PHP files are syntactically valid and that nothing would crash your app before it even gets to runtime.

How to use it:
You can run a simple syntax check across your codebase using:

find . -path ./vendor -prune -o -type f -name "*.php" -print | xargs -P 4 -n 1 php -l

This command finds all .php files and runs php -l (PHP’s built-in linter) on each one. It doesn’t execute the code — it just checks if the file is valid PHP.

In GitHub Actions:

- name: PHP Lint
  run: find . -path ./vendor -prune -o -type f -name "*.php" -print | xargs -P 4 -n 1 php -l

Note: the above command ignores checking the "vendor" library, this is primarily to save time while running the pipeline, but in case you want to check the vendor library as well, you'll need to change the command to the following:

find . -type f -name "*.php" -print0 | xargs -0 -n1 php -l

 

2. Code Style Formatting

Once you've confirmed your code is syntactically correct, the next step is to ensure it follows a consistent style across the project. Code style formatting isn't just about aesthetics, it's about maintaining a clean, readable, and collaborative codebase.

When every file follows the same structure, indentation, and naming conventions, it's easier to understand each other's work, spot logic errors, and avoid distracting diffs in code reviews that are purely style-related.

Recommended Tool: Laravel Pint (Laravel's official code style tool)

Laravel Pint is the official Laravel code style tool, built on top of PHP-CS-Fixer. It comes pre-configured with a style guide that matches Laravel's conventions, so you don’t have to spend time customizing rules.

It works out of the box with:

./vendor/bin/pint

In GitHub Actions:

- name: Check Code Style
  run: ./vendor/bin/pint --test

Pro tip:

To enforce formatting on every commit locally, you can integrate Pint into a pre-commit. Check out this tutorial for more on that.

3. Static Analysis

Static analysis is one of the most effective (yet underused) tools in a Laravel developer’s toolbox. Unlike unit tests or runtime checks, static analysis tools scan your code without executing it, and can still detect bugs, type mismatches, unreachable code, incorrect assumptions, and architectural issues.

It acts like a second pair of eyes that never gets tired, helping you catch problems before they become runtime errors, especially helpful in a dynamically typed language like PHP, where many errors only show up during execution.

Recommended Tool: Larastan

Larastan is a Laravel-friendly wrapper around PHPStan. It understands Laravel's "magic" (like __get accessors, route model binding, container resolution, and dynamic relationships) far better than vanilla PHPStan.

Setup example:

composer require --dev nunomaduro/larastan

Then run:

./vendor/bin/phpstan analyse

Choosing the right level for Larastan:

PHPStan (and Larastan) use levels from 0 to 9, with higher levels being stricter. Laravel apps with a real-world complexity should start at Level 5 or 6, which catches meaningful issues without being overly noisy, then gradually increase level as your codebase improves and your team becomes more comfortable.

Set the level in phpstan.neon:

parameters:
  level: 6

And in Github Actions CI:

- name: Run Static Analysis
  run: ./vendor/bin/phpstan analyse

 

4. Unit and Feature Tests

Laravel makes writing tests smooth and expressive, from unit tests to feature and browser tests. But writing tests is only half the job. The real power of testing comes when those tests are run automatically, consistently, and early, which is why integrating them into your CI pipeline is non-negotiable.

- name: Run Tests
  run: php artisan test --parallel

Use the --parallel flag for performance if your test suite is large.

 

5. Database Migration and Seeding Check

Before you even think about deploying your Laravel application, you should have absolute confidence that your database migrations and seeds run cleanly. If your CI pipeline doesn’t validate this, you're one broken migration away from bringing down production.

Even if your application passes all tests, a failed migration on deploy can break features, crash the app, or leave your database in a partially migrated state. That’s why validating your schema changes is a critical step in your CI/CD pipeline.

Laravel makes it easy to run migrations and seeders in an isolated environment. In CI, this usually runs against:

  • A temporary SQLite in-memory database (super fast for small apps).

  • A MySQL/PostgreSQL service container (closer to production environment).

Use the following commands:

php artisan migrate --env=testing
php artisan db:seed --env=testing

GitHub Actions Example Using SQLite (for Speed)

- name: Migrate Database
  run: |
    php artisan migrate --env=testing
    php artisan db:seed --env=testing

You’ll need a phpunit.xml or .env.testing with:

DB_CONNECTION=sqlite
DB_DATABASE=:memory:

This setup is super fast and perfect for smaller apps or quick sanity checks.

 

GitHub Actions Example Using MySQL

For better production parity, use the same database that you'd use on production environment in the testing environment:

- name: Prepare Database (MySQL)
  run: |
    php artisan migrate --force
    php artisan db:seed --force

Make sure your CI job has the correct environment variables (as in the previous test example) and uses a real database service.

 

6. Security Vulnerability Scanning

Security should never be an afterthought, especially when working with open-source dependencies. Most Laravel apps rely on dozens (sometimes hundreds) of Composer packages, and any one of them could introduce a critical vulnerability. That's why running security audits in your CI pipeline is essential.

Tool: Roave/SecurityAdvisories or [GitHub's Dependabot]

Or using composer audit in your CI pipeline:

- name: Run Security Audit
  run: composer audit

 

8. Deployment

Once your Laravel app passes all the previous CI pipeline steps, linting, style checks, static analysis, testing, and security audits — you're in a strong position to automate your deployment process. This is where CI turns into CI/CD (Continuous Deployment).

By automating deployment, you can:

  • Ship features faster and more frequently

  • Eliminate manual errors during deploys

  • Ensure a consistent and repeatable release process

A common trigger for deployment automation is:

  • A push to the main branch

  • The creation of a version tag (e.g., v1.2.0)

 

There are several reliable options to deploy a Laravel app, depending on your hosting environment and preference:

  • Laravel Cloud – a modern deployment platform by Laravel team.
  • Laravel Envoyer – A zero-downtime deployment service from Laravel.

  • Deployer – A PHP-based deployment tool you can self-host and script.

  • GitHub Actions + SSH – DIY method using ssh to trigger deploy commands.

  • Laravel Forge Deploy Hooks – Automatically trigger deploys via Forge when new commits land on main.

 

Example GitHub Actions step:

- name: Deploy via SSH
  run: ssh user@your-server "cd /path/to/project && git pull origin main && php artisan migrate --force"

⚠️ Note: Including full deployment pipelines for the above deployment providers is outside the scope of this tutorial. Each deployment method has its own setup and nuances depending on your infrastructure and goals.


9. Notifications (Optional)

While CI pipelines run quietly in the background, it’s important to keep your team aware of what’s happening, especially when things go wrong. Automated notifications help ensure no one misses a failed build or broken deployment, and they also reinforce a culture of continuous delivery and shared ownership.

Common Notification Channels:

  • Slack (via webhooks)

  • Email (via GitHub Actions or third-party tools)

  • Microsoft Teams, Discord, or other chat tools

Example with Slack:

- name: Slack Notification
  uses: rtCamp/action-slack-notify@v2
  env:
    SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

You can configure this step to:

  • Run only on failure (if: failure())

  • Run always (to show both success and failure)

  • Include rich messages with emojis, custom icons, or commit info

Example with a conditional:

- name: Notify Slack on Failure
  if: failure()
  uses: rtCamp/action-slack-notify@v2
  env:
    SLACK_COLOR: '#FF0000'
    SLACK_MESSAGE: '🚨 Build failed on ${{ github.ref }} by ${{ github.actor }}'
    SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

 

Full Pipeline Example (MySQL)

Below is a full example of a Github actions workflow that includes all the pipelines we discussed above. Create .github/workflows/ci.yml file in your Laravel project and copy the following pipeline definition into it:

name: Laravel CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  laravel-tests:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: testing
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    env:
      DB_CONNECTION: mysql
      DB_HOST: 127.0.0.1
      DB_PORT: 3306
      DB_DATABASE: testing
      DB_USERNAME: root
      DB_PASSWORD: root
      APP_ENV: testing

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: mbstring, bcmath, mysql

      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-progress

      - name: Copy .env
        run: cp .env.example .env

      - name: Generate app key
        run: php artisan key:generate

      # 1. Syntax check
      - name: PHP Syntax Check
        run: find . -path ./vendor -prune -o -type f -name "*.php" -print | xargs -P 4 -n 1 php -l

      # 2. Code style formatting
      - name: Check code style (Pint)
        run: ./vendor/bin/pint --test

      # 3. Static analysis
      - name: Static analysis (Larastan)
        run: ./vendor/bin/phpstan analyse --level=max

      # 4. Migrate and seed database
      - name: Run migrations and seeds
        run: |
          php artisan migrate --force
          php artisan db:seed --force

      # 5. Run tests
      - name: Run tests
        run: php artisan test --no-tty

      # 6. Security audit
      - name: Run security audit
        run: composer audit

#       7. (Optional) Deploy on push to main
#            - name: Deploy to production
#              if: github.ref == 'refs/heads/main' && success()
#              run: ssh user@your-server "cd /var/www/your-app && git pull origin main && php artisan migrate --force"

      # 8. (Optional) Slack notification on failure
#      - name: Notify Slack on Failure
#        if: failure()
#        uses: rtCamp/action-slack-notify@v2
#        env:
#          SLACK_COLOR: '#FF0000'
#          SLACK_MESSAGE: '🚨 Laravel CI pipeline failed on ${{ github.ref }} by ${{ github.actor }}'
#          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

Before this will run smoothly, make sure you configure the following in your repository’s Settings > Secrets and variables:

  • SLACK_WEBHOOK – for Slack notifications

  • SSH keys (optional) for deploy if using ssh

 

Full Pipeline Example (Postgres)

And in case you use Postgres, here is the pipeline for you:

name: Laravel CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  laravel-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_DB: testing
          POSTGRES_USER: root
          POSTGRES_PASSWORD: root
        ports:
          - 5432:5432
        options: >-
          --health-cmd="pg_isready"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    env:
      DB_CONNECTION: pgsql
      DB_HOST: 127.0.0.1
      DB_PORT: 5432
      DB_DATABASE: testing
      DB_USERNAME: root
      DB_PASSWORD: root
      APP_ENV: testing

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: mbstring, bcmath, pdo_pgsql

      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-progress

      - name: Copy .env
        run: cp .env.example .env

      - name: Generate app key
        run: php artisan key:generate

      # 1. Syntax check
      - name: PHP Syntax Check
        run: find . -path ./vendor -prune -o -type f -name "*.php" -print | xargs -P 4 -n 1 php -l

      # 2. Code style formatting
      - name: Check code style (Pint)
        run: ./vendor/bin/pint --test

      # 3. Static analysis
      - name: Static analysis (Larastan)
        run: ./vendor/bin/phpstan analyse --level=max

      # 4. Migrate and seed database
      - name: Run migrations and seeds
        run: |
          php artisan migrate --force
          php artisan db:seed --force

      # 5. Run tests
      - name: Run tests
        run: php artisan test --no-tty

      # 6. Security audit
      - name: Run security audit
        run: composer audit

 

Final Thoughts

A good CI/CD pipeline is like a safety net -> it catches issues early, enforces consistency, and builds trust in your deployment process. You don’t need to implement everything on day one, but aim to evolve your pipeline over time.

Each pipeline piece adds confidence to your development and deployment workflow. If you're building a Laravel SaaS, this setup isn't just helpful, it's essential, so you could sleep better at night :)

Keep building! 🤘

Share this post.
Liked that? Subscribe to our newsletter for more
Ship fast & don't reinvent the wheel

Build your SaaS using SaaSykit

SaaSykit is a SaaS boilerplate that comes packed with all components required to run a modern SaaS software.

Don't miss this

You might also like