WordPress and Git
Git tracks files. WordPress keeps its content in a database and its uploads on disk, so the useful question is not whether to version a WordPress site but which parts of it to version.
Git tracks files; WordPress keeps most of what matters somewhere else. Content is in the database, uploads are on disk, and settings are rows in the options table. So "put WordPress in Git" is the wrong instruction. The right one is: version the code that defines the site, and manage everything else with tools built for it.
Get that division right and version control makes a WordPress project dramatically easier to work on. Get it wrong and you have a repository full of noise that nobody trusts.
What to track, and what not to
| Track | Do not track |
|---|---|
| Your own theme, including a child theme | WordPress core |
| Plugins you wrote | Third-party plugins (manage as dependencies) |
composer.json / lock file |
wp-config.php with real credentials |
| Build config, CI config, deploy scripts | wp-content/uploads/ |
A committed .gitignore |
Cache and log directories |
| Documentation and setup notes | Anything containing a secret |
A workable .gitignore for a project that manages dependencies with Composer:
/wp-admin/
/wp-includes/
/wp-*.php
!/wp-config-sample.php
/index.php
/license.txt
/readme.html
/wp-content/uploads/
/wp-content/upgrade/
/wp-content/cache/
/wp-content/plugins/*
!/wp-content/plugins/my-site-plugin/
/wp-content/themes/*
!/wp-content/themes/my-theme/
wp-config.php
*.log
.DS_Store
The * then ! pattern is the important idiom: ignore everything in a directory, then explicitly re-include the things you wrote.
Core and plugins as dependencies
Committing third-party code means every plugin update is a large, unreviewable diff, and your repository slowly becomes a mirror of someone else's release history.
The alternative is to declare them. Composer can install WordPress core and plugins from the public directory, so composer.json becomes the statement of what the site is made of, and the lock file pins exact versions:
{
"require": {
"johnpbloch/wordpress": "^6.7",
"wpackagist-plugin/wordpress-seo": "^24.0"
},
"repositories": [
{ "type": "composer", "url": "https://wpackagist.org" }
]
}
This gives you reproducible environments and a readable history of dependency changes. It costs you the ability to update plugins from the WordPress admin — updates now happen in the repository and deploy from there. That is a real workflow change, and it is the main reason teams adopt it and solo site owners often should not.
If that is too much, the middle path is legitimate: track only your own theme and site-specific plugin, and keep updating everything else through the admin as usual, following how to update WordPress plugins safely.
The database stays out
There is no useful way to version a WordPress database. It changes on every page view, a dump produces diffs nobody can read, and merging two of them is not a thing.
What to do instead:
- Content moves down, never up. Pull production content to staging and local, not the reverse. Pushing a database up destroys everything written on production since the copy — orders, comments, form entries.
- Rewrite URLs when you move a database between environments, using a serialisation-aware tool:
wp search-replace, from WP-CLI. - Configuration that must be versioned — post types, taxonomies, settings a plugin stores in options — belongs in code where possible. Custom post types registered in a site-specific plugin are in the repository; the same types created through a UI plugin are database rows and are invisible to Git.
- Backups remain a separate obligation — how to back up a WordPress site.
Secrets
wp-config.php holds database credentials and salts. It must not be in the repository.
Commit an example instead — wp-config-sample.php or a .env.example — listing the keys with no values, and load real values from the environment or from an untracked file per environment. If credentials were ever committed, rotate them: removing the file in a later commit does not remove it from history.
What belongs in the config file at all is covered in wp-config.php explained.
Deploying from a repository
Three approaches, in increasing order of discipline:
- Pull on the server. A
git pullover SSH, run by hand or by a small script. Simple; easy to forget a step. - A build and deploy pipeline. CI installs dependencies, builds assets, runs tests, and pushes the result. The repository holds source; the server receives a built artifact.
- A host with built-in Git deployment, which many managed hosts provide — see managed WordPress hosting.
Whichever you use, the deploy should also handle the steps a file copy does not: clearing caches, flushing rewrite rules after registering new post types, and running any migration. Never deploy straight to production without the same change having run on staging first.
Common mistakes
- Committing
wp-content/uploads/, which makes the repository enormous and still is not a media backup. - Committing
wp-config.phpwith live credentials. - Committing core and every plugin, then losing the ability to review any diff.
- Treating Git as a backup. It has no content, no orders and no images.
- Editing files directly on production, so the server and the repository disagree and the next deploy silently reverts a fix.
- Pushing a database upward and destroying live data.
- No
.gitignoreuntil later, leaving secrets in the history.
Verify
Clone the repository into an empty directory, install dependencies, supply a config and a database, and confirm you can bring the site up from nothing but the repository plus a content dump. If you cannot, something the site depends on is not tracked — and finding out now is considerably cheaper than finding out during a recovery.
Frequently asked
- Usually not. Track your own themes and plugins and the configuration that defines the site. Core and third-party plugins are better managed by a dependency tool, and uploads do not belong in a repository at all.
- Not usefully. It changes on every visit and every content edit, and a database dump produces meaningless diffs. Content moves through exports and migration tooling, not through Git.
- Yes, and they are different things. Git protects code. Your content, orders and uploads live in the database and the uploads folder, neither of which is in the repository.