Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 059 / 100

Deploying WordPress with Git: what developers should know

Code changes without FTP.

Expona Admin6 min read

Introduction

Most teams still manage WordPress through FTP uploads and manual file edits on the server—a workflow that scales poorly, invites mistakes, and leaves no audit trail. Deploying WordPress with Git: what developers should know means treating your WordPress codebase the same way you treat any modern application: version-controlled, automated, and reproducible across environments.

In this piece, we'll walk through a Git-based deployment strategy that removes FTP entirely, keeps your production code clean, and lets your team deploy with confidence. Whether you're running a small agency or managing WordPress for a SaaS platform, this approach pays dividends fast.

Track code only — Themes, plugins and config in Git

The first principle: your Git repository should contain only code, not content or uploads. This distinction matters because WordPress mixes application code with user-generated content in the same filesystem, and that's where things fall apart.

A well-structured WordPress repository looks like this:

php
wordpress/
├── web/
│   ├── wp-config.php          (generated, not committed)
│   ├── wp-content/
│   │   ├── plugins/           (managed via composer)
│   │   ├── themes/
│   │   │   └── custom-theme/  (your code)
│   │   └── uploads/           (gitignored)
│   └── ...
├── composer.json              (committed)
├── .gitignore                 (critical)
└── README.md

Your .gitignore must exclude:

  • wp-config.php and .env files (database credentials)
  • wp-content/uploads/ (user media)
  • wp-content/plugins/ from third parties (managed by Composer instead)
  • node_modules/ and vendor/ (installed at deployment time)

This means your custom themes and any custom plugins you've written go into Git. A typical custom theme structure:

php
wp-content/themes/custom-theme/
├── functions.php
├── style.css
├── template-parts/
├── assets/
│   ├── css/
│   ├── js/
│   └── images/
├── package.json              (for frontend tooling)
└── README.md

