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! 🤘