Using Git with WordPress — Part 2
June 2020 by Kristian Lumme

Using Git with WordPress — Part 2

In the second article in our series on using version control with WordPress, we go beyond the basics, introducing an improved directory structure and looking at how we can use WordPress as a Git submodule. In upcoming articles, we'll discuss dependency management using Composer and consider the role of the database in our workflow.

Be sure to check out article #1, #3 and #4 — and get notified when the next post in this series is available, simply by signing up for our newsletter or by following us on Twitter.

Building High-Quality Web Apps

"The Twelve-Factor App" is a document describing a methodology for building high-quality web applications. The methodology consists of twelve recommendations on how best to handle such concerns as configuration, logging and dependency management. To what degree this methodology can and should apply to a WordPress site is debatable. However, the first factor is probably a good recommendation for just about any long-term project involving code today: in short, it states that the project should consist of a single version-controlled codebase. This is what we'll take a look at next.

In the previous article, we considered a site with a custom theme, with only the theme in version control. Yet there are parts of the project outside the theme that contribute to the final site: the WordPress code itself is an example of this. There are also the files uploaded by the authors of content on the site, as well as any plugins and themes installed. Finally, there is the WordPress configuration to consider.

Git and WordPress Issues

At this point, some of the issues related to using Git with WordPress become apparent. You may be thinking that it makes little sense to put user-uploaded files in version control, as the content using these files is going to be saved in the database, not on the file system. You'd be right, and furthermore, the database is not only used for content in WordPress. Many settings are saved here as well and, if you're using a page builder plugin, much of the layout too! Evidently, putting our files into version control is not going to cover all the parts of the site setup. There is simply no clean division between what may be called the site structure — layout, options, code — and content in WordPress. This fact complicates version control, and requires us to make some trade-offs when putting our site in a Git repository.

Another objection worth considering is this: the WordPress project is already version-controlled. The codebase of WordPress is developed using Subversion, with a mirror using Git on GitHub. As a rule of thumb, if something is already version-controlled elsewhere it should not be put into your repository — sticking the WordPress code into a Git repository essentially means duplicating the effort of the WordPress core developers. We'll look at a workaround in this article, and get back to alternative ways of handling the problem later.

Finally, it may be noted that tracking plugins and themes in our repository is subject to the same objection as tracking WordPress core: these are likely already version-controlled somewhere else. Also, consider your development workflow. Maybe you develop the site locally on your own computer, committing changes to your custom theme, to the configuration, and to installed plugins to your Git repository. At some point, you then deploy the changes to a production site... but if a user installs a new plugin through the production site, should it go into version control? Should user-installed plugins live outside version control, along your version-controlled ones? Or maybe users of the site shouldn't be allowed to install plugins at all? You have to decide on which alternative makes sense for your workflow.

What's the Point?

At some point, then, a thought occurs: if we're not tracking plugins, themes and user uploads in Git, and the WordPress core is version-controlled elsewhere, what do we gain by putting more of our codebase in version control compared to the previous scenario where only our custom theme was version-controlled? Good question.

  • For one, we'll lock down our WordPress version (that is, the code in our repository will be a specific version of WordPress).
  • We'll also put the WordPress configuration (i.e. the wp-config.php file) in version control.
  • Finally, we may choose to track the specific plugins we install as we develop the site without tracking user-installed plugins, possibly disallowing users from installing new plugins altogether.

Again, you're going to have to consider the requirements inherent in your own workflow. In the end, if you're only tracking your custom theme and the WordPress core files, maybe the previous scenario of just keeping your theme in version control is going to work better for you!

Our Scenario

In this article, the approach we've settled on is to disable plugin and theme installations from the WordPress admin, as well as automatic updates. We'll assume that we want to put as much of the site under version control as possible. This would work well with a workflow where the site is developed locally and then deployed to a production server, with only content updates done on the server. For this purpose, you could give other site users limited roles, making them editors rather than administrators for example. However, we'll lock down installations and updates using the WordPress configuration file.

It was mentioned earlier that we'd look at a workaround for duplicating the version control efforts of the WordPress developers. That workaround involves Git submodules. Put simply, Git submodules allow you to keep a Git repository as a subdirectory of another repository. The files of both repositories show up on your file system, but the outer Git repository doesn't track the content of the inner one. By setting up a submodule pointing to a GitHub repository, for example, you can include the project of some other developer inside your own project.

Maybe you see where this is going? Using a Git submodule, we can point to the official WordPress GitHub mirror in our project, including that code without checking it into our own repository. We will be able to choose a specific version of WordPress and even update WordPress by changing which version our submodule points at. There's just one caveat...

Submodule Considerations

Let's take a step back for a moment: we use WordPress through a Git submodule, which puts the WordPress code in a directory in our project. We want to track changes to our custom theme, to the WordPress configuration and possibly to installed plugins and themes... all of which currently happen inside this submodule directory. The issue is that all of those changes will be interpreted as happening to the submodule, not to our main project repository. In other words, committing and pushing these changes would mean pushing them to the main WordPress repository. As you probably understand, this is neither a realistic nor a useful scenario. Getting this setup to function is going to require a little more work.

WordPress has a few options that allows it to better accommodate this kind of workflow. Currently, the directory holding WordPress is the root directory of our site (hereafter referred to as the webroot). We can, however, put WordPress in a subdirectory of its own. Combined with another option that lets us move the wp-content directory outside the directory containing the WordPress core files, this will let us make things a lot cleaner.


So, let's get going! Here, we're going to work with a fresh install of WordPress, creating a repository directly in our webroot, where the WordPress files are initially located. If you're working with an existing site, adapting these instructions should not be too difficult.

This article follows the official WordPress documentation, which at the time of writing only covers putting WordPress in a subdirectory using Apache. The .htaccess files mentioned in the article allow per-directory configuration of Apache, and are often used to set up rewrite rules, for example. While WordPress does support other servers such as nginx, special-case setups for these are not as thoroughly documented — perhaps, in the case of nginx, because nginx has no feature directly corresponding to .htaccess files. As a consequence, this article covers configuration in Apache, but finding resources on how to accomplish similar setups using other web servers should be possible.

I did have some success with nginx combining "method 2" described here with the "location strategies" described here.

To begin with, we create a directory called wordpress inside of our webroot, and move all the WordPress files from the webroot into this directory. In particular, note that if you've gone through the WordPress install, there is probably an .htaccess file here that is hidden, according to the convention of files with names starting with a dot being hidden. We want to make sure this is moved into the wordpress directory as well!

Then we create a new .htaccess file in the webroot, next to the wordpress folder, with these contents (remember to replace the two instances of my-site.test with the URL of your site):

<IfModule mod_rewrite.c>
    RewriteEngine on
    RewriteCond %{HTTP_HOST} ^(www.)?my-site.test$
    RewriteCond %{REQUEST_URI} !^/wordpress/
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ /wordpress/$1
    RewriteCond %{HTTP_HOST} ^(www.)?my-site.test$
    RewriteRule ^(/)?$ wordpress/index.php [L]

This will rewrite requests to our site to hit the WordPress installation in the subfolder correctly.

That's all there is to making the site work with WordPress in a subdirectory!

Next, we'll make it so that plugins, themes and uploads exist outside the WordPress core directory. We copy the wp-content folder out of the wordpress directory to our webroot. Until we configure WordPress to use the new directory, it will still use wp-content in the wordpress directory. To make WordPress use the new directory, we need to add the following lines to the file wordpress/wp-config.php, right before the comment saying "That's all, stop editing!", again replacing the URL with the URL of your site:

define('WP_CONTENT_DIR', dirname(__FILE__) . '/wp-content');
define('WP_CONTENT_URL', 'http://my-site.test/wp-content');

/* That's all, stop editing! Happy publishing. */

Now, our webroot only holds two directories — wp-content, which holds plugins, themes and user uploads, and wordpress, which holds the WordPress core files — and an .htaccess file. But there's another step to take before we're finished. As mentioned earlier, we're currently version-controlling all of the WordPress core files in our main repository. With the way we've set things up now, we're ready to use a Git submodule for WordPress instead.

We'll want to retain our configuration file, so we first move our wp-config.php out of the wordpress directory into our webroot. WordPress will automatically detect a wp-config.php file that is one level above the directory where WordPress is installed. Then we remove or move the wordpress directory — these files will be provided by the Git submodule in the future. Finally, let's initialize our Git repository and add the WordPress submodule in our webroot:

$ git init
$ git submodule add git:// wordpress

When this command finishes, we find that the webroot directory again contains a wordpress subfolder, in which the WordPress core files can be found. In addition, the webroot contains a wp-content directory and the wp-config.php file (and the hidden .htaccess file, along with the Git repository files).

At this point, if we go ahead and refresh our site, things should work just fine! However, we're not quite done yet.

By default, Git will initialize the submodule to point to the latest commit on the master branch of the WordPress project. This is probably not what we want, so let's set it up to point to a specific WordPress version instead. At the time of writing, the latest WordPress version is 5.4, which also exists as a tag in the repository. Specifying this version is as easy as entering the submodule directory and checking out that tag:

$ cd wordpress
$ git checkout 5.4

The same thing can be done in the Tower Git client by double-clicking the submodule, showing the tag list in the sidebar, and then simply double-clicking the relevant tag:

A submodule will generally be in "detached HEAD mode". In a normal repository, this might be an indication that something is not right, but in the submodule, this is the normal order of business: we want the submodule to point to a specific commit, ensuring that its contents don't change unexpectedly.

There are a couple of small things to fix. First, note that if WordPress core was to be updated from the admin, or automatically updated, this would show up as a bunch of modified files in our submodule WordPress repository. We're going to be handling updates using Git in the future, so we'll disable other ways of updating. As mentioned, we're also going to lock down plugin and theme installations from the admin. There's an option in WordPress that will accomplish both of these things: we add the following line towards the end of our wp-config.php, after the lines we added previously:

define( 'DISALLOW_FILE_MODS', true );

This will prohibit any file modifications done by WordPress automatically or through the admin. Not only will plugin and theme installations be disabled, but also any automatic updates, and the ability to edit plugins and themes from the admin. User media uploads will still be possible.

Note that there is a security aspect here: if you turn off any automatic updates, you're going to be the one responsible for making sure the site is updated. As updates often fix potential security holes, keeping up-to-date with new versions is important. If you want a bit more control over what to disable, this article is a great resource.

Finally, we do want to track the plugin and theme directories in wp-content, but we don't want to track user uploads. To that end, we'll create a .gitignore file in our repository root, with the following contents:


Let's commit the project at this point (in the webroot — the directory of the parent repository):

$ git add .
$ git commit -m "Initial commit"

In the future, updating WordPress or installing a specific version is easy: we just enter the submodule directory, run git fetch to get the latest updates from the remote, and check out the tag in question! The parent repository will pick up the changes and we can make a commit there to make it point to the new version.


There are a few things worth mentioning about Git submodules and the workflow we've established here. To begin with, when we add the submodule, Git will check out the relevant commit and place its contents in the submodule directory for us. However, when cloning the project, this doesn't happen. There are a few ways to populate the submodule directory with the correct contents. When cloning the project, we can provide the option --recurse-submodules, and Git will initialize and clone the submodules for us. If we've already cloned the project, we can instead run git submodule update --init --recursive, and Git will do the same for us.

When saving permalinks in WordPress, it may save an .htaccess file in the wordpress directory, resulting in Git showing an untracked file in the submodule repository. We can't ignore this file from the parent repository, and any changes in the .gitignore file in the submodule repository would be overwritten when we check out a new commit. However, we can ignore it locally by editing .git/modules/wordpress/info/exclude (in the parent repository) to include the line /.htaccess. These changes will not be included when pushing to a remote, but will be considered locally. We can do the same thing in the Tower Git client by double-clicking the submodule, right-clicking the .htaccess file, and then selecting "Exclude → Exclude This Item", as seen below.


There we go! We now have most of our site under version control, with WordPress as a Git submodule. As we work on the site, install new plugins or perhaps develop our custom theme, everything will be tracked in version control. Collaboration with other developers will be aided by the ability to synchronize changes and to have different branches, perhaps shared through a Git hosting service. In addition, we may have a deployment workflow which entails pulling down the latest version of our code to the server from such a hosting service.

We did mention the fact that, when it comes to plugins and themes, we're essentially duplicating the version control effort made elsewhere. With WordPress, we used a Git submodule as a solution, but were content at this time to just include the plugins directly in our repository. In the next installment, that won't be the case anymore. We'll take the approach of considering the plugins and themes — indeed, even WordPress itself — as dependencies of our project, and look at how we might go about handling such a scenario.

That concludes the second article in the series. We've updated our folder structure and looked at using WordPress as a Git submodule. In the next article, we'll use the Composer package manager to handle our dependencies — "dependencies" in this case meaning WordPress as well as plugins and possibly themes.

Make sure to get notified when the next article is available — simply by signing up for our newsletter below or by following us on Twitter.

Your Download is in Progress…

Giveaways. Cheat Sheets. eBooks. Discounts. And great content from our blog!