Managing Your WordPress Site with Git and Composer Part 4 – Installing WordPress in a Subdirectory

By Gilbert Pellegrom

In part 1 of this series we looked at how to store and manage your WordPress site in Git. In parts 2 and 3 we looked at using Composer and Git Submodules to manage the themes and plugins in your WordPress site. In this final part of the series we’re going to look at how we can improve what we did in part 1 and store WordPress itself in a subdirectory using Composer or a Git Submodule.

WordPress as a Dependency

What are the advantages of storing WordPress in a subdirectory? Well, by treating WordPress itself as a dependency you can make the structure of your Git repo more modular and clean. By keeping WordPress out of your Git repo, you don’t have to replicate the code and updates become much easier as you don’t have to commit updates to WordPress as part of your workflow.

Iain covered some of the benefits in his post when he detailed how to install WordPress core in a subdirectory using WP-CLI or Composer. In this post we’re going to build on his Composer example and also have a look at how to do it using Git Submodules.

Restructuring WordPress

No matter which direction you choose when installing WordPress in a subdirectory there is a certain amount of restructuring that needs to be done to make it work. Mainly three things need changed:

  1. WordPress needs to be bootstrapped from the new location using an index.php file
  2. The wp-content folder needs to be moved outside of the WordPress install subdirectory
  3. Any environment/config files (e.g. .htaccess/wp-config.php) also need to be moved to the new location

A lot of modern web applications also consider it best practice to move any sensitive config information above the “public” directory so that it can’t be directly accessed via a URL. We’re going to do this here too. So our directory structure should now look something like this:

|-- public/
|   |-- wp-content/
|   |-- wp/    <-- Our subdirectory install location for WordPress (not stored in Git repo)
|   |-- .htaccess
|   |-- index.php
|   `-- wp-config.php
|-- .gitignore
|-- composer.json    <-- If using Composer
`-- local-config.sample.php

1. Bootstrap index.php

This part is relatively simple. We’re going to install WordPress into the public/wp subdirectory, so we need to tell our webserver where to find WordPress. As everything is routed through the main index.php file (see .htaccess) we can simply create a bootstrap index.php file with the following contents:

// WordPress bootstrap
define( 'WP_USE_THEMES', true );
require( './wp/wp-blog-header.php' );

2. Move wp-content

We achieve this by moving the wp-content directory to public/wp-content. WordPress won’t know that we want it to use this new wp-content location so we need to tell it to by setting the WP_CONTENT_DIR and WP_CONTENT_URL in our new public/wp-config.php file (see below).

3. Environment config

You’ll notice the local-config.sample.php file in the root directory above. While this is not technically necessary it’s a good idea to split up environment-specific config information (such as database credentials and secret keys) from the “global” config information stored in public/wp-config.php (such as the table prefix and the wp-content location).

The idea here is that the public/wp-config.php file will hold any “global” config information and then load the correct “environment” config file as required. So for your production site you can deploy a production-config.php file which can be loaded by public/wp-config.php if it exists (we don’t store it in the Git repo though for security purposes) and if production-config.php doesn’t exist fall back to local-config.php which assumes you are working on a local development environment.

The content of our new public/wp-config.php file should look like:

ini_set( 'display_errors', 0 );

// ===================================================
// Load database info and local development parameters
// ===================================================
if ( file_exists( dirname( __FILE__ ) . '/../production-config.php' ) ) {
    define( 'WP_LOCAL_DEV', false );
    include( dirname( __FILE__ ) . '/../production-config.php' );
} else {
    define( 'WP_LOCAL_DEV', true );
    include( dirname( __FILE__ ) . '/../local-config.php' );

// ========================
// Custom Content Directory
// ========================
define( 'WP_CONTENT_DIR', dirname( __FILE__ ) . '/wp-content' );
define( 'WP_CONTENT_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/wp-content' );

// ================================================
// You almost certainly do not want to change these
// ================================================
define( 'DB_CHARSET', 'utf8' );
define( 'DB_COLLATE', '' );

// ================================
// Language
// Leave blank for American English
// ================================
define( 'WPLANG', '' );

// ======================
// Hide errors by default
// ======================
define( 'WP_DEBUG_DISPLAY', false );
define( 'WP_DEBUG', false );

// =========================
// Disable automatic updates
// =========================

// =======================
// Load WordPress Settings
// =======================
$table_prefix  = 'wp_';

if ( ! defined( 'ABSPATH' ) ) {
    define( 'ABSPATH', dirname( __FILE__ ) . '/wp/' );
require_once( ABSPATH . 'wp-settings.php' );

We need to rename our local-config.sample.php to local-config.php to use it. It should look something like this:

define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'exampleuser' );
define( 'DB_PASSWORD', 'examplepassword' );
define( 'DB_HOST', 'localhost' );

ini_set( 'display_errors', E_ALL );
define( 'WP_DEBUG_DISPLAY', true );
define( 'WP_DEBUG', true );

define('AUTH_KEY',         ',7jxG`)ZYM/m1OB6G/&z)gL=oW={,B1-&xcGuySKE.vQh_-fI)j$y^222Hs8cR&W');
define('SECURE_AUTH_KEY',  '=3/+qt|eD2>(|nx@-p|vp&8T*n6;ZKZ1[91m`a^-PbV+wzbiK ,gyNe&iTpHI(+1');
define('LOGGED_IN_KEY',    'Wu5@R=lv&j!.~IMD_D;%gzG>NSYfNG-K B@6wd]cp2|jYCgHV;>dSe[. u|f@2V]');
define('NONCE_KEY',        '=d#!`FCws;6W-z%j%:Jh8@-~U|k[PoA8Lb+.r4yf_fi*1bJXMQm2bu{j@ObTNlSe');
define('AUTH_SALT',        '|~mjb|}FFR~b=jcF--;6.`KEO|wP>f&|+2s-#4]6QzY7o4#^y2&9mabHT..DR<O-');
define('SECURE_AUTH_SALT', '+YmVlzrBIw!kMkq(j3p&5+IU17>+ea[E9ZNdH-*k)(cCc74N^7CVd|ol(*^i]do!');
define('LOGGED_IN_SALT',   'rG9_QAj+~/q2UA*5Fk(Q](/NY&IG}[Z8&uf+Q{;YB/uA0Q.iFU:UW *OCN;|FUi1');
define('NONCE_SALT',       'uBW!%ut#F]]5Etl3MwAi|;9 82#qY9(x:])4BU*y{4BrSHk^hT&E6>m<`)zwsaIs');

One last thing we want to do is make sure we’re ignoring the correct files and folders in our .gitignore file so that only the required files and folders are stored in our Git repo. The /vendor folder is where composer will store its dependencies so we want to ignore it too.


Now that we have our structure in place we can go ahead and install WordPress.

Install WordPress using Composer

As we looked at in part 2 Composer is a great way to manage packages in your PHP application, and the same applies if you want to use WordPress as a dependency. As in part 2 we’re going to use WP Packagist for our themes and plugins (although I’ve excluded them for this example) but this time we’re also going to add the johnpbloch/wordpress package which is a Composer compatible duplicate of the WordPress repo. Our composer.json file should look like:

    "repositories": [
            "type": "composer",
            "url": ""
    "require": {
        "php": ">=5.4",
        "composer/installers": "1.*",
        "johnpbloch/wordpress": "4.3.*"
    "extra": {
        "wordpress-install-dir": "public/wp",
        "installer-paths": {
            "public/wp-content/mu-plugins/{$name}/": ["type:wordpress-muplugin"],
            "public/wp-content/plugins/{$name}/": ["type:wordpress-plugin"],
            "public/wp-content/themes/{$name}/": ["type:wordpress-theme"]

There are three things to note here:

  1. We are installing version 4.3.* of WordPress. This means Composer will install the latest version of WordPress 4.3.x but not version 4.4 and above (once it’s released). This is to prevent Composer installing breaking changes by mistake. If you want WordPress to always update to the latest version when using composer update change this to *.
  2. Inside the “extra” settings we’re telling Composer to install WordPress in our new subdirectory location at public/wp via “wordpress-install-dir”.
  3. We’re also using the “installer-paths” directive to tell Composer that the location of our wp-content directory has changed to public/wp-content and this is where it should install our themes and plugins.

It might seem like we’ve done a lot of work but this is where we begin to see the advantage of a setup like this. Now we just need to run composer install to install WordPress or composer update to update WordPress. Deployment strategies are outside the scope of this article but hopefully you can see how powerful and easy this kind setup makes deployments.

Install WordPress using Git Submodules

If Composer isn’t your thing we can achieve the same outcome using Git Submodules. Instead of using a composer.json file we can add WordPress as a Git Submodule (just like we did for themes and plugins in part 3) to install WordPress in a subdirectory.

To add WordPress as a submodule run the following command from the root of the project:

git submodule add public/wp

For stability reasons it is recommended that we install a stable version of WordPress. We can do this by checking out a specific tag:

cd public/wp
git checkout tags/4.3.1
cd ../..

Remember we need to commit any changes we make to submodules:

git commit -am "Install WordPress 4.3.1 as a submodule"

Updating WordPress is a similar workflow:

cd public/wp
git fetch -a
git checkout tags/4.3.2
cd ../..
git commit -am "Update WordPress to 4.3.2"

And that’s how we are able to install and manage WordPress as a Git Submodule.

It’s worth mentioning at this point that some of the concepts that I’ve used in both of the above methods are based on Mark Jaquith’s Skeleton repo which is definitely worth checking out if you want to look into these kind of setups a bit further.

I hope you’ve enjoyed this series on managing your WordPress site with Git and Composer and are able to take some, or all, of this workflow and implement it in your own WordPress sites.

Remember that what we have covered in this series is not necessarily the “right” way to manage WordPress in Git, but just one way you can use Git and WordPress together. Take the bits that work for you and create your own workflow and ignore the rest, or improve this workflow. It’s up to you.

Got any tips or suggestions I haven’t covered in this series? Let me know in the comments.

About the Author

Gilbert Pellegrom

Gilbert loves to build software. From jQuery scripts to WordPress plugins to full blown SaaS apps, Gilbert has been creating elegant software his whole career. Probably most famous for creating the Nivo Slider.