If you are still deploying your Laravel app manually, you are taking unnecessary risk. Manual deployments are slow, error‑prone, and impossible to scale when you have multiple environments, feature branches, and teams.
As a senior Laravel developer, you already know you need a CI/CD pipeline: every push should trigger linting, tests, build steps, and a safe, repeatable deployment to production.
In this guide, we’ll set up a Laravel CI/CD pipeline with GitHub Actions that:
- Runs Pint for code style
- Runs PHPUnit (and Pest) tests
- Builds frontend assets (Vite)
- Deploys to Laravel Cloud (or a similar platform)
- Integrates cleanly with tools like LaraCopilot for AI‑assisted workflows
By the end, you’ll have a production‑grade Laravel CI/CD pipeline you can reuse across projects.
Why Your Laravel Project Needs CI/CD
Manual deployment problems you already know:
- You forget a migration or cache clear.
- A “quick hotfix” breaks another area because tests didn’t run.
- You deploy from your laptop with uncommitted changes.
- You can’t easily roll back.
A Laravel CI/CD pipeline with GitHub Actions solves this:
- Every push triggers the same repeatable workflow.
- Style violations fail fast (Pint), before code review.
- Tests run before anything reaches staging/production.
- Deployments happen from a clean, reproducible build, not a developer machine.
Once this is in place, the deployment process becomes:
Push to main → GitHub Actions runs lint + tests + build → auto‑deploy to Laravel Cloud if everything passes.
Architecture of a Laravel CI/CD Pipeline
Before we start writing YAML, let’s design the pipeline at a high level.
1. Trigger Strategy
You typically want:
- On pull requests to
develop/main: run lint + tests (CI). - On push to main (or
release/*): run lint + tests + build + deploy (CD).
2. Pipeline Stages
We’ll structure a full pipeline:
- Code Quality / Lint
- Run Laravel Pint.
- Automated Tests
- Run unit, feature, and integration tests (PHPUnit or Pest).
- Build
- Install Node dependencies.
- Build frontend assets with Vite.
- Deploy
- Ship to Laravel Cloud (or another host) using an API key/CLI.
You can keep these as a single GitHub Actions workflow with multiple jobs, or split into separate workflows (CI and CD). For clarity, we’ll show a combined approach and call out where you might split.
Prerequisites
Before setting up the workflow, make sure you have:
- A Laravel app in a GitHub repo.
- GitHub Actions enabled for the repository.
- A Laravel Cloud project or another hosting target (e.g., Forge, Vapor, custom server).
- Deployment credentials:
- API token or SSH key for your hosting provider.
- Stored as GitHub Secrets.
- PHP test suite in place (PHPUnit / Pest).
- Laravel Pint installed (globally or via
devdependency).
For Laravel Pint via Composer:
composer require laravel/pint --dev
Setting Up GitHub Secrets
You should never hard‑code secrets in your workflow file. Instead, configure:
LARAVEL_CLOUD_API_TOKENLARAVEL_CLOUD_PROJECT_ID- Any environment‑specific variables:
APP_ENV,APP_KEY,DB_*if needed for integration tests.
In GitHub:
- Go to your repo → Settings → Secrets and variables → Actions.
- Click New repository secret.
- Add your tokens/keys.
We’ll reference these secrets in the YAML later.
Base GitHub Actions Workflow File
Create the workflow:
mkdir -p .github/workflows
touch .github/workflows/laravel-ci-cd.yml
We’ll build it step by step.
Step 1: Define Triggers and Basic Configuration
Start with the top of the workflow:
name: Laravel CI CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main, develop ]
jobs:
# Jobs will go here
- Pull requests to
mainordevelop: run CI (lint + tests). - Push to main: run full CI + CD (including deploy).
Step 2: CI Job – Lint and Test
We’ll create a ci job that runs for both PRs and pushes.
jobs:
ci:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8
env:
MYSQL_DATABASE: laravel
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -u laravel --password=secret"
--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.2'
extensions: mbstring, intl, pdo_mysql
coverage: none
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist --no-progress
- name: Copy .env
run: cp .env.example .env
- name: Generate app key
run: php artisan key:generate
- name: Configure test database
run: |
php artisan config:clear
php artisan migrate --force
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: laravel
DB_USERNAME: laravel
DB_PASSWORD: secret
- name: Run Laravel Pint
run: ./vendor/bin/pint
If you want Pint to only check and not auto‑fix, add --test:
- name: Run Laravel Pint
run: ./vendor/bin/pint --test
Now add the test step (PHPUnit or Pest):
- name: Run tests
run: php artisan test
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: laravel
DB_USERNAME: laravel
DB_PASSWORD: secret
At this point, you already have a functioning CI pipeline:
- Every PR and push runs Pint and tests.
- Failures block merges and deployments.
Step 3: Build Job – Frontend Assets (Vite)
For many apps, you also need to build assets before deploying. We’ll create a dedicated build job that depends on ci.
build:
runs-on: ubuntu-latest
needs: ci
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Cache node modules
uses: actions/cache@v4
with:
path: node_modules
key: node-${{ hashFiles('**/package-lock.json') }}
restore-keys: node-
- name: Install Node dependencies
run: npm ci
- name: Build assets
run: npm run build
Notes:
- We restrict this job to main using
if: github.ref == 'refs/heads/main'. - You can later archive built assets as artifacts, or deploy from this job directly.
Step 4: CD Job – Deploy to Laravel Cloud
Now we’ll add a deploy job that runs after build succeeds and only on main.
You’ll need a deployment mechanism:
- Laravel Cloud CLI or API.
- Or another provider’s CLI (Forge, Vapor, etc.).
- We’ll assume a simple CLI/API command.
Example:
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Composer dependencies (production)
run: composer install --no-interaction --prefer-dist --no-dev --no-progress
- name: Install Node dependencies
run: npm ci
- name: Build assets
run: npm run build
- name: Deploy to Laravel Cloud
env:
LARAVEL_CLOUD_API_TOKEN: ${{ secrets.LARAVEL_CLOUD_API_TOKEN }}
LARAVEL_CLOUD_PROJECT_ID: ${{ secrets.LARAVEL_CLOUD_PROJECT_ID }}
run: |
# Example pseudo command – replace with real CLI/API call
curl -X POST \\
-H "Authorization: Bearer $LARAVEL_CLOUD_API_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{"project_id": "'"$LARAVEL_CLOUD_PROJECT_ID"'", "branch": "main"}' \\
<https://api.laravelcloud.com/deploy>
Adjust the deploy command to match your actual provider. The key ideas:
- Deployment runs only after CI and build pass.
- Deployment runs only for main.
- Secrets stay in GitHub, not in the repo.
Handling Multiple Environments (Staging, Production)
Senior teams often want:
- PR → run CI only.
- Merge to
develop→ deploy to staging. - Merge to
main→ deploy to production.
You can handle this using environments:
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: production
url: <https://your-production-app.com>
if: github.ref == 'refs/heads/main'
And a second deploy job for staging:
deploy_staging:
runs-on: ubuntu-latest
needs: ci
environment:
name: staging
url: <https://staging-your-app.com>
if: github.ref == 'refs/heads/develop'
# staging deploy steps...
GitHub environments allow:
- Manual approvals before production deploy.
- Environment‑specific secrets.
Optimizing Performance: Caching and Matrix Builds
For mature teams, the next pain point is pipeline speed. Some optimizations:
Composer and Node Caching
We already added caches, but you can tune them if you have multiple workflows:
- Use consistent keys across workflows.
- Cache built Vite assets if needed.
Matrix for PHP Versions
If you maintain packages or broad PHP version support, use a matrix:
ci:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.1', '8.2']
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, intl, pdo_mysql
You can still restrict deploy to a single run on a specific version.
Integrating Laravel GitHub Actions With Automated Testing Culture
The value of a Laravel GitHub Actions pipeline depends on the quality of your test suite and conventions. A few patterns that work well in senior teams:
- Fail fast on style: run Pint first. No point running tests if the codebase doesn’t meet standards.
- Separate quick tests and slow suites:
- Run unit tests on every PR.
- Run full integration/end‑to‑end tests on
mainand nightly.
- Use coverage for critical modules only: keep CI fast by avoiding heavy coverage runs on every push.
You can also integrate code coverage or static analysis (PHPStan, Psalm):
- name: Run PHPStan
run: ./vendor/bin/phpstan analyse --memory-limit=1G
Connecting This Pipeline With AI-Powered Tools (LaraCopilot)
Your CI/CD pipeline is already doing what it’s supposed to do. It runs Pint, executes tests, builds your app, and deploys it. That part is not the problem.
The real friction shows up before CI even starts, when developers push code that isn’t fully ready. You’ve probably seen this yourself: a feature gets pushed, CI runs, and something fails. Maybe formatting is off, maybe a test is missing, maybe a small edge case was ignored. The pipeline fails, the developer fixes it, pushes again, and waits for CI to rerun. This loop repeats more often than it should.
That’s exactly where LaraCopilot fits in not inside your CI/CD pipeline, but right before it.
Instead of writing everything manually and relying on CI to catch issues, you start with intent. You describe what you want to build, and LaraCopilot helps generate structured Laravel code, including a solid starting point for tests and code that already follows common formatting conventions. It’s not about replacing your workflow, it’s about reducing the friction inside it.
What changes in practice is simple. Instead of “write → push → fail → fix → push again,” your flow becomes “generate → refine → push → pass.” CI/CD still does its job, but now it’s validating good code instead of catching avoidable mistakes.
Because your pipeline runs on GitHub Actions, this fits naturally into your existing setup. Every push still triggers linting, testing, and deployment. The difference is that developers spend less time reacting to CI failures and more time building actual features.
The key thing to understand is this: CI/CD doesn’t improve your code, it enforces standards. The real improvement happens before the code ever reaches the pipeline. That’s the gap LaraCopilot helps close.
Common Pitfalls and How to Avoid Them
Even senior Laravel developers run into similar issues when setting up a Laravel deployment pipeline for the first time:
- Environment drift
- Local
.envand CI.envdiffer. - Fix: explicitly define environment variables in CI and ensure they match staging/production config.
- Local
- Database dependency in tests
- Tests rely on a database state not properly seeded in CI.
- Fix: use migrations + seeders or dedicated test factories in the CI job.
- Long‑running pipelines
- Too many heavy tests or unnecessary steps on every push.
- Fix: split workflows (PR vs main) and move heavy jobs to nightly or main branch only.
- Fragile deploy scripts
- Hand‑rolled scripts that break with small environment changes.
- Fix: prefer provider CI integrations or stable CLIs, keep scripts minimal and idempotent.
Putting It All Together: Example Full Workflow
Here is a consolidated example you can adapt:
name: Laravel CI CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
ci:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8
env:
MYSQL_DATABASE: laravel
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -u laravel --password=secret"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, intl, pdo_mysql
- uses: actions/cache@v4
with:
path: vendor
key: composer-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist --no-progress
- name: Copy .env
run: cp .env.example .env
- name: Generate app key
run: php artisan key:generate
- name: Run migrations
run: php artisan migrate --force
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: laravel
DB_USERNAME: laravel
DB_PASSWORD: secret
- name: Run Laravel Pint
run: ./vendor/bin/pint --test
- name: Run tests
run: php artisan test
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: laravel
DB_USERNAME: laravel
DB_PASSWORD: secret
build:
runs-on: ubuntu-latest
needs: ci
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: actions/cache@v4
with:
path: node_modules
key: node-${{ hashFiles('**/package-lock.json') }}
restore-keys: node-
- name: Install Node dependencies
run: npm ci
- name: Build assets
run: npm run build
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
environment:
name: production
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Composer dependencies (production)
run: composer install --no-interaction --prefer-dist --no-dev --no-progress
- name: Install Node dependencies
run: npm ci
- name: Build assets
run: npm run build
- name: Deploy to Laravel Cloud
env:
LARAVEL_CLOUD_API_TOKEN: ${{ secrets.LARAVEL_CLOUD_API_TOKEN }}
LARAVEL_CLOUD_PROJECT_ID: ${{ secrets.LARAVEL_CLOUD_PROJECT_ID }}
run: |
# Replace with your actual deployment command
curl -X POST \\
-H "Authorization: Bearer $LARAVEL_CLOUD_API_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{"project_id": "'"$LARAVEL_CLOUD_PROJECT_ID"'", "branch": "main"}' \\
<https://api.laravelcloud.com/deploy>
Use this as a template and adapt for your hosting environment and stack.
Where to Go Next
Once your Laravel CI/CD pipeline with GitHub Actions is running end-to-end, you’ve already solved the hardest part. Your deployments are no longer manual, your tests run automatically, and your code is validated before it reaches production.
From here, the focus shifts from “getting CI/CD working” to making it smarter and more reliable over time.
You can start tightening quality by introducing deeper checks like static analysis (PHPStan or Psalm) or even mutation testing if you want to push test reliability further. As your application grows, you might also look into safer deployment strategies like canary releases or blue-green deployments, especially if downtime or risky releases become a concern.
This is also the stage where your development workflow and CI/CD pipeline start to connect more closely. Tools like LaraCopilot can help earlier in the process during development and pull requests, so that by the time code reaches your pipeline, it’s already cleaner, better structured, and less likely to fail.
If you want to go deeper into deployment automation and AI-assisted Laravel workflows, it’s worth exploring how modern teams are combining both to move faster without breaking things.
And if you’ve made it this far, you already know the real shift isn’t just automation, it’s confidence.
You’re no longer shipping from your laptop.
You’re shipping through a system.