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."
| Path | Track in Git? | Why |
|---|---|---|
wp-content/themes/your-theme/ | Yes | Code you write. The whole point. |
wp-content/plugins/your-plugin/ | Yes | Custom plugins you author. |
wp-content/mu-plugins/ | Usually | Small must-use snippets you maintain. |
WordPress core (wp-admin/, wp-includes/, wp-*.php) | No | Downloaded code. Reinstallable. Huge churn on every update. |
| Third-party plugins and themes | No | Not yours. Manage via the dashboard or Composer. |
wp-content/uploads/ | No | User content, often gigabytes. Belongs in a backup, not a repo. |
wp-config.php | No | Contains DB credentials and salts. A secret. |
wp-content/cache/, transients, logs | No | Generated. Different on every machine. |
vendor/, node_modules/ | No | Installed 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:
# 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
*.tmpThe 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.
| Approach | Repo root | Best when |
|---|---|---|
| Whole project | WordPress install root | You own the server and deploy the full site, including which plugins are active. |
| Theme/plugin only | Your theme or plugin folder | You 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:
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:
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:
| Method | How it works | Good for |
|---|---|---|
git pull on the server | SSH in, git pull in the web root | A single VPS you control. Simple, manual. |
post-receive hook | Push to a bare repo on the server; a hook checks out the files | Self-hosted, push-to-deploy without CI. |
| GitHub Actions | A workflow builds assets and rsyncs/deploys on push | Build 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.
- gitignore: official Git documentationgit-scm.com
- Editing wp-config.php: WordPress Developer documentationdeveloper.wordpress.org
- Recording Changes to the Repository: Pro Git bookgit-scm.com





