
August 16, 2026
12 min read
Table of Contents
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.
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.
| Strategy | Downtime | Rollback Speed | Complexity | Best For |
|---|---|---|---|---|
| Direct rsync/scp | 30–120 seconds | Manual, slow | Low | Staging, hobby projects |
| Deployer 7 symlinked releases | Zero (atomic swap) | Instant (previous release) | Medium | Production business apps |
| Docker/container deployment | Near-zero | Image rollback | High | Microservices, large teams |
| Laravel Cloud / Envoyer | Zero | One-click | Low | Teams 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.
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.
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.