When a developer clones the repository and runs deployment steps (which we'll cover), they get the exact same code as production. No more "it works on my machine but not on staging"—your local environment and production become reproducible.

Composer — Manage plugins with WPackagist

Installing plugins via the WordPress admin UI or by uploading ZIP files is convenient but makes deployments fragile. WPackagist.org mirrors the WordPress.org plugin repository as Composer packages, letting you declare dependencies in composer.json and install them consistently across all environments.

Set up Composer in your WordPress project:

php
{
  "name": "company/wordpress-project",
  "require": {
    "php": ">=8.1",
    "wpackagist-plugin/advanced-custom-fields": "^6.1",
    "wpackagist-plugin/wpml-core": "^4.4",
    "wpackagist-theme/storefront": "^4.0"
  },
  "repositories": [
    {
      "type": "composer",
      "url": "https://wpackagist.org"
    }
  ],
  "extra": {
    "wordpress-install-dir": "web/wordpress",
    "wordpress-content-dir": "web/wp-content"
  }
}

When you run composer install, all plugins and themes listed install into the correct WordPress directories. When you commit composer.lock, your entire team and CI pipeline get identical versions. No surprises in production.

For premium plugins you've purchased (like GeneratePress Pro), you can use private Composer repositories. Satis or a private Packagist mirror lets you host them yourself, keeping authentication out of version control.

This approach also means plugin updates happen in your local environment first, tested thoroughly, then deployed through Git commits. It's a far cry from the one-click "Update" button that can break a live site.

CI deploys — GitHub Actions deploys to the server

Once your code is in Git and dependencies are managed through Composer, automation becomes straightforward. A CI/CD pipeline (like GitHub Actions) watches your repository, runs tests, and deploys on merge to main.

Here's a minimal GitHub Actions workflow that deploys WordPress to a production server:

php
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'

      - name: Install Composer dependencies
        run: composer install --no-dev --optimize-autoloader

      - name: Deploy to server
        uses: deploymentio/actions-sh@main
        with:
          host: ${{ secrets.PRODUCTION_HOST }}
          username: ${{ secrets.PRODUCTION_USER }}
          key: ${{ secrets.PRODUCTION_KEY }}
          script: |
            cd /var/www/wordpress
            git pull origin main
            composer install --no-dev --optimize-autoloader
            wp db migrate
            wp cache flush

The workflow:

  1. Developer pushes a commit to the main branch.
  2. GitHub Actions checks out the code.
  3. Composer installs dependencies (production-only, no dev packages).
  4. SSH into your server and pull the latest code.
  5. Run any database migrations (if you use a migration tool).
  6. Flush caches to serve fresh content.

This removes the manual FTP step entirely. Every deploy is logged, versioned, and reproducible. If something breaks, you roll back by reverting the commit and pushing again.

You can also add testing steps before deploy: PHPUnit tests, PHPCS linting, or theme validation. If tests fail, the deployment stops automatically.

Bedrock — A modern project structure for WordPress

Roots' Bedrock project is a modernized WordPress skeleton that bakes in best practices from day one. Instead of the default WordPress structure, Bedrock organizes files for version control, environment configuration, and deployment automation.

Key features:

  • Environment-based configuration: Use .env files for database credentials, API keys, and environment-specific settings instead of scattered wp-config.php constants.
  • Separated web root: The web/ directory is your WordPress install; your project root stays clean for Git.
  • Composer-first dependency management: WordPress core itself is installed via Composer, not uploaded as a ZIP.
  • WP-CLI integration: Built-in support for WP-CLI commands like database imports and cache clearing.

A Bedrock project structure:

php
project-root/
├── web/                      (web server root)
│   ├── wp-content/
│   │   ├── plugins/
│   │   ├── themes/
│   │   └── uploads/          (gitignored)
│   ├── wp-config.php         (generated from .env)
│   └── index.php
├── .env                       (gitignored, secrets here)
├── .env.example               (committed, shows structure)
├── composer.json
├── composer.lock
└── config/
    ├── environments/
    │   ├── development.php
    │   └── production.php
    └── application.php

Environment-specific settings live in PHP files, not wp-config.php:

php
// config/environments/production.php
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('SCRIPT_DEBUG', false);
define('WP_MEMORY_LIMIT', '256M');
define('WP_POST_REVISIONS', 10);

// config/environments/development.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('SCRIPT_DEBUG', true);

On deployment, your server reads the production .env file, which isn't in Git. Each environment (local, staging, production) has its own .env, keeping secrets out of version control and configuration environment-aware.

Bedrock also includes sensible defaults for security: DISALLOW_FILE_EDIT is true by default (no theme editor in the admin), and database table prefixes are randomized.

To start a Bedrock project, use Composer:

php
composer create-project roots/bedrock my-wordpress-site

Common mistakes

Committing wp-config.php: Database passwords, API keys, and salts should never be in Git. Use .env files or a secrets management tool instead.

Ignoring uploads/ but tracking plugins: Users' media shouldn't be in Git (it's huge and changes constantly), but third-party plugins should be managed via Composer, not committed directly.

Deploying without running composer install: If your server checks out code but skips dependency installation, you'll have missing files and broken sites. Always run Composer as part of your deployment script.

Manual database migrations: Running schema changes by hand on production is error-prone. Use a migration tool (like Phinx or a custom WP-CLI command) to automate them as part of your deploy.

Testing only in production: Use GitHub Actions or similar to catch mistakes before they hit live. At minimum, run PHPUnit tests and code standards checks before merging.

Key takeaways

  • Keep Git repositories clean: commit code and configuration, gitignore secrets, uploads, and dependencies.
  • Use Composer and WPackagist to manage plugins and themes as versioned dependencies, not manual uploads.
  • Automate deployments with CI/CD (GitHub Actions, GitLab CI, etc.) to eliminate manual FTP steps and add safety checks.
  • Bedrock provides a modern WordPress structure out of the box, handling environment config, dependency management, and deployment patterns.
  • A Git-based workflow gives you audit trails, rollback capabilities, and reproducible environments across your entire team.
---
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles