GitHub Actions for Laravel Testing and Deploy

Khimananda Oli 8 min read CI/CD and Automation
GitHub Actions for Laravel Testing and Deploy

By Khimananda Oli | Last reviewed: August 2026

Shipping Laravel applications without automated verification is a liability, not a strategy. Implementing GitHub Actions for Laravel testing and deploy eliminates manual release friction while enforcing quality gates before code reaches production. This guide provides the exact workflow configurations, security patterns, and deployment scripts I use to manage Laravel infrastructure for clients ranging from Kathmandu startups to global SaaS platforms.

Git Pushmain / PRTest JobPHP 8.3 / 8.4 MatrixComposer Install (Cached)Pest / PHPUnitStatic AnalysisCode Coverage GateBuild ArtifactOptimized BundleDeploy ProdZero-DowntimeGitHub Actions for Laravel Testing and Deploy Pipeline
End-to-end GitHub Actions for Laravel testing and deploy workflow from commit to production

How do you configure GitHub Actions for Laravel testing and deploy with matrix builds?

A robust CI configuration must validate your application against every supported runtime. In 2026, most Laravel projects support PHP 8.3 and 8.4 simultaneously, and your pipeline must catch version-specific regressions automatically. The following workflow establishes a complete testing foundation with dependency caching and proper environment isolation.

<!-- .github/workflows/laravel-tests.yml -->
name: Laravel Test Suite

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

jobs:
  tests:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ['8.3', '8.4']

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, bcmath, ctype, fileinfo, json, openssl, pdo, tokenizer, xml, curl
          coverage: xdebug

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: composer-${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
          restore-keys: composer-${{ runner.os }}-${{ matrix.php }}-

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

      - name: Prepare environment
        run: |
          cp .env.ci .env
          php artisan key:generate
          php artisan config:cache

      - name: Run tests
        run: ./vendor/bin/pest --coverage-clover=coverage.xml

      - name: Upload coverage
        if: matrix.php == '8.4'
        uses: codecov/codecov-action@v4
        with:
          file: coverage.xml

Environment preparation matters

Never run tests against your local .env or production secrets. Create a dedicated .env.ci file committed to your repository containing safe defaults: SQLite or an ephemeral MySQL service, dummy app keys, and disabled external integrations. This file should mirror your production structure exactly but with sanitized values. For database-dependent tests, add a service container rather than mocking persistence layers—this catches real query issues that mocks hide.

Caching strategy for faster feedback

The cache key above includes both the OS and PHP version because compiled dependencies are platform-specific. A common mistake is using only hashFiles('composer.lock'), which causes cross-version cache corruption when running matrix builds. The restore-keys fallback ensures partial cache hits when dependencies change slightly, reducing cold-start install times from 90 seconds to under 15.

What is the best way to handle secrets securely in Laravel CI/CD pipelines?

Secret management separates professional pipelines from amateur ones. Your Laravel application needs database credentials, API keys, and potentially cloud provider tokens during both testing and deployment phases. Hardcoding these in workflow files or committing them to any branch is an immediate security failure.

  • GitHub Encrypted Secrets: Store sensitive values like DB_PASSWORD, AWS_ACCESS_KEY_ID, and SSH private keys in repository settings under Secrets & Variables → Actions. These are encrypted at rest and never exposed in logs, even on failure.
  • Environment-scoped secrets: Use GitHub Environments (staging, production) to restrict secret access to specific branches and require approval gates. Production secrets should never be accessible from feature branch workflows.
  • OIDC over long-lived keys: For AWS, Azure, or GCP deployments, configure OpenID Connect federation instead of storing static credentials. This grants temporary, scoped tokens valid only for the duration of the workflow run. See my guide on deploying to AWS from GitHub Actions with OIDC for implementation details.
  • Rotate and audit: Treat CI secrets like production credentials. Rotate quarterly, audit access logs monthly, and immediately revoke any secret that appeared in a public fork or log output.

For Laravel specifically, inject secrets as environment variables during the workflow step, never as build artifacts. The .env generated during testing should be discarded after the job completes. During deployment, secrets should be injected directly into the server's environment or fetched from a vault at runtime—not baked into Docker images or tarballs.

GitHub SecretsEncrypted at RestEnv-Scoped AccessWorkflow RunnerEphemeral VMNo Persistent StateOIDC ProviderShort-Lived TokenScoped PermissionsProduction ServerRuntime Env OnlyVault IntegrationSecrets Never Touch Disk, Images, or Logs — Injected at Runtime Only
Secure secret lifecycle in GitHub Actions for Laravel testing and deploy environments

How do you deploy Laravel to a VPS with zero downtime using GitHub Actions?

Deployment is where most Laravel CI/CD guides fall short. They show test configurations but leave you guessing about atomic releases. Zero-downtime deployment means new code serves requests only after health checks pass, with instant rollback capability if anything fails.

I recommend Deployer for VPS targets because it handles symlinks, shared directories, and rollbacks natively. Your GitHub Actions workflow triggers Deployer only after the test job succeeds on the main branch:

<!-- Add to laravel-tests.yml after test job -->
  deploy:
    needs: tests
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Setup Deployer
        uses: shimataro/ssh-key-action@v2
        with:
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          known_hosts: ${{ secrets.SSH_KNOWN_HOSTS }}

      - name: Deploy to production
        run: |
          curl -LO https://deployer.org/deployer.phar
          php deployer.phar deploy production \
            --branch=${{ github.sha }} \
            -o keep_releases=5

Atomic release mechanics

Deployer creates timestamped release directories under /var/www/app/releases/, symlinks shared storage and .env files, runs migrations and cache warmup, then atomically swaps the current symlink. If any step fails, the symlink never changes and traffic continues hitting the previous release. The keep_releases=5 parameter maintains rollback history without consuming excessive disk space.

Health check verification

Add a post-deploy health check step that curls your application's /up endpoint (Laravel 11+ ships this by default). Fail the workflow explicitly if the health check returns non-200 within 60 seconds. This catches migration failures, missing environment variables, and permission errors that syntactically valid deployments still produce. Without this gate, you'll discover problems from user reports instead of automated alerts.

How does GitHub Actions compare to GitLab CI for Laravel projects in 2026?

Choosing between CI platforms depends on your team's existing ecosystem, budget, and compliance requirements. Both handle Laravel competently, but operational differences matter at scale. Teams evaluating GitHub Actions versus GitLab CI should weigh these concrete trade-offs:

CriteriaGitHub ActionsGitLab CI
Laravel ecosystem integrationNative marketplace actions for PHP, Node, databasesStrong templates but fewer community-maintained Laravel-specific jobs
Self-hosted runnersSupported; requires manual provisioning and security hardeningBuilt-in runner manager with auto-scaling and Kubernetes integration
Matrix build syntaxClean YAML with fail-fast controlParallel keyword with slightly more verbose configuration
Secret managementEnvironment-scoped with OIDC federationHierarchical group/project variables with masking
Pricing (private repos)2,000 minutes/month free; per-minute overage400 compute minutes/month free; self-hosted unlimited
Compliance evidenceAudit logs exportable; SOC 2 compatible with enterprise planBuilt-in audit trails and approval gates on all tiers

For teams already hosting code on GitHub and deploying to AWS or Azure, GitHub Actions reduces context switching and integrates cleanly with OIDC. For organizations requiring extensive self-hosted infrastructure, on-premise compliance, or deeply integrated merge request workflows, GitLab CI's runner architecture and built-in DevSecOps tooling provide stronger defaults. Neither is universally superior—the right choice follows from your existing platform commitments and scaling trajectory.

Where is your code hosted?GitHubGitLab / Self-hostedNeed self-hosted runners?Require on-prem compliance?YesNoYesNoGH Actions + Self-HostedGitHub Actions CloudGitLab Self-ManagedGitLab SaaSChoose Based on Existing Platform, Not Feature Checklists
Decision framework for selecting CI platform for Laravel projects in 2026

Automate Your Laravel Releases With Confidence

Implementing GitHub Actions for Laravel testing and deploy transforms your release process from a source of anxiety into a competitive advantage. Start with the matrix test configuration above, add secret scoping before your first production deploy, and integrate health checks before trusting any automation. The time invested in getting this right pays compound returns in developer velocity, incident reduction, and audit readiness. If your team needs help designing a compliant, scalable Laravel CI/CD pipeline tailored to your infrastructure, reach out to discuss your specific requirements.

Frequently Asked Questions

Create a workflow YAML file in .github/workflows specifying ubuntu-latest, setting up PHP 8.4 via shivammathur/setup-php, installing Composer dependencies, generating an app key, running migrations on SQLite or MySQL service containers, executing PHPUnit tests, and deploying via SSH or rsync only after successful test completion.

Yes.

Use shivammathur/setup-php@v2 as it preinstalls required extensions like mbstring, xml, ctype, iconv, mysql, pdo_sqlite, and bcmath needed by Laravel 12. It supports caching Composer packages natively and allows specifying exact PHP versions matching your production environment to prevent deployment drift.

Enable the built-in composer-cache option in shivammathur/setup-php or use actions/cache@v4 targeting vendor directory with hashFiles('composer.lock') as the key. This reduces dependency installation time from minutes to seconds on subsequent runs while ensuring lock file changes trigger fresh installs automatically.

Yes, install chromium-driver and start Xvfb virtual display before running php artisan dusk. Configure Dusk to use headless Chrome with no-sandbox flags since Actions runners lack GUI. Store screenshots and logs as artifacts using actions/upload-artifact for debugging failed browser tests in CI environments without local reproduction.

Store SSH keys, API tokens, and database passwords as encrypted repository or organization secrets under Settings then Secrets. Reference them as ${{ secrets.DEPLOY_KEY }} in workflows. Never hardcode credentials in YAML files. Rotate keys regularly and use deploy-specific keys with minimal permissions rather than personal access tokens.

Usually missing SSH key configuration or incorrect file permissions on remote server directories. Ensure the private key is added via webfactory/ssh-agent action, verify known_hosts includes target server fingerprint, and confirm deployment user owns release paths. Check that storage and bootstrap/cache directories are writable by web server user.

For testing, use SQLite in-memory or MySQL service containers defined in workflow services section. Never run migrations against production databases during test jobs. For deployment workflows, execute php artisan migrate --force only after successful tests and within atomic deployment scripts that support rollback on failure to prevent partial schema changes.

Yes.

Cache Composer dependencies and node_modules, use setup-php built-in extension caching, run npm ci instead of npm install, skip unnecessary steps on non-main branches using conditional expressions, and split long-running test suites across matrix strategies. Consider self-hosted runners for large monorepos to eliminate cold start overhead entirely.

Missing APP_KEY generation causing encryption failures, forgetting php artisan config:cache leading to stale configurations, not running storage:link creating broken asset paths, skipping queue restart after code updates leaving old workers active, and deploying without verifying migration status first which causes runtime errors on schema mismatches.

GitHub Actions offers tighter integration with GitHub repositories, larger marketplace of prebuilt Laravel actions, and generous free tier for public repos. GitLab CI provides superior self-hosted runner management and built-in container registry. Both handle Laravel testing equally well, but Actions reduces boilerplate through community-maintained PHP and deployment actions significantly.

Define separate test and deploy jobs in same workflow where deploy job uses needs: [test] directive ensuring execution only upon test job success. Alternatively use branch protection rules requiring status checks before merging to main, triggering deployment workflow exclusively on protected branch pushes to guarantee tested code reaches production environments always.

Add tmate or mxschmitt/action-tmate step for interactive SSH debugging sessions, enable verbose PHPUnit output with --debug flag, upload test logs and screenshots as artifacts, use act tool locally to reproduce workflow behavior, and check runner system logs via actions/upload-artifact targeting /var/log for infrastructure-level issues affecting test execution.

Laravel 10 through 12 work identically with GitHub Actions using PHP 8.2 to 8.4. Older Laravel versions may require specific PHP version pinning and legacy dependency handling. Always match CI PHP version exactly to production to avoid subtle behavioral differences. No framework modifications are needed solely for Actions integration regardless of Laravel release.