CI/CD Pipeline for PHP with GitHub Actions

Khimananda Oli 7 min read Programming and Languages
CI/CD Pipeline for PHP with GitHub Actions

By Khimananda Oli | Last reviewed: August 2026

Shipping PHP applications reliably requires more than just pushing code to a server; it demands a reproducible, automated verification process. A properly configured CI/CD pipeline for PHP with GitHub Actions eliminates manual testing bottlenecks and prevents broken deployments from reaching production. This guide walks you through building a production-grade workflow that handles dependency caching, database migrations, and secure artifact delivery.

How do you structure a CI/CD pipeline for PHP with GitHub Actions?

An effective pipeline mirrors your local development environment while enforcing stricter quality gates. The architecture typically flows through three distinct phases: validation, build, and release. In my experience auditing deployment processes for teams across Nepal and globally, the most common failure point isn't the code itself but the inconsistency between the CI runner and the production server. Your workflow must treat the CI environment as an immutable artifact factory, not a development sandbox.

ValidateLint & Unit TestBuildCompile AssetsPackageCreate ArtifactDeployRelease
High-level architecture of a CI/CD pipeline for PHP with GitHub Actions separating validation from deployment

This separation ensures that a failed lint check never triggers a deployment, and a successful test suite produces a deterministic artifact. For teams managing complex data backends, integrating these checks early prevents schema drift issues similar to those discussed in our MySQL performance tuning guide. Always validate your configuration syntax before optimizing for speed.

How do you configure PHP testing with service containers?

PHP applications rarely exist in isolation. They depend on databases, caches, and sometimes message queues. GitHub Actions provides service containers that spin up Docker images alongside your runner. A frequent mistake I see in audits is configuring the service container but failing to implement health checks, causing tests to run against an unready database.

Defining the Workflow and Services

Create a file at .github/workflows/php-ci.yml. The following configuration sets up PHP 8.3, MySQL 8.0, and Redis, ensuring all services are healthy before executing tests.

name: PHP CI Pipeline

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: testing_db
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3
      
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
        options: >-
          --health-cmd="redis-cli ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3

    steps:
      - uses: actions/checkout@v4
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, redis
          coverage: xdebug
          
      - name: Validate Configuration
        run: php artisan config:cache || true
        
      - name: Run Tests
        env:
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          REDIS_HOST: 127.0.0.1
        run: vendor/bin/phpunit --coverage-clover=coverage.xml

The options field is non-negotiable for production reliability. Without --health-retries, your pipeline becomes flaky because the test runner starts milliseconds before MySQL finishes initialization. If you are comparing database engines for your stack, refer to our analysis on MariaDB vs MySQL to select the right service image.

How do you optimize Composer dependency caching?

Installing dependencies is often the slowest step in a PHP pipeline. Downloading packages from Packagist on every run wastes bandwidth and adds 30–90 seconds to your feedback loop. Proper caching reduces this to near-zero for subsequent runs.

composer.lock HashCache HITCache MISSRestore Vendor DirRun composer install
Dependency caching logic determines whether to restore vendor directory or reinstall packages based on lock file hash

Use the official actions/cache action with a key derived from your composer.lock file. This guarantees that any change to dependencies invalidates the cache automatically.

- name: Get Composer Cache Directory
  id: composer-cache
  run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache Dependencies
  uses: actions/cache@v4
  with:
    path: ${{ steps.composer-cache.outputs.dir }}
    key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
    restore-keys: |
      ${{ runner.os }}-composer-

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

Note the restore-keys fallback. If the exact lock file hash doesn't match, GitHub restores the most recent partial cache. This means composer install only downloads changed packages rather than starting from scratch. For larger monorepos, consider splitting this into matrix builds as covered in our reusable workflows guide.

How do you manage secrets and environment variables securely?

Hardcoding credentials in workflow files is a critical security vulnerability. GitHub Actions provides encrypted secrets at the repository and organization levels. However, managing environment-specific variables (staging vs. production) requires a structured approach.

  • Repository Secrets: Use for sensitive tokens like DEPLOY_SSH_KEY or AWS_ACCESS_KEY_ID. These are masked in logs.
  • Environment Variables: Use for non-sensitive configuration like APP_ENV or DB_HOST. Define these in the workflow or as repository variables.
  • OIDC Authentication: Prefer OpenID Connect over long-lived access keys when deploying to AWS or Azure. This eliminates static credentials entirely.

When deploying to traditional VPS infrastructure common in Nepal's SME sector, SSH key management is paramount. Store the private key as a repository secret and inject it only during the deploy job. Never persist keys on the runner filesystem beyond the current step.

- name: Deploy to Production
  env:
    DEPLOY_KEY: ${{ secrets.PROD_SSH_KEY }}
    SERVER_USER: deploy
    SERVER_HOST: ${{ vars.PROD_HOST }}
  run: |
    mkdir -p ~/.ssh
    echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
    chmod 600 ~/.ssh/deploy_key
    rsync -avz --delete -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \
      ./build/ $SERVER_USER@$SERVER_HOST:/var/www/app/current/
    rm -f ~/.ssh/deploy_key

This pattern ensures the key exists only in memory during execution. For teams requiring SOC 2 compliance, audit logs of secret access are automatically generated by GitHub Enterprise.

How does GitHub Actions compare to other CI tools for PHP?

Choosing the right tool depends on your existing ecosystem, budget, and compliance requirements. While Jenkins dominated the PHP space a decade ago, cloud-native solutions have shifted the landscape significantly by 2026.

FeatureGitHub ActionsGitLab CIJenkins
Setup ComplexityLow (YAML in repo)Medium (.gitlab-ci.yml)High (Server + Plugins)
PHP Ecosystem IntegrationNative Marketplace ActionsBuilt-in Auto DevOpsManual Plugin Config
Self-Hosted RunnersSupported (Free)Supported (Free)Default Architecture
Secret ManagementEncrypted Repo/Org SecretsCI/CD Variables + VaultCredentials Store + Vault
Pricing ModelMinutes-based (Free tier)Minutes-based (Free tier)Infrastructure Cost Only

GitHub Actions wins for most PHP projects due to its tight integration with the source code and the vast marketplace of pre-built actions. GitLab CI remains strong for teams already in the GitLab ecosystem, particularly those using self-managed instances for data residency. Jenkins is now primarily reserved for legacy enterprises with highly customized build requirements that cloud runners cannot satisfy.

Setup Time & Maintenance OverheadGitHub Actions: HoursJenkins: Days to WeeksMaintenance GapGitHub Actions Pros• Zero infra maintenance• Native PHP actions• Integrated PR checks• Free public repo minutesJenkins Considerations• Full control over agents• Complex plugin ecosystem• High maintenance burden• Legacy enterprise use
Visual comparison showing GitHub Actions significantly reduces setup time and ongoing maintenance compared to self-hosted Jenkins

Implementing Your CI/CD Pipeline for PHP with GitHub Actions

Start with the testing workflow provided above, then incrementally add deployment stages as your confidence grows. Monitor your pipeline duration weekly; if tests exceed five minutes, investigate parallelization or database seeding optimization. Remember that automation without observability is just silent failure—integrate notifications and track deployment frequency as a key metric.

If you need help architecting a compliant, high-performance pipeline for your PHP application or migrating from legacy systems, reach out to discuss your infrastructure needs. Secure, automated delivery is the foundation of modern software reliability.

Frequently Asked Questions

Create a .github/workflows/php.yml file defining jobs for checkout, setup-php, composer install, and testing. Use shivammathur/setup-php action to configure the runtime. Trigger on push and pull_request events to validate code changes automatically before merging into main branches.

The shivammathur/setup-php action is the standard choice in 2026. It supports all maintained PHP versions, pre-installs common extensions like intl and bcmath, and integrates directly with tools like Composer and PHPUnit without requiring manual Docker configuration or complex shell scripting in your workflow files.

Private repositories receive 2,000 free minutes monthly on standard plans. Minutes are multiplied by OS; Linux uses 1x multiplier while macOS uses 10x. Exceeding limits incurs per-minute charges. Public repositories remain completely free with unlimited minutes for open-source PHP projects hosted on GitHub.

Use actions/cache with path vendor and key based on hashFiles('composer.lock'). This skips redundant downloads between runs, reducing build times significantly. Restore keys allow partial cache hits when lock files change slightly, ensuring faster installs even after minor dependency updates in your PHP project.

Yes, use shivammathur/setup-php with phpunit extension and add laravel/dusk via Composer. Configure ChromeDriver using browser-actions/setup-chromedriver. Start the app server in background before running php artisan dusk. Store screenshots as artifacts on failure for debugging headless browser test issues effectively.

Never hardcode secrets. Add SSH keys, API tokens, or database passwords as encrypted repository secrets under Settings > Secrets. Reference them as ${{ secrets.NAME }} in workflows. Rotate credentials regularly and restrict access using environment protection rules to prevent unauthorized production deployments from feature branches.

GitHub Actions offers superior marketplace integration and native PHP tooling via community actions. GitLab CI provides built-in container registry and tighter DevOps lifecycle features. For pure PHP CI/CD pipelines in 2026, GitHub Actions typically requires less boilerplate configuration due to specialized setup-php ecosystem maturity and documentation availability.

Ensure the runner user owns the working directory by adding chmod -R 777 . before installation or using sudo chown -R runner:runner . in Linux runners. Alternatively, configure COMPOSER_HOME to a writable path. Permission issues usually stem from cached directories created by previous root-level operations in self-hosted environments.

Absolutely. Define strategy.matrix.php with versions like [8.3, 8.4] to test compatibility simultaneously. Each version runs in parallel, cutting total validation time nearly in half. Combine with fail-fast: false to ensure all versions complete testing even if one fails, providing comprehensive regression coverage for library maintainers.

Add a deploy job needing test job success. Use appleboy/ssh-action to execute git pull and composer install --no-dev on remote host. Restrict deployment to main branch pushes only. Implement rollback scripts and health checks post-deployment to verify application stability before marking the pipeline run as fully successful.

Specify required extensions explicitly in setup-php inputs like extensions: mbstring, xml, curl. Default installations exclude many PECL modules. Check php -m output in debug step to verify loaded extensions match your composer.json requirements. Missing platform requirements often cause silent failures during dependency resolution or runtime execution phases.

Cache aggressively, use conditional steps with if expressions to skip unchanged modules, and prefer Linux runners over macOS. Split workflows into reusable components triggered only when relevant paths change. Self-hosted runners eliminate minute costs entirely for high-volume teams processing hundreds of PHP builds weekly across multiple microservices and shared libraries.

No. Native runners with setup-php handle most scenarios without containers. Docker adds overhead unless you require specific system libraries or reproducible production-matching environments. Reserve containerization for integration tests needing databases or message queues. Direct runner execution is faster and simpler for unit testing and static analysis tasks.

Install via composer require --dev phpstan/phpstan then add a lint job running vendor/bin/phpstan analyse. Fail the workflow on non-zero exit codes. Cache results using phpstan resultCachePath to speed up subsequent runs. Integrate as a required status check to enforce static analysis standards before allowing pull request merges.

External API calls without mocks, unoptimized database queries, or resource-constrained runners cause flaky timeouts. Increase timeout-minutes for long-running jobs, add retry logic with nick-fields/retry action, and profile slow tests locally. Consistent infrastructure and deterministic test data eliminate most intermittent failures in automated PHP continuous integration pipelines by 2026.