Blog · Guides

A Sane Git Workflow for WordPress Sites

Most WordPress git workflows fail for the same reason: someone commits the entire wp-content folder, including uploads and cache files, and six months later the repo is 4GB and nobody trusts it. A sane workflow draws a clear line between code (which git should track) and state (which it should not). Get that line right and deploys become boring, in the good way.

What Actually Belongs in Version Control

Think of your repo as the blueprint for the site, not a backup of it. That means:

  • Your theme, especially if it’s custom or a child theme you actively edit.
  • Custom plugins you’ve written or are maintaining.
  • Configuration files: .htaccess rules you maintain by hand, build configs, composer.json if you’re managing PHP dependencies that way.
  • A version-locked list of third-party plugins, ideally via composer.json pointing at WPackagist, or a simple text manifest you feed to wp-cli during deploy.

Notice what’s not on that list: WordPress core, third-party plugins, uploaded media, and the database. Those are either reproducible or not code at all, and they belong somewhere else.

What to Keep Out, and Why

Three categories cause almost all the pain in WordPress repos:

WordPress core

Core is a released artifact. You don’t hand-edit it, so there’s no reason to track its history in your repo. Pull it in during deploy with wp core download or install it once on each environment and update via wp-cli or your host’s dashboard.

Uploads and generated files

The wp-content/uploads directory grows without bound and has nothing to do with your codebase. Same goes for cache directories from LiteSpeed Cache, any object cache drop-ins, and generated CSS or JS if you have a build step. These are runtime artifacts, not source.

The database

Git is built for tracking text file changes over time. A WordPress database is a blob of serialized options, post content, and user data that changes constantly and doesn’t diff meaningfully. Trying to version it just creates merge conflicts nobody can resolve. Handle the database with dedicated tools instead (more on that below).

The .gitignore Foundation

A solid starting point for a typical WordPress repo:

/wp-admin/
/wp-includes/
/wp-*.php
!wp-config-sample.php
/wp-content/uploads/
/wp-content/cache/
/wp-content/upgrade/
/wp-content/plugins/*
!/wp-content/plugins/your-custom-plugin/
.htaccess
wp-config.php
*.log

Adjust the plugins section based on whether you’re managing third-party plugins with Composer (in which case ignore the whole vendor-managed plugin directories) or tracking a curated list manually. The key habit is being deliberate: everything ignored by default, then explicitly un-ignored for the handful of custom directories you actually maintain.

Handling wp-config.php Across Environments

This file is the single biggest source of git headaches because it holds environment-specific secrets: database credentials, salts, and debug flags that differ between local, staging, and production. The fix is to never commit the real file at all.

Keep wp-config-sample.php in the repo as a template, ignore the real wp-config.php, and generate it per environment. A clean pattern is to split constants into an environment-detecting include:

<?php
// wp-config.php (not tracked)
require_once __DIR__ . '/wp-config-env.php';
require_once ABSPATH . 'wp-settings.php';

Then keep environment-specific values, database credentials, debug settings, and salts, in a small file that’s generated at deploy time or set via server environment variables and read with getenv(). Some teams prefer a single wp-config.php with a conditional block based on server hostname or an environment variable like WP_ENV:

if ( getenv('WP_ENV') === 'production' ) {
    define( 'WP_DEBUG', false );
} else {
    define( 'WP_DEBUG', true );
    define( 'WP_DEBUG_LOG', true );
    define( 'WP_DEBUG_DISPLAY', false );
}

Either approach works. What matters is that credentials never sit in git history, even in a private repo. Secrets leak through accidental public forks, contractor access, and old clones far more often than through server breaches.

Deploy Strategies That Hold Up

Pick one of these based on your comfort level and team size:

Git pull on the server

Simplest option: the production server has its own clone of the repo, and deploying means SSHing in and running git pull, followed by a wp-cli step to flush caches or run wp cache flush. Fine for solo developers or small teams, but easy to forget a step.

CI/CD push deploys

A pipeline (GitHub Actions or similar) builds and tests on push, then rsyncs the built files to the server or triggers a deploy hook. This adds a layer of safety since nothing ships without passing whatever checks you’ve defined, and it keeps SSH access to production limited.

Deploy hooks with a build step

If your theme uses a build process (compiling Sass, bundling JS), don’t commit the compiled output. Commit the source, run the build in CI, and ship only the built assets. This keeps diffs meaningful and avoids merge conflicts in minified files nobody reads.

Whichever method you choose, always run a post-deploy step that clears both the LiteSpeed page cache and the Redis or Memcached object cache. A code deploy that leaves stale cached HTML or object cache entries in place will make you think the deploy failed when it actually succeeded.

A Practical Branch Model

You don’t need anything elaborate. A main branch that mirrors production, a staging branch that mirrors your staging environment, and short-lived feature branches merged into staging first is enough for the vast majority of WordPress projects. Tag releases if you want an easy rollback point; a bad deploy is much less stressful when reverting is git checkout to a known-good tag rather than reconstructing what changed by memory.

Database: The Part Git Can’t Help With

Since the database isn’t in git, you need a separate discipline for it. wp-cli’s wp db export and wp db import handle the mechanics, and wp search-replace is essential for swapping URLs between environments (local, staging, production often use different domains). A typical staging refresh looks like:

wp db export staging-backup.sql
wp db import production-dump.sql
wp search-replace 'https://production.example.com' 'https://staging.example.com'
wp cache flush

Some teams use a migration plugin to sync specific tables or options between environments, which can be worth it if content editors work directly in a staging environment. Managed hosts that offer one-click staging environments, ServerBorn included, handle a lot of this database and URL-swap complexity for you, which is worth using if it’s available rather than scripting it by hand.

The Takeaway

A sane WordPress git workflow comes down to three habits: track code, not state; keep secrets out of the repo entirely; and treat the database as a separate concern with its own export and import discipline. Get those three right and your deploys stop being an event and start being routine, which is exactly what you want.