Skip to main content
datronix.
← All articles
SiteOpsNovember 2026Β·11 min read

Managing Multi-Tenant WordPress Configurations with Git Submodules and Composer

When a digital agency manages 5 to 10 WordPress websites, managing custom code is relatively straightforward: developers push commits directly to independent GitHub repositories, and plugins are updated via WP Admin or WP-CLI. When that roster scales to 40, 80, or 150+ client sites, an agency inevitably crashes into the multi-tenant codebase dilemma: The Copy-Paste […]

D
Datronix
Digital Engineering & Security
managing multi tenant wordpress git submodules composer

When a digital agency manages 5 to 10 WordPress websites, managing custom code is relatively straightforward: developers push commits directly to independent GitHub repositories, and plugins are updated via WP Admin or WP-CLI.

When that roster scales to 40, 80, or 150+ client sites, an agency inevitably crashes into the multi-tenant codebase dilemma:

  • The Copy-Paste Nightmare: Your agency builds a proprietary custom block library, an enterprise security mu-plugin, or a custom checkout analytics integration. When you fix a bug or add a feature, an engineer must manually copy-paste the patch across 60 separate repositories, create 60 pull requests, and test 60 deploys.
  • The Monorepo Fragility: Frustrated by fragmentation, some agencies attempt a single monolithic Git repository containing all client sites. Within months, the monorepo swells to gigabytes, client credentials bleed across branches, and deploying a fix for Client A risks taking down Client B.
  • Rampant Configuration Drift: Without deterministic dependency tracking, no two client sites share the same plugin versions, PHP standards, or directory structures. Maintenance becomes an unbillable game of whack-a-mole.

Modern software engineering solved this decades ago through modular dependency management.

In enterprise WordPress engineering, combining Composer (for versioned, reproducible dependency resolution) with Git submodules (for shared internal codebases) provides the ultimate architecture: isolating client configurations while maintaining a single, upstream source of truth.

This technical guide breaks down the multi-tenant GitOps blueprint top-tier agencies use to manage 100+ client sites, details production Composer and submodule configurations, and explains how to integrate automated visual QA and rollback pipelines with SiteOps.

011. Deconstructing the Architecture: Composer vs. Git Submodules

To build a scalable agency infrastructure, engineering leadership must understand the exact operational boundary between Composer and Git submodules:

+-------------------------------------------------------------------------------+
|                       COMPOSER VS. GIT SUBMODULES BOUNDARY                    |
+-------------------------------------------------------------------------------+
|  COMPOSER (DEPENDENCY RESOLUTION) | Semantic versioning (^2.1.0)               |
|                                   | Third-party plugins, WordPress Core, themes|
|                                   | Frozen, reproducible builds via composer.lock|
|                                                                               |
|  GIT SUBMODULES (CODE SHARING)    | Pinned to exact Git commit SHA-1           |
|                                   | Proprietary agency frameworks & mu-plugins |
|                                   | Bi-directional active feature development  |
+-------------------------------------------------------------------------------+

When to Use Composer:

Composer is the definitive tool for third-party and vendor code:

  1. WordPress Core & Public Plugins: Pinned through WordPress Packagist (wpackagist-plugin/*).
  2. Commercial Premium Plugins: Tracked via private Composer repositories (Satis, Private Packagist) for Advanced Custom Fields Pro, Gravity Forms, and WP Migrate.
  3. Deterministic Deployments: Committing composer.lock guarantees that staging and production execute bit-for-bit identical code versions.

When to Use Git Submodules:

Git submodules excel where Composer can feel cumbersome for fast-moving agency development:

  1. Shared Agency Frameworks: A custom parent theme, reusable Gutenberg block suite, or internal WebOps mu-plugin developed actively across multiple client builds.
  2. Bi-Directional Commits: Developers working inside a client project can enhance an agency utility component, commit the change to the submodule’s origin branch, and immediately push it back to the central repository.

Learn how consistent environments prevent release failures in our guide on Eliminating Staging Environment Drift Using Automated CI/CD.

022. Directory Hierarchy Blueprint: The Modern Twelve-Factor WordPress Setup

To prevent WordPress core and third-party vendor code from cluttering your client repositories, modern agencies adopt the Bedrock directory standard (popularized by Roots):

client-acme-store/ β”œβ”€β”€ .env.example (Environment variable template) β”œβ”€β”€ composer.json (Client-specific dependency manifest) β”œβ”€β”€ composer.lock (Locked dependency snapshot) β”œβ”€β”€ config/ (Application configuration) β”‚ β”œβ”€β”€ application.php (Environment-agnostic WP config) β”‚ └── environments/ (Development, staging, production overrides) β”œβ”€β”€ vendor/ (Composer dependencies - Git ignored) └── web/ (Public document web root) β”œβ”€β”€ index.php (WordPress bootstrapper) β”œβ”€β”€ wp/ (WordPress Core - managed by Composer) β”œβ”€β”€ app/ (Custom wp-content replacement) β”‚ β”œβ”€β”€ mu-plugins/ (Must-Use plugins) β”‚ β”‚ β”œβ”€β”€ 00-agency-webops.php (Autoloader bootstrapper) β”‚ β”‚ └── agency-core/ (GIT SUBMODULE: Shared Agency WebOps Suite) β”‚ β”œβ”€β”€ plugins/ (Third-party plugins - Composer managed) β”‚ β”œβ”€β”€ themes/ β”‚ β”‚ β”œβ”€β”€ agency-parent/ (GIT SUBMODULE: Shared Parent Theme) β”‚ β”‚ └── client-child-theme/ (Client-specific custom child theme) β”‚ └── uploads/ (Persistent media - symlinked / ignored)

The Architectural Advantages:

  • WordPress Core is Disposable: WordPress Core lives in /web/wp/ and is never committed to Git. Upgrading WordPress across 50 sites is as simple as updating composer.json and running composer update roots/wordpress.
  • Decoupled Web Root: Non-public configuration files, secrets (.env), and build scripts reside outside the public web/ document root, eliminating direct file access exploits.

033. Production composer.json Manifest for Client Repositories

Below is an enterprise-grade composer.json file configuring WordPress Core, premium plugin repositories, and directory mapping:

{
  "name": "agency/client-acme-store",
  "description": "Production enterprise WordPress architecture for Acme Store",
  "license": "proprietary",
  "type": "project",
  "authors": [
    {
      "name": "Agency Engineering Team",
      "email": "devops@agency.com"
    }
  ],
  "repositories": [
    {
      "type": "composer",
      "url": "https://wpackagist.org"
    },
    {
      "type": "composer",
      "url": "https://composer.deliciousbrains.com"
    },
    {
      "type": "package",
      "package": {
        "name": "advanced-custom-fields/advanced-custom-fields-pro",
        "version": "6.3.8",
        "type": "wordpress-plugin",
        "dist": {
          "type": "zip",
          "url": "https://connect.advancedcustomfields.com/index.php?a=download&p=pro&k={%ACF_PRO_KEY}&t=6.3.8"
        }
      }
    }
  ],
  "require": {
    "php": ">=8.2",
    "roots/wordpress": "6.7.1",
    "roots/wp-config": "1.0.0",
    "roots/wp-password-bcrypt": "1.1.0",
    "vlucas/phpdotenv": "^5.6",
    "wpackagist-plugin/woocommerce": "^9.4",
    "wpackagist-plugin/redis-cache": "^2.5",
    "advanced-custom-fields/advanced-custom-fields-pro": "^6.3"
  },
  "config": {
    "optimize-autoloader": true,
    "preferred-install": "dist",
    "allow-plugins": {
      "composer/installers": true,
      "roots/wordpress-core-installer": true
    }
  },
  "extra": {
    "installer-paths": {
      "web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin"],
      "web/app/plugins/{$name}/": ["type:wordpress-plugin"],
      "web/app/themes/{$name}/": ["type:wordpress-theme"]
    },
    "wordpress-install-dir": "web/wp"
  }
}

044. Taming Git Submodules: Eliminating Common Developer Pitfalls

While Git submodules are extraordinarily powerful for multi-tenant code sharing, inexperienced developers frequently encounter three operational headaches:

+-------------------------------------------------------------------------------+
|                      THE 3 GIT SUBMODULE HAZARDS & FIXES                      |
+-------------------------------------------------------------------------------+
|  1. DETACHED HEAD STATE   | Git checks out commit SHA, not active branch      |
|                           | Fix: Configure submodule.recurse & branch tracking|
|                                                                               |
|  2. BLANK SUBMODULE DIRS  | Junior dev clones repo; submodule folders empty   |
|                           | Fix: git clone --recurse-submodules               |
|                                                                               |
|  3. OUT-OF-SYNC CI BUILDS | CI runner lacks deploy keys to private submodule  |
|                           | Fix: Deploy keys / Personal Access Token in CI/CD |
+-------------------------------------------------------------------------------+

1. Adding a Shared Agency Submodule

To attach your agency’s proprietary shared WebOps suite into a client project:

# Add shared agency core as a submodule inside the mu-plugins directory git submodule add -b main git@github.com:agency/agency-core.git web/app/mu-plugins/agency-core git commit -m "feat(webops): integrate shared agency-core submodule"

2. The Universal Clone & Pull Commands

Ensure your agency engineering runbook mandates these commands:

# Clone a client repository with all shared submodules automatically populated git clone --recurse-submodules git@github.com:agency/client-acme-store.git # Pull upstream client changes and sync all submodule commits in one step git pull --recurse-submodules

3. Pushing Submodule Updates Across the Fleet

When a developer updates agency-core inside Client A and wants to roll the feature out to 50 other client sites, execute an automated batch update via bash:

#!/usr/bin/env bash
# ==============================================================================
# Multi-Tenant Submodule Fleet Synchronization Script
# ==============================================================================
set -euo pipefail

CLIENT_REPOS_DIR="/home/devops/clients"

echo "==> Synchronizing agency-core submodule across all client repositories..."

for REPO in "$CLIENT_REPOS_DIR"/*; do
    if [ -d "$REPO/.git" ]; then
        echo "Updating: $(basename "$REPO")"
        cd "$REPO"
        git checkout main
        git pull origin main
        # Update submodule to latest upstream main commit
        git submodule update --remote --merge web/app/mu-plugins/agency-core
        if ! git diff --quiet; then
            git commit -am "chore(webops): bump agency-core submodule to latest"
            git push origin main
            echo "SUCCESS: $(basename "$REPO") updated and pushed."
        else
            echo "NOTICE: $(basename "$REPO") already up to date."
        fi
    fi
done

055. Enterprise CI/CD Pipeline: Automated Multi-Tenant Deployment

Below is a battle-tested GitHub Actions workflow that clones submodules, caches Composer packages, executes syntax linter tests, and executes an atomic zero-downtime symlink deployment:

name: Enterprise Multi-Tenant CI/CD

on:
  push:
    branches:
      - main

jobs:
  build_and_deploy:
    name: Build & Atomic Deploy
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Codebase with Submodules
        uses: actions/checkout@v4
        with:
          submodules: 'recursive'
          token: ${{ secrets.AGENCY_CI_GITHUB_TOKEN }} # Allows reading private submodules

      - name: Setup PHP Environment
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: mbstring, pdo_mysql, zip, opcache
          coverage: none

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

      - name: Install Locked Composer Dependencies
        run: composer install --no-dev --no-progress --optimize-autoloader --prefer-dist

      - name: Compile Front-End Production Assets
        run: |
          cd web/app/themes/client-child-theme
          npm ci
          npm run build

      - name: Atomic Deployment via SSH
        uses: easingthemes/ssh-deploy@v5.0.3
        env:
          SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_KEY }}
          ARGS: "-rltgoDzvO --delete --exclude='web/app/uploads'"
          SOURCE: "/"
          REMOTE_HOST: ${{ secrets.PROD_SERVER_IP }}
          REMOTE_USER: ${{ secrets.DEPLOY_USER }}
          TARGET: "/var/www/app/releases/${{ github.run_id }}"

Explore our zero-downtime symlink deployment architecture in Why Your Agency Needs Zero-Downtime Deployment Pipelines.

066. Integrating Multi-Tenant GitOps with Automated Rollbacks via SiteOps

Automating dependencies across 100 repositories introduces a single crucial requirement: automated verification.

What happens if an upstream Composer update in agency-core introduces a breaking change that clashes with Client C’s specific payment gateway?

This is where SiteOps provides continuous operational protection:

[GitHub Actions Deploys Submodule / Composer Update]
                         |
                         v
       [SiteOps Executes Automated Canary Checks]
       - Multi-Viewport Visual Regression Testing
       - Synthetic Checkout User Journey
       - PHP Error Log & Telemetry Scan
                         |
             +-----------+-----------+
             |                       |
        [Pass: <0.05% Diff]     [Fail: PHP TypeError or Visual Break]
             |                       |
        [Promote Build]         [SiteOps Triggers 30-Second Rollback]
                                [Isolates Breaking Submodule Commit]

The WebOps Safety Net:

  1. When a new submodule commit deploys, SiteOps executes automated multi-viewport visual diffing and synthetic checkout canaries.
  2. If Client C’s checkout button breaks or throws an error, SiteOps halts the release and restores the previous pre-update snapshot in under 30 seconds.
  3. The agency engineering team receives an instant alert showing the exact breaking commit, protecting client revenue with zero downtime.

Read how automated visual regression eliminates deployment anxiety in Automating Visual Regression Testing Across 100+ Sites.

077. Actionable GitOps Migration Checklist for Agency Leadership

Follow this 7-step checklist to transition your agency’s fleet to modular Composer and Git submodule management:

  • [ ] Step 1: Standardize Directory Hierarchy: Adopt Roots Bedrock or an equivalent structure that isolates public web files from private configuration.
  • [ ] Step 2: Externalize Secrets via .env: Remove all hardcoded database credentials and API keys from wp-config.php.
  • [ ] Step 3: Migrate Third-Party Plugins to Composer: Transition core and public plugins to wpackagist-plugin/*.
  • [ ] Step 4: Centralize Proprietary Utilities: Move shared custom blocks and mu-plugins into a dedicated agency-core repository.
  • [ ] Step 5: Attach Shared Code via Git Submodules: Link agency-core into client repositories with tracking branches enabled.
  • [ ] Step 6: Automate Fleet Synchronization Scripts: Deploy automated bash scripts to bump and commit submodule updates across all client repos.
  • [ ] Step 7: Enforce Automated Rollbacks with SiteOps: Integrate SiteOps to automatically verify visual layouts and roll back regressions in under 30 seconds.

08Summary & Next Steps: Achieve True Agency Scale with SiteOps

Managing 50+ client websites by manually copy-pasting code and clicking update buttons in WP Admin is an unscalable operational model. By adopting modern GitOps with Composer and Git submodules, forward-thinking agencies create modular, reproducible, and easily maintainable client codebases that scale effortlessly.

Related Resources for Technical Agency Leadership:

Streamline Your Multi-Tenant Operations with SiteOps

Stop wasting hundreds of unbillable developer hours on manual client updates. Automate multi-site deployments, visual regression testing, and 30-second rollbacks across your agency fleet.

09πŸ‘‰ Start your free trial of SiteOps today and modernize your first three client repositories in under five minutes.

10Frequently Asked Questions (FAQ)

What is the difference between using Composer and Git submodules for WordPress?

Composer manages versioned external dependencies using semantic versioning (^6.7) and locked snapshots (composer.lock), making it ideal for WordPress Core and third-party plugins. Git submodules embed an exact Git commit of another repository directly into your project, making them perfect for proprietary agency mu-plugins and shared parent themes that require bi-directional active development.

Why shouldn’t agencies commit WordPress Core directly to Git?

Committing WordPress Core bloats repositories with thousands of third-party vendor files, creates noisy commit histories, and increases the temptation to make fragile edits directly to core files. Managing WordPress Core via Composer (e.g., roots/wordpress) keeps core files disposable and enables 1-command fleet-wide updates.

How do you avoid blank submodule directories when developers clone a repository?

By default, standard git clone creates empty placeholder directories for submodules. Developers must run git clone --recurse-submodules, or run git submodule update --init --recursive inside an existing working copy to populate all shared submodule codebases automatically.

Can private premium plugins (like ACF Pro) be managed through Composer?

Yes. Agencies can define private package endpoints inside composer.json using licensed download endpoints, or maintain an internal private Composer repository (using open-source Satis or Private Packagist) to distribute premium commercial plugins deterministically across client sites.

How does SiteOps protect multi-tenant agencies when updating shared submodules?

When an updated submodule is deployed across a fleet of client repositories, SiteOps runs automated visual regression testing and synthetic checkout tests across every site. If a shared component breaks a specific client’s checkout layout, SiteOps automatically rolls back that site in under 30 seconds without impacting other clients.

Keep reading.
All articles β†’
pci dss 4.0 compliance wordpress agencyAgency Growth→
November 2026Β·10 min read

The Agency Guide to PCI-DSS 4.0 Compliance for Custom WordPress E-Commerce Builds

For over a decade, digital agencies building custom WooCommerce stores relied on a single comforting defense when clients asked about payment card security: &#8220;We don&#8217;t touch credit card data. We use Stripe Elements (or PayPal, Square, Authorize.net) hosted iframes. Card numbers are sent directly to their secure vault, so our site is completely exempt under [&hellip;]

redis object caching woocommerce high concurrencyAgency Growth→
November 2026Β·12 min read

How to Configure Redis Object Caching for High-Concurrency WooCommerce Stores Without Cache Pollution

When a WooCommerce store experiences traffic spikes such as Black Friday promotions, influencer product drops, or major email newsletter blasts page caching alone is insufficient. Because checkout steps, shopping carts, and dynamic account pages cannot be statically cached, the database server bears the full brunt of incoming traffic. Without an in-memory object cache, hundreds of [&hellip;]

synthetic user monitoring woocommerce checkoutAgency Growth→
November 2026Β·10 min read

How to Implement Synthetic User Monitoring to Guarantee Checkout Flow Integrity

In high-volume e-commerce, the most dangerous outages do not display an HTTP 500 error page or trigger an uptime alert. They are silent checkout breakages: A minor plugin update introduces a JavaScript syntax error that prevents the Stripe credit card iframe from mounting. A third-party tax calculation API changes its JSON schema, causing the &#8220;Place [&hellip;]