TechEarl

How to Use Git with WordPress

How to put a WordPress project under Git: what to track vs ignore, a ready .gitignore, version-controlling just your theme or plugin, and deploy options.

Ishan Karunaratne⏱️ 9 min readUpdated
Share thisCopied
Using Git with a WordPress project: what to track, what to ignore, and how to deploy.

The short answer: do not put your whole WordPress install under Git. Track only the code you actually write, your theme and your custom plugins, and ignore WordPress core, the uploads folder, third-party plugins, and anything with secrets in it like wp-config.php. That keeps your repository small, your deploys clean, and your database password out of GitHub.

WordPress mixes three very different kinds of files in one folder: code you author, code you downloaded, and content your users uploaded. Git is for the first kind. The rest is noise (or worse, a secret leak) if you commit it. This guide shows what to track, gives you a ready-to-use .gitignore, and walks through the two common scoping choices and how to deploy.

If you are brand new to Git, start with Git for Beginners and come back. The rest of this page assumes you can run git init, git add, and git commit.

What goes in Git and what does not

Here is the mental model. Walk through every part of a WordPress install and sort it into "I wrote this" or "I did not."

PathTrack in Git?Why
wp-content/themes/your-theme/YesCode you write. The whole point.
wp-content/plugins/your-plugin/YesCustom plugins you author.
wp-content/mu-plugins/UsuallySmall must-use snippets you maintain.
WordPress core (wp-admin/, wp-includes/, wp-*.php)NoDownloaded code. Reinstallable. Huge churn on every update.
Third-party plugins and themesNoNot yours. Manage via the dashboard or Composer.
wp-content/uploads/NoUser content, often gigabytes. Belongs in a backup, not a repo.
wp-config.phpNoContains DB credentials and salts. A secret.
wp-content/cache/, transients, logsNoGenerated. Different on every machine.
vendor/, node_modules/NoInstalled dependencies. Rebuilt from a lockfile.

Two rules cover almost every decision. First, if a tool can regenerate it, do not commit it (core, vendor, node_modules, cache). Second, if it contains a credential, never commit it (wp-config.php, .env, any API key file). The deeper reasoning behind ignore patterns is in my .gitignore walkthrough, and the staging step that decides what actually enters a commit is covered in the staging area explained.

A ready WordPress .gitignore

Create a file named .gitignore at the root of whatever you put under Git (the WordPress root if you track the whole site, or your theme folder if you track just the theme). Here is a sensible default for tracking a full project minus core:

text
# WordPress core (reinstallable, never edited)
/wp-admin/
/wp-includes/
/wp-*.php
!/wp-content/

# Secrets and config
wp-config.php
.env
.env.*
*.pem
*.key

# User uploads and generated content
wp-content/uploads/
wp-content/cache/
wp-content/upgrade/
wp-content/backup*/
wp-content/blogs.dir/

# Third-party plugins and themes (manage outside Git)
wp-content/plugins/*
wp-content/themes/*

# ...but DO track the ones I actually wrote
!wp-content/plugins/my-plugin/
!wp-content/themes/my-theme/

# Dependencies (rebuilt from a lockfile)
/vendor/
node_modules/

# Build output
wp-content/themes/my-theme/dist/

# OS and editor cruft
.DS_Store
Thumbs.db
*.log
*.sql
*.tmp

The pattern worth understanding is the negation. wp-content/plugins/* ignores everything in the plugins folder, then !wp-content/plugins/my-plugin/ un-ignores the one you wrote. The order matters: the un-ignore line has to come after the broad ignore, or Git never reconsiders the path. Rename my-plugin and my-theme to your real folder names.

One gotcha that trips people up: .gitignore only affects files Git is not already tracking. If you committed wp-config.php before adding it to the ignore list, it stays tracked. To fix that without deleting your local copy, see removing a file from Git but keeping it locally. And if a secret already landed in a past commit, ignoring it now does nothing for the history; you have to scrub the secret out of the history properly.

Two ways to scope the repository

You do not have to track the entire site. The two common choices are tracking the whole WordPress root (minus core) or tracking only your theme or plugin. Pick based on who else touches the server. Most WordPress sites exist long before anyone introduces Git, so if you are retrofitting version control onto a running install rather than starting fresh, see add Git to a site that is already live for the safe way to do that first commit.

ApproachRepo rootBest when
Whole projectWordPress install rootYou own the server and deploy the full site, including which plugins are active.
Theme/plugin onlyYour theme or plugin folderYou ship a product to clients, or core and plugins are managed separately (managed host, Composer).

Tracking just your theme or plugin

This is the cleaner default for most people. If you are building a theme, your Git root is the theme folder itself:

bash
cd wp-content/themes/my-theme
git init
git add .
git commit -m "Initial theme commit"

If this is your first repository for the theme, set up Git for a new project the right way walks through the full first-repo setup so you start with sane defaults. The .gitignore at the theme root only needs to worry about theme-level cruft (build output, node_modules, editor files), because nothing else lives in this folder. The same approach works for a custom plugin: git init inside wp-content/plugins/my-plugin. This keeps the repository tightly scoped to code you maintain, which makes the history readable and the deploy trivial.

Tracking the whole project

If you want the full site under version control (common when you run your own VPS), put the .gitignore from the previous section at the WordPress root and:

bash
cd /var/www/my-site
git init
git add .
git commit -m "Initial WordPress project"

Run git status before that first commit and actually read it. If you see wp-config.php or a thousand files under wp-admin/, your ignore rules are not matching yet. Fix the .gitignore first; a clean first commit is worth the extra minute.

A few WordPress-specific gotchas

Salts and keys belong in config, not Git. WordPress writes its auth salts into wp-config.php. Since that file is ignored, your salts never reach the repo, which is exactly right. On a new server, regenerate them from the official salt generator rather than copying.

Line endings on Windows. WordPress core and many plugins ship with mixed line endings. If you develop on Windows you will see the LF will be replaced by CRLF warning constantly. It is harmless, but if it bothers you, here is what that warning means and how to quiet it.

The database is not in Git. Posts, pages, options, and users live in MySQL, not in files. Git versions your code; it does not version your content. Back up the database separately (wp db export, a cron mysqldump, or a plugin). Do not try to commit .sql dumps into the repo as a content sync mechanism; that path leads to merge pain.

Branch per feature. Build a new template or plugin feature on its own branch, not on main. If branching is new to you, I explain Git branching for beginners here. It keeps a half-finished redesign from blocking a quick hotfix. While you are at it, keep a clean .git history with conventional commit messages; a theme or plugin repo you hand to a client reads far better when every commit says what changed and why.

Deploy options

Once your theme or site is in Git, deploying is just moving the tracked files to the server. Three common approaches, from simplest to most automated:

MethodHow it worksGood for
git pull on the serverSSH in, git pull in the web rootA single VPS you control. Simple, manual.
post-receive hookPush to a bare repo on the server; a hook checks out the filesSelf-hosted, push-to-deploy without CI.
GitHub ActionsA workflow builds assets and rsyncs/deploys on pushBuild steps (Sass, JS bundling), staging plus production, teams.

The simplest workflow: SSH to the server, cd into your theme, and git pull. To avoid pulling secrets or build artifacts you never committed, this works precisely because the ignore rules kept the repo clean in the first place.

For a hands-off setup where pushing to your remote updates the server automatically, a bare repo with a post-receive deploy hook is the classic WordPress pattern. That a post-receive hook is itself just a Git hook, a script Git runs at a defined point in its workflow, is worth understanding before you wire one up. If your theme has a build step (compiling Sass, bundling JavaScript) you want it to run on deploy, push-to-deploy with GitHub Actions is the cleaner route, and the same runner can install Node and run your build (see setting up Node in GitHub Actions).

Whichever you choose, deploy over SSH and lock down the server. A misconfigured deploy that leaves the .git folder web-accessible is a real, exploited problem; an exposed .git directory hands an attacker your entire source and config. Make sure your web server blocks /.git/.

Connecting to GitHub

If you host the repo on GitHub, you will push over SSH or HTTPS. For a deploy server, SSH keys are the right call; if you are unsure which transport fits your box, choosing between SSH and HTTPS remotes lays out the trade-offs. Add an SSH key to GitHub once, and you push without typing a password every time. GitHub removed password authentication for Git operations in 2021, so if you are still on an HTTPS remote and hitting auth errors, that is usually why.

FAQ

Sources

Authoritative references this article was fact-checked against.

TagsGit for WordPressGitVersion ControlWordPressgitignoreDeployment

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Use .gitignore (with Examples)

A practical guide to .gitignore: pattern syntax, per-repo vs global ignore, ready-made templates, and the gotcha that trips everyone up - already-tracked files keep showing up.

How to Use git stash

How to use git stash to set work aside without committing. Save, list, pop, apply, and drop stashes, stash untracked files, do a partial stash, and switch branches cleanly.

How to Use ElasticPress with WP_Query

Wire ElasticPress to WP_Query so WordPress queries hit Elasticsearch instead of MySQL. Covers installation, indexable post types, ep_integrate, the wp-cli index command, faceted search with aggregations, and when ES actually beats MySQL FULLTEXT.