CI CD for Monorepo with Multiple PHP Apps

Khimananda Oli 9 min read CI/CD and Automation
CI CD for Monorepo with Multiple PHP Apps

By Khimananda Oli | Last reviewed: August 2026

Managing CI CD for monorepo with multiple PHP apps introduces complexity that standard single-repo pipelines cannot handle efficiently. When your repository contains a Laravel API, a Symfony worker, and shared packages, rebuilding everything on every commit wastes resources and slows feedback loops. You need a pipeline that understands dependency graphs, detects actual changes, and deploys only what is necessary while maintaining atomic versioning.

Monorepo Change Detection ArchitectureGit Pushmain / feature/*Change Detectorgit diff --name-onlyMatrix Generatorapps/api, apps/workerBuild: APILaravel + TestsBuild: WorkerSymfony + QueueSkippedNo ChangesDeploy: StagingDocker PushDeploy: StagingHelm UpgradeOnly modified apps enter the pipeline; unchanged services are skipped entirely
Figure 1: Change detection architecture for CI CD for monorepo with multiple PHP apps ensures only modified services trigger builds and deployments.

How do you detect changes selectively in CI CD for monorepo with multiple PHP apps?

The foundation of any efficient monorepo pipeline is accurate change detection. Without it, you rebuild every PHP application on every commit, which becomes untenable as your repository grows. In practice, I have seen teams waste 45+ minutes per push building six Laravel apps when only one had changes. The solution is a dedicated detection step that outputs a structured list of affected projects before any build job starts.

Implementing path filters with Git diff

Most CI systems provide native path filtering, but these are often too coarse for PHP monorepos where shared packages create implicit dependencies. A more reliable approach uses git diff against the merge base to identify changed directories, then maps those paths to your application structure. For GitHub Actions, this means a preliminary job that sets output variables consumed by downstream matrix jobs.

# .github/workflows/detect-changes.yml
name: Detect Affected Apps
on: [push, pull_request]

jobs:
  detect:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - name: Get changed files
        id: changed
        run: |
          BASE=${{ github.event.pull_request.base.sha || github.event.before }}
          CHANGED=$(git diff --name-only $BASE HEAD | \
            grep -E '^apps/' | \
            cut -d'/' -f2 | \
            sort -u | \
            jq -R -s -c 'split("\n")[:-1]')
          echo "apps=$CHANGED" >> $GITHUB_OUTPUT
      
      - name: Set matrix
        id: set-matrix
        run: echo "matrix={\"app\":${{ steps.changed.outputs.apps }}}" >> $GITHUB_OUTPUT

This script compares against the PR base or previous commit, extracts unique app directories from changed files, and formats them as JSON for matrix consumption. If you use GitLab CI, the equivalent uses rules:changes combined with a custom script for dependency-aware detection. Understanding these Git branching strategies helps determine the correct comparison base for accurate detection.

Handling shared package dependencies

PHP monorepos frequently contain shared libraries under packages/ that multiple apps depend on via Composer path repositories. A change to packages/auth should trigger builds for every app requiring it. Maintain a dependency map in a configuration file or parse composer.json files dynamically during detection. Tools like turborepo or nx automate this graph analysis, but for pure PHP setups without Node tooling, a simple shell script parsing composer requirements works reliably.

How do you structure matrix builds for multiple PHP applications?

Once you have identified affected applications, matrix builds allow parallel execution across all changed services. This is where CI CD for monorepo with multiple PHP apps diverges significantly from single-app pipelines. Each matrix entry must be self-contained yet able to leverage shared caching and common setup steps.

Parallel Matrix Build ExecutionShared Setup LayerApp: customer-apicomposer installphp artisan testdocker build✓ Cache HitApp: billing-workercomposer installphpunit --testsuite=unitdocker build✓ Cache HitApp: admin-panelcomposer installpest --paralleldocker build⚠ Cache MissPush ImagePush ImagePush ImageEach app builds independently with isolated vendor caches keyed by composer.lock hash
Figure 2: Matrix builds execute PHP application tests and container builds in parallel, with each job leveraging hashed dependency caches.

Configuring dynamic matrices in GitHub Actions

Static matrices list every app regardless of changes. Dynamic matrices consume the output from your detection job, spawning jobs only for affected applications. This requires the needs keyword and proper output passing between jobs.

# .github/workflows/build.yml
jobs:
  build:
    needs: detect
    if: ${{ needs.detect.outputs.matrix != '{"app":[]}' }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.detect.outputs.matrix) }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: pdo_mysql, redis, bcmath
          coverage: none
      
      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: apps/${{ matrix.app }}/vendor
          key: php-${{ matrix.app }}-${{ hashFiles(format('apps/{0}/composer.lock', matrix.app)) }}
          restore-keys: php-${{ matrix.app }}-
      
      - name: Install & Test
        working-directory: apps/${{ matrix.app }}
        run: |
          composer install --prefer-dist --no-progress
          php artisan test --parallel

The fail-fast: false setting is critical for monorepos. One failing app should not cancel builds for unrelated services. Each job maintains its own cache scope using the app name in the cache key, preventing cross-contamination between applications with different dependency versions. For teams evaluating their automation platform, comparing GitHub Actions vs GitLab CI reveals meaningful differences in matrix flexibility and cache performance.

Isolating application contexts

A common mistake in PHP monorepo pipelines is running commands from the repository root. Always set working-directory explicitly or use cd before executing Composer and Artisan commands. Environment variables like APP_NAME and database credentials must be scoped per application, not globally. Store these as environment-specific secrets or use a centralized secrets manager referenced during the build.

How do you optimize caching and dependencies across PHP services?

Dependency installation dominates PHP build times. In a monorepo with five Laravel applications, naive composer install calls can consume 3–5 minutes per app even with warm caches. Optimizing this layer yields the largest time savings in CI CD for monorepo with multiple PHP apps.

  • Hash-based cache keys: Key caches on composer.lock hashes, not branch names. Lock files are deterministic; branches are not. A feature branch sharing identical dependencies with main should reuse the same cache entry.
  • Vendor directory caching: Cache the entire vendor/ directory rather than Composer's global cache. Restoring pre-installed dependencies skips extraction and autoloader generation entirely.
  • Platform optimization: Run composer config platform.php 8.4.0 in each app to prevent CI from resolving dependencies against the runner's exact PHP patch version, improving cache hit rates across minor updates.
  • Parallel installation: Use --prefer-dist to download archives instead of cloning repositories, and consider composer install --no-scripts followed by explicit script execution to separate network-bound and CPU-bound phases.

For Docker-based builds, multi-stage images with COPY instructions targeting specific composer.lock files enable layer caching independent of source code changes. This means dependency layers persist across application code commits, reducing image build times to seconds when only PHP source changes. Proper Docker containerization for Laravel establishes patterns that translate directly to monorepo optimization.

What deployment strategy works best for monorepo PHP applications?

Deployment in a monorepo context means selective promotion. You never deploy "the monorepo"; you deploy individual applications that happened to change. This requires artifact tagging that encodes both the application identity and the triggering commit, enabling traceability back to the exact source state.

StrategyBest ForComplexityRollback SpeedCache Efficiency
Per-app Docker tagsKubernetes / ECS deploymentsMediumFast (tag revert)High (layer reuse)
Unified version stampAtomic releases, complianceLowMedium (full rollback)Medium
Git SHA taggingTraceability, debuggingLowFastHigh
Semantic per-appPublic APIs, client libsHighFastVariable

In my experience with Nepal-based fintech teams and global SaaS platforms, per-app Docker tagging with Git SHA suffixes provides the best balance. Images are tagged as customer-api:a1b2c3d rather than monorepo:v2.4.0. This allows independent rollbacks, clear audit trails, and efficient registry storage since unchanged apps retain their existing tags. For teams adopting GitOps, tools like ArgoCD or Flux can watch specific image repositories per application, triggering deployments only when that app's tag changes.

Monorepo vs Polyrepo: CI/CD Trade-offsMonorepo Pipeline✓ Single source of truth, atomic commits✓ Shared packages without publishing✓ Unified CI config, consistent tooling⚠ Complex change detection required⚠ Longer clone times at scale✗ Permission boundaries harder to enforcePolyrepo Pipeline✗ Cross-repo changes need coordination✗ Package publishing overhead⚠ Duplicated CI configs across repos✓ Simple triggers, no path filtering✓ Independent permissions per team✓ Faster shallow clones per repo
Figure 3: Trade-off comparison between monorepo and polyrepo approaches informs architectural decisions for PHP CI CD pipelines.

How do you handle testing isolation and quality gates?

Testing in a PHP monorepo requires balancing thoroughness with speed. Unit tests should always run for changed apps. Integration tests that touch shared databases or message queues need careful orchestration to avoid flaky cross-service interference. I recommend a tiered approach: fast unit tests in the matrix build, integration tests in a dedicated post-build stage that spins up ephemeral infrastructure, and end-to-end tests reserved for pre-production promotion.

Quality gates must be enforced per application, not globally. A failing test suite in the billing worker should block that app's deployment without preventing the customer API from shipping. Configure your CI system to report status checks per matrix entry. In GitHub Actions, this happens automatically with dynamic matrices. For SonarQube or similar tools, pass the app name as the project key to maintain separate quality profiles and historical trends.

Implementing Selective CI CD for Monorepo with Multiple PHP Apps

Building effective CI CD for monorepo with multiple PHP apps demands intentional design around change detection, parallel execution, and selective deployment. Start with accurate path filtering and dependency-aware triggering before optimizing cache layers and matrix configurations. Measure your pipeline's feedback cycle weekly; if developers wait more than ten minutes for PR checks, your change detection or caching needs refinement. The goal is a system where adding a new PHP application to the monorepo increases capability without proportionally increasing build time. If your current pipeline rebuilds everything on every push, implement the detection job described above as your first improvement. For teams ready to modernize their deployment workflow, explore zero-downtime deployment with Deployer to complement your optimized CI pipeline. Need help architecting a monorepo pipeline that fits your specific PHP stack and team size? Get in touch to discuss your infrastructure challenges.

Frequently Asked Questions

Use tools like Turborepo or Nx with affected commands to analyze git diffs. These tools map dependency graphs and only trigger PHPUnit or deployment jobs for modified packages, significantly reducing build times for large PHP monorepos containing multiple Laravel applications.

Yes. Use the paths filter in workflow triggers or dorny/paths-filter action to conditionally run jobs. This ensures specific Laravel apps only build when their directory changes, preventing unnecessary test suite executions across unrelated services within the same repository structure.

Cache the global Composer cache directory and vendor folders per app using hashFiles on composer.lock. In 2026, most CI providers support granular caching keys, ensuring dependency installation remains fast even when individual PHP applications update their lock files independently.

Separate Dockerfiles are preferred. Each Laravel app likely has different extensions and system dependencies. Multi-stage builds with shared base images reduce duplication while maintaining isolation, preventing bloated containers and security risks from bundling unrelated application requirements into one production artifact.

Store secrets centrally using Vault or AWS Secrets Manager, injecting them at runtime based on app context. Avoid committing .env files. Use matrix strategies in CI to map specific variable sets to each PHP application during testing and deployment phases securely.

Yes. Configure turbo.json to define tasks like test, build, and lint for each PHP app. It caches outputs and orchestrates parallel execution based on topological dependencies, making CI CD for monorepo with multiple PHP apps significantly faster than sequential script execution.

Isolate migration jobs per application using matrix builds. Run migrate:fresh --seed only against ephemeral test databases scoped to that specific Laravel app. Never share test databases between apps to prevent schema collisions and ensure accurate integration testing results for each service.

Shared state pollution is critical. Failing to isolate vendor directories or database connections causes flaky tests. Always use fresh containers per job and explicit path filtering to ensure CI CD for monorepo with multiple PHP apps remains deterministic and reliable across concurrent runs.

Adopt semantic versioning per app using tags like app/api/v1.2.0. Tools like Changesets or custom release scripts can automate changelog generation and tagging based on conventional commits, enabling independent release cycles without coupling deployment schedules across distinct Laravel services.

GitHub Actions offers superior YAML ergonomics and native path filtering for PHP monorepos in 2026. Jenkins requires extensive plugin configuration for similar functionality. Choose Actions for tighter Git integration unless you require complex self-hosted runner orchestration or legacy enterprise compliance mandates.

Extract shared logic into internal Composer packages within the monorepo. Reference them via path repositories in composer.json. CI must validate these packages independently before consuming apps build, ensuring interface contracts remain valid across all dependent PHP applications.

Combine unit tests per package with integration tests per app. Use matrix builds to parallelize execution. Reserve end-to-end tests for critical cross-app user flows only, as they are expensive and slow down CI feedback loops significantly in large PHP codebases.

Aggressive change detection reduces compute minutes by skipping unaffected apps. Spot instances and ARM-based runners cut costs further. Caching Composer and Docker layers prevents redundant downloads, making CI CD for monorepo with multiple PHP apps economically sustainable at scale.

Absolutely. Decouple deployment pipelines from the repository structure. Trigger releases based on app-specific tags or directory changes. Independent staging environments allow teams to ship features for one Laravel app without coordinating releases across the entire monorepo ecosystem.

Run PHP-CS-Fixer and PHPStan as early pipeline stages using cached configurations. Apply rulesets consistently via a shared preset package. Fail fast on style violations before running expensive test suites to maintain code quality uniformly across all Laravel applications in the monorepo.