Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

GitHub Actions for Laravel Testing and Deploy

By Kokil Thapa | Last reviewed: August 2026

Setting up GitHub Actions for Laravel testing and deploy eliminates the fragile manual processes that break production sites during late-night releases. For developers building business-critical applications, whether legal-tech portals or eCommerce platforms, automated pipelines ensure every commit is validated against PHP 8.4 and Node 22 LTS before reaching your server. This guide provides battle-tested workflow configurations derived from maintaining production Laravel systems since 2010, focusing on practical reliability over theoretical perfection.

If you are evaluating whether to build this infrastructure yourself or bring in specialist help, understanding the scope is essential. Many teams I work with as a Laravel developer in Nepal start with basic test automation before graduating to full zero-downtime deployments. The patterns below reflect what actually works in production environments where budget constraints mean you cannot afford flaky pipelines or servers left in broken states after failed deploys.

How Do You Configure GitHub Actions for Laravel Testing and Deploy Workflows?

The foundation of reliable GitHub Actions for Laravel testing and deploy is separating concerns into distinct workflow files. Never combine testing and deployment into a single monolithic YAML file; debugging failures becomes exponentially harder when you cannot tell if a problem originated in the test suite or the deployment script.

Laravel CI/CD Pipeline ArchitectureCI Workflow (tests.yml)PHP 8.4 + MySQL 8.4Composer Install (cached)Vite Build (Node 22)PHPUnit / Pest SuiteCD Workflow (deploy.yml)Trigger: main branch pushSSH Key AuthenticationDeployer 7 ReleaseOPcache Reset + Health CheckOnly on Test SuccessShared InfrastructureGitHub Secrets: SSH_PRIVATE_KEY, DB_PASSWORD, APP_KEYArtifact Storage: Vite manifest, compiled assetsEnvironment Protection Rules: Required reviewers for production
Separating CI and CD workflows prevents deployment when tests fail and simplifies debugging each stage independently

Create .github/workflows/tests.yml for continuous integration. This workflow must run on every pull request and push to feature branches. The critical mistake many developers make is skipping database migrations or using SQLite when production runs MySQL 8.4; schema differences between engines cause tests to pass locally but fail in staging. Always use service containers that match your production database version exactly.

<!-- .github/workflows/tests.yml -->
name: Laravel Tests

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

jobs:
  test:
    runs-on: ubuntu-24.04
    services:
      mysql:
        image: mysql:8.4
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: laravel_test
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping -h localhost"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP 8.4
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: mbstring, xml, ctype, iconv, intl, pdo_mysql, bcmath
          coverage: none

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

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

      - name: Setup Node 22
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Build assets with Vite
        run: |
          npm ci
          npm run build

      - name: Prepare Laravel environment
        run: |
          cp .env.example .env
          php artisan key:generate
          php artisan migrate:fresh --force
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: laravel_test
          DB_USERNAME: root
          DB_PASSWORD: password

      - name: Run tests
        run: php artisan test --parallel
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: laravel_test
          DB_USERNAME: root
          DB_PASSWORD: password

This configuration pins Ubuntu 24.04 rather than ubuntu-latest because GitHub periodically updates the latest tag, which can introduce breaking changes without warning. On client projects where stability matters more than having the newest OS packages, explicit version pinning has prevented numerous mysterious CI failures. The parallel test flag leverages multiple cores and typically cuts execution time by 40–60% on suites with over 200 tests.

What Is the Best Deployment Strategy for Laravel via GitHub Actions?

For production deployments, zero-downtime release strategies using symlinked directories remain the gold standard in 2026. While tools like Envoyer or Laravel Cloud offer managed solutions, self-hosted deployment via Deployer 7 gives you complete control at a fraction of the cost—critical when billing clients in NPR where SaaS subscription fees compound quickly across multiple projects.

StrategyDowntimeRollback SpeedComplexityBest For
Direct rsync/scp30–120 secondsManual, slowLowStaging, hobby projects
Deployer 7 symlinked releasesZero (atomic swap)Instant (previous release)MediumProduction business apps
Docker/container deploymentNear-zeroImage rollbackHighMicroservices, large teams
Laravel Cloud / EnvoyerZeroOne-clickLowTeams avoiding DevOps overhead

I have standardized on Deployer 7 for most Laravel projects because it handles shared directories (.env, storage/), manages release symlinks atomically, and integrates cleanly with GitHub Actions through simple SSH commands. The deployment workflow should only trigger after the test workflow succeeds on protected branches, creating a hard gate that prevents untested code from reaching production regardless of developer urgency.

<!-- .github/workflows/deploy.yml -->
name: Deploy to Production

on:
  workflow_run:
    workflows: ["Laravel Tests"]
    types:
      - completed
    branches: [main]

jobs:
  deploy:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-24.04
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP 8.4
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'

      - name: Install Deployer
        run: |
          curl -LO https://deployer.org/deployer.phar
          mv deployer.phar /usr/local/bin/dep
          chmod +x /usr/local/bin/dep

      - name: Configure SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy with Deployer
        run: dep deploy production --no-interaction
        env:
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}

      - name: Notify on failure
        if: failure()
        run: |
          curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
            -d '{"text":"❌ Production deploy failed for ${{ github.repository }}"}'

The environment: production declaration enables GitHub's environment protection rules, allowing you to require manual approval before deployment even when tests pass. For legal-tech platforms handling sensitive client data or eCommerce sites processing payments, this additional checkpoint has caught issues that automated tests missed—like misconfigured payment gateway credentials or missing environment variables that only surface during actual transaction processing.

Zero-Downtime Deploy Sequence (Deployer 7)1. Upload Codersync to new release dir/releases/20260816120000/2. Shared LinksSymlink .env, storage/from /shared/ directory3. Composer/ViteInstall deps, build assetsIn isolated release dir4. Atomic Symlink Swapln -sfn new_release current+ OPcache reset + health checkServer Directory Structure After Deploy/var/www/site/current → /var/www/site/releases/20260816120000//var/www/site/releases/20260816120000/.env → ../../shared/.env/var/www/site/releases/20260816120000/storage → ../../shared/storage/var/www/site/releases/20260815100000/ (previous, kept for rollback)/var/www/site/shared/.env, storage/, vendor/ (persistent across releases)
Deployer creates timestamped release directories and atomically swaps the current symlink, ensuring zero downtime and instant rollback capability

How Do You Handle Environment Variables and Secrets Securely in GitHub Actions?

Secret management is where most Laravel CI/CD implementations fail security audits. Never commit .env files, never echo secrets to logs, and never pass sensitive values as workflow inputs that appear in the GitHub UI. Use GitHub's encrypted secrets exclusively, and structure them to mirror your application's environment variable names for clarity.

  • Repository secrets for values shared across all environments (API keys for third-party services like eSewa or Khalti payment gateways).
  • Environment secrets for deployment-specific values (database passwords, app keys) scoped to staging or production environments.
  • OIDC authentication instead of long-lived SSH keys when possible, rotating credentials automatically without manual intervention.

On legal-tech projects handling sensitive client information, I enforce environment protection rules requiring two-person approval for production deploys. This catches configuration drift that tests cannot validate—like a staging database password accidentally copied to production secrets. The extra thirty seconds of approval time prevents hours of incident response and potential data exposure that would violate client confidentiality agreements.

For applications integrating Nepal payment gateways, store webhook signing secrets and API credentials as environment secrets rather than repository secrets. This allows different keys for staging sandbox environments versus production live environments without conditional logic in your workflow files. When debugging payment callback failures—a common issue documented in my Laravel payment integrations guide—having correctly scoped secrets eliminates an entire category of "works in staging, fails in production" problems.

Why Are Your Laravel GitHub Actions Slow and How Do You Fix Them?

Slow CI pipelines kill developer productivity and encourage skipping tests entirely. If your GitHub Actions for Laravel testing and deploy takes longer than eight minutes, you are losing momentum on every pull request. The three biggest performance killers in Laravel workflows are uncached Composer installs, redundant Node module installations, and sequential test execution.

CI Runtime Optimization: Before vs AfterBEFORE (Unoptimized): ~14 minutesComposer install (no cache): 3m 20snpm install (no cache): 2m 45sSequential tests (single thread): 6m 10sVite build: 1m 45sAFTER (Optimized): ~4 minutesComposer (cached): 25snpm ci (cached): 18sParallel tests (--parallel): 2m 30sVite build: 1m 40s70% fasterKey Optimizations Applied✓ actions/cache@v4 for vendor/ and node_modules/ keyed on lock file hashes✓ php artisan test --parallel (requires brianium/paratest package)✓ npm ci instead of npm install (deterministic, faster with cache)✓ coverage: none in setup-php unless generating reports (saves ~45s)
Caching dependencies and enabling parallel test execution reduces typical Laravel CI runtime from 14 minutes to under 4 minutes

Enable Composer caching using actions/cache@v4 keyed on composer.lock hash. Without caching, every workflow run downloads and extracts 200+ packages from scratch. With caching enabled, subsequent runs restore the vendor directory in under ten seconds. Apply identical caching for node_modules using package-lock.json as the cache key. Always use npm ci instead of npm install in CI; it respects the lock file exactly and fails fast on mismatches rather than silently resolving different versions.

Install brianium/paratest and add the --parallel flag to your test command. Laravel's built-in parallel testing support in version 12.x wraps ParaTest and automatically provisions separate test databases for each process. On a project with 450 feature tests, this reduced execution from seven minutes to under three. The trade-off is slightly higher memory usage on the runner, but GitHub's standard Ubuntu runners handle this comfortably for typical Laravel applications.

Disable Xdebug and code coverage collection unless you are specifically generating coverage reports. The coverage: none option in setup-php skips installing coverage extensions entirely, saving 30–60 seconds per run. If you need coverage, run it in a separate matrix job that executes nightly rather than on every pull request. Most teams I work with care about test pass/fail status during development, not coverage percentages, and reserving expensive analysis for off-hours keeps feedback loops tight.

How Do You Debug Failed Laravel Deployments in GitHub Actions?

Deployment failures in CI are uniquely frustrating because they often involve infrastructure state that differs from your local machine. When a deploy fails, resist the urge to immediately re-run the workflow; instead, gather diagnostic information first. Add verbose flags to your deployment commands and capture output as artifacts for post-mortem analysis.

A pattern I have seen repeatedly on production Laravel applications involves OPcache holding stale bytecode after deployment. Even though Deployer resets OPcache as part of its default recipe, custom PHP-FPM configurations sometimes disable the reset endpoint or restrict access to localhost only. Verify your server's OPcache configuration allows programmatic invalidation, and add an explicit health check step that hits a dedicated /up endpoint returning the application version from git. If the health check fails within 30 seconds of symlink swap, automatically trigger rollback rather than leaving the site in a degraded state.

For teams managing multiple sister sites on shared infrastructure—a common pattern for Nepal-focused businesses operating regional variants of legal or translation services—document which sites share deployment pipelines and which have independent workflows. I maintain several sites using the same Deployer 7 and GitLab CI configuration on shared EC2 instances, and clear documentation prevents accidental cross-site contamination when updating one application's dependencies or environment variables. When onboarding new developers or handing off projects, this documentation proves more valuable than the workflow files themselves.

When troubleshooting permission errors during deployment, remember that GitHub Actions runs as a non-root user via SSH. Ensure the deploy user owns all target directories and has write access to shared storage. A common gotcha occurs when previous manual deployments created files owned by root; subsequent automated deploys then fail trying to overwrite those files. Run chown -R deploy:deploy /var/www/site once manually after initial server setup, and include ownership verification as the first step in your deploy workflow to catch permission drift early.

Implementing Reliable GitHub Actions for Laravel Testing and Deploy

Reliable GitHub Actions for Laravel testing and deploy comes from treating your CI/CD configuration as production code subject to the same standards as your application. Pin dependency versions explicitly, cache aggressively, separate testing from deployment, and protect production environments with approval gates. The workflows provided here have been refined across dozens of production Laravel deployments serving real business operations, from legal-tech portals processing sensitive documents to eCommerce platforms handling international transactions.

Start with the testing workflow, get it green and fast, then layer on deployment automation. Resist the temptation to optimize prematurely; a working pipeline that takes ten minutes beats a broken one that promises three. As your team grows and release frequency increases, invest in parallelization and caching incrementally based on actual bottleneck measurements rather than assumptions.

If you need help setting up GitHub Actions for Laravel testing and deploy for your specific infrastructure, or want to audit an existing pipeline that has become unreliable, reach out to discuss your project requirements. Whether you are modernizing a legacy PHP application or building a new Laravel system from scratch, getting CI/CD right early prevents costly firefighting later.

Frequently Asked Questions

Create a .github/workflows/laravel.yml file in your repository root. Define jobs for testing (PHPUnit, Pest) and deployment using SSH or Deployer. Specify PHP 8.2+ and Node 22 LTS in the setup steps, cache Composer and npm dependencies to reduce build times, and use environment secrets for server credentials.

Yes, up to 2,000 minutes monthly on free tier.

GitHub Actions offers tighter integration with GitHub repositories and a larger marketplace of pre-built actions, while GitLab CI provides more mature self-hosted runner support. In my experience deploying Laravel apps via both, GitLab CI handles complex multi-stage pipelines slightly better, but GitHub Actions wins for simpler test-and-deploy workflows due to superior documentation and community-maintained Laravel-specific actions.

Store sensitive values like APP_KEY, database credentials, and SSH keys as GitHub repository secrets under Settings > Secrets and variables > Actions. Reference them in workflows using ${{ secrets.SECRET_NAME }} syntax. Never commit .env files or hardcode credentials. For non-sensitive configuration, use GitHub environment variables or pass them directly in the workflow YAML. This approach keeps production credentials secure while allowing different configurations per deployment target.

Add a job step that runs composer install --no-interaction --prefer-dist followed by php artisan test or vendor/bin/phpunit. Ensure your workflow installs PHP 8.2 or higher using shivammathur/setup-php action, configures a test database service container (MySQL 8.0 or SQLite), and sets TESTING=true in environment variables. Cache the vendor directory between runs to speed up execution. Tests should complete within the free tier limits for most Laravel applications.

Technically yes via FTP/SFTP actions, but not recommended.

Use the deployerorg/deployer-action or configure manual SSH commands that invoke Deployer 7. Your workflow should build frontend assets locally, transfer only production-ready artifacts to the server, and execute dep deploy which handles atomic symlink swaps. Configure shared directories for storage and .env persistence across releases. After deployment, reload PHP-FPM to clear opcache. This pattern mirrors what I use on client projects where downtime during business hours is unacceptable.

Permission errors on storage and bootstrap/cache directories after deployment are frequent. Fix by adding post-deploy chmod commands. Another issue is mismatched PHP versions between local development and CI runners causing test failures. Always pin exact PHP versions in workflows. Missing environment variables cause cryptic application errors during testing; validate required vars exist before running tests. Finally, uncached dependency installations make workflows painfully slow; always enable Composer and npm caching from the start.

Approximately Rs 500-1500 monthly (~USD 4-11) for typical usage.

Build assets in GitHub Actions and upload compiled artifacts to the server. Servers running Laravel should focus on serving requests, not compiling Vite or webpack bundles. Building in CI ensures consistent output regardless of server state and enables parallel asset compilation alongside testing. Upload the public/build directory as an artifact, then extract it during deployment. This approach eliminates Node.js requirements on production servers and reduces deployment time significantly, especially for resource-constrained VPS instances commonly used in Nepal.

Generate a dedicated ED25519 SSH key pair without passphrase specifically for CI deployment. Add the private key as a repository secret named DEPLOY_SSH_KEY and the public key to the server's authorized_keys file. Use webfactory/ssh-agent action in your workflow to load the key temporarily during deployment. Restrict this key to only the deployment user with limited shell access. Rotate keys quarterly and immediately revoke if compromised. Never reuse personal SSH keys or keys with sudo privileges for automated deployments.

Yes, but requires careful Chrome driver configuration. Use the laravel-dusk-action or manually install Chromium and chromedriver matching your Laravel Dusk version. Run tests headless with --disable-gpu and --no-sandbox flags since GitHub runners lack display servers. Allocate sufficient timeout (10-15 minutes) as browser tests are slower than unit tests. Consider running Dusk only on main branch pushes rather than every pull request to conserve minutes. In practice, I reserve full browser testing for staging deployments and rely on faster unit and feature tests for PR validation.

Enable debug logging by re-running the workflow with "Enable debug logging" checked. Check the specific failing step's output for error messages. Common issues include missing SSH host key verification (add ssh-keyscan step), incorrect file permissions after deployment, or stale opcache serving old code. Verify secrets are correctly named and populated. Test deployment commands locally first using the same Deployer recipe or SSH commands. Review recent commits that might have broken the build. If using custom actions, check their issue trackers for known compatibility problems with current Laravel versions.

Define a strategy matrix testing PHP 8.2, 8.3, and 8.4 against Laravel 12. Use shivammathur/setup-php with matrix.php-version to dynamically configure each job. Include MySQL 8.0 and PostgreSQL 16 service containers to validate database compatibility. Mark one combination as primary (PHP 8.4 + MySQL) and others as optional to avoid blocking merges on secondary version failures. This catches version-specific regressions early while keeping CI feedback fast. Most production Laravel systems I maintain target PHP 8.3 or 8.4, so those receive priority in the matrix.

Use sandbox API endpoints and test credentials stored as GitHub secrets. Mock external HTTP calls in unit tests using Laravel's Http::fake() to avoid hitting live payment gateways during CI. Reserve actual sandbox API integration tests for a separate job that runs less frequently. Configure webhook URLs pointing to temporary services like ngrok or dedicated test endpoints for callback verification. Document test card numbers and OTP codes in repository wiki. Payment gateway testing in CI requires balancing coverage with reliability since third-party sandboxes occasionally experience downtime unrelated to your code changes.

Share this article

Quick Contact Options
Choose how you want to connect me: