Skip to main content
Technical Details

CI/CD for PHP: A Comprehensive Guide

← Technical Details

Written by Evren BalPublished Updated  · 8 min read

A PHP elephant, inspection lens and blue packages sit along an assembly line.
Discuss this article with your AI

PHP CI/CD is not a single YAML file that blindly copies code to production. It is a chain of gates that turns a code change into a verified release, then controls how that release reaches an environment. For a PHP project, that usually means installing the exact Composer dependencies, running static analysis and tests, building a deployable artifact, and allowing deployment only after those checks pass.

💡 Quick Summary (TL;DR):

  • Continuous integration: Verify each change with reproducible dependency installation, static analysis, linting, and automated tests.
  • Continuous delivery or deployment: Keep the verified release deployable, then use an explicit environment boundary to decide whether production deployment requires approval or happens automatically.
  • Deployment safety: Prevent overlapping releases, limit secret access, deploy a known revision or artifact, check application health, and keep a rollback path.

What CI/CD Means for a PHP Project

CI/CD is often used as one abbreviation, but its parts describe different responsibilities:

  • Continuous integration (CI) verifies changes as they enter a shared repository. A CI workflow installs dependencies, runs checks, and reports whether the change is safe to merge.
  • Continuous delivery keeps the main branch in a deployable state. A verified release can reach production through a deliberate approval step.
  • Continuous deployment automatically releases every change that passes the required gates.

The second meaning of CD should be explicit. A team can have strong CI without deploying automatically, and a workflow that only runs tests is not yet a complete deployment pipeline.

A practical PHP pipeline usually follows this path:

  1. A pull request or commit triggers the workflow.
  2. Composer installs the versions recorded in composer.lock.
  3. Linters, static analysis, and automated tests verify the change.
  4. The pipeline identifies or builds the exact revision, package, or container image to release.
  5. A separate deployment job targets staging or production.
  6. Health checks confirm the release, while the deployment mechanism retains a rollback path.

The platform coordinates this sequence. The safety comes from the gates and release design, not from the CI product's name.

Choose Checks Based on the Cost of Failure

A PHP pipeline does not need every possible test on every commit. It needs checks that cover the failures the application can actually create:

  • Unit tests verify functions and classes in isolation, commonly with PHPUnit.
  • Integration tests verify boundaries such as databases, queues, caches, and external service adapters.
  • Functional or end-to-end tests exercise important user flows with tools such as Symfony Panther, Playwright, or Cypress.
  • Static analysis with PHPStan or Psalm catches type mismatches, impossible calls, and other defects without executing the application.
  • Performance tests belong in the pipeline when latency, throughput, or resource use can block a release, but they do not need to run on every small pull request.

The goal is not the largest test suite. The goal is enough evidence to stop an unsafe change before it reaches the next environment.

Build Once and Identify What You Deploy

PHP applications do not always have a traditional compile step, but they still have a build boundary. That boundary can include:

  • validating composer.json and installing locked dependencies;
  • compiling and minifying frontend assets;
  • generating optimized autoload files;
  • running static analysis and tests;
  • creating an archive for a release directory;
  • building a container image tagged with the commit SHA.

A deployment should point to a known revision or artifact. Rebuilding with different dependencies during deployment can produce code that is not identical to what the CI job verified. The exact packaging method depends on the application and hosting model, but the identity of the release should remain traceable.

Choosing a CI/CD Platform

GitHub Actions and GitLab CI/CD can both run a PHP delivery pipeline. The more useful choice depends on where the repository lives and how the application must be deployed.

Before choosing a platform, check:

  • whether hosted or self-hosted runners can reach the required services;
  • how environments, approvals, concurrency, and secrets are controlled;
  • whether the deployment target expects an archive, container image, serverless package, or remote command;
  • how failed releases are detected and rolled back;
  • which logs and deployment records the team needs to retain.

Once those decisions are clear, the setup is straightforward:

  1. Put the workflow configuration in version control.
  2. Define the PHP version and required extensions explicitly.
  3. Install dependencies from composer.lock.
  4. Add the checks that must pass before merge or release.
  5. Keep deployment in a separate job with its own permissions and environment.
  6. Test the rollback procedure before treating the pipeline as production-ready.

Example: PHP CI with GitHub Actions

The following .github/workflows/ci.yml example implements the verification half of the pipeline. It does not claim to deploy an application because the deployment command depends on the actual infrastructure.

name: Verify PHP

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: verify-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  verify:
    runs-on: ubuntu-latest

    steps:
      - name: Check out the repository
        uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, xml, ctype, iconv, zip
          coverage: none

      - name: Find the Composer cache directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"

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

      - name: Validate Composer metadata
        run: composer validate --strict

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

      - name: Run static analysis
        run: vendor/bin/phpstan analyse

      - name: Run tests
        run: vendor/bin/phpunit

This example keeps the default token at read-only repository access. It also removes the obsolete --no-suggest option from the Composer command. Adjust the PHP version, extensions, and project commands to match the application rather than copying them without review.

Version tags keep examples readable, but a production repository should evaluate its action supply-chain policy. GitHub recommends pinning third-party actions to a reviewed full commit SHA and explicitly granting only the permissions a workflow needs.

Add Deployment as a Separate Controlled Job

A deployment job should depend on verification and run only for the release event you intend. The following job shows the boundary without pretending that one deployment command fits every server or platform:

  deploy:
    needs: verify
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    permissions:
      contents: read

    environment: production

    concurrency:
      group: production
      cancel-in-progress: false

    steps:
      - name: Check out the deployment scripts
        uses: actions/checkout@v4

      - name: Deploy the verified revision
        env:
          RELEASE_SHA: ${{ github.sha }}
        run: ./scripts/deploy-production.sh "$RELEASE_SHA"

The script is an explicit project boundary, not a universal recipe. Depending on the target, it might ask Deployer to activate a release directory, update a container image by digest, or publish a serverless package. Whatever it does, it should:

  • deploy the revision or artifact associated with RELEASE_SHA;
  • prevent two production releases from running at the same time;
  • read deployment credentials from the protected production environment;
  • fail when the health check fails;
  • retain or identify the previous release for rollback.

GitHub environments can restrict deployment branches, delay a job, require approval, and withhold environment secrets until protection rules pass. A protected environment is therefore more than a label in the YAML file.

Modern PHP Deployment Strategies

The deployment strategy should match the hosting model and failure mode:

  • Release directories and symlink switches: Tools such as Deployer prepare a new release directory and switch the active symlink after preparation succeeds. Rollback can point the symlink to a previous release.
  • Container deployments: A pipeline builds an image once, tags or records it immutably, and lets the orchestrator replace instances through rolling or blue-green deployment. Health checks decide when the previous instances can stop serving traffic.
  • Serverless PHP: Tools such as Bref package PHP applications for AWS Lambda. Infrastructure management changes rather than disappearing, and rollback still requires identifiable versions or aliases.

None of these strategies is universally best. The relevant question is which one provides a repeatable release, safe secret handling, observable health, and an acceptable recovery time for the application.

Common PHP Pipeline Failure Modes

  • Stale dependency cache: Key the Composer download cache with composer.lock. Cache downloaded packages, not an unexplained vendor/ directory that can hide installation problems.
  • Missing extensions: Declare required PHP extensions explicitly so the runner resembles the application's supported runtime.
  • Overlapping deployments: Use deployment-level concurrency and do not automatically cancel a production release halfway through.
  • Secrets exposed too early: Keep production credentials out of pull-request jobs and scope them to a protected environment.
  • Tests pass but deployment fails: Verify target connectivity, writable paths, migrations, health checks, and rollback separately from application tests.
  • Different code is deployed: Tie the release to the commit SHA, artifact digest, or package identifier produced by the verified workflow.

Official Documentation and Further Reading

The Practical Decision

A useful PHP CI/CD pipeline makes the path from change to release explicit. It identifies what is being verified, what is being deployed, who or what can approve production, and how the system returns to a known state when a release fails.

Start with the failure and recovery model of the application. Then choose the checks, platform, and deployment mechanism that enforce it. A shorter pipeline with clear gates and a tested rollback path is more credible than a long workflow that calls itself production-ready without controlling the final step.

Changes Made in This Article

  • 27.08.2026: Distinguished continuous integration, continuous delivery, and continuous deployment; rebuilt the GitHub Actions example around a verified CI job and a separate controlled deployment boundary; added environment, concurrency, permissions, secret, release identity, health-check, and rollback guidance; removed obsolete Composer and repetitive FAQ material.
  • 20.06.2026: Added a GitHub Actions workflow, modern testing recommendations, zero-downtime deployment strategies, troubleshooting notes, and official documentation links.
  • 11.05.2022: Updated article summary.

If this article was useful

Linking to it from a relevant page on your website or sharing it on social media genuinely helps it reach more people. Thank you for your support.

Linking and brand guidelines →