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

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:

<?php
// 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:

<?php
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
// =========================
define( 'AUTOMATIC_UPDATER_DISABLED', false );

// =======================
// 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:

<?php
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.

/public/wp
/vendor
production-config.php
local-config.php

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": "http://wpackagist.org"
        }
    ],
    "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 https://github.com/WordPress/WordPress 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.

  • I’m running my sites like this using Mark Jaquith’s WordPress skeleton as a starting point.

  • Shawn Beelman

    For local development I’m running MAMP on a Mac. My hard drive is littered with 15+ installs of WP. Of course I tried creating a local site running from symlinks to one set of master WP core files, but as you can probably guess, that doesn’t work. Does anyone know of a way to do this? Or am I stuck with maintaining separate installations of core WP files for every local site?

    • It’s probably not a good idea to run multiple sites from a single WP install like that. I’d recommend either using WordPress Multisite to run multiple sites from one WordPress install or use something like WP CLI to manage multiple sites in an easy way.

  • I installed wp like descripted above.

    Now there’s always the “/wp/” folder inside the uri.

    my .htaccess (inside public folder) looks like:

    RewriteEngine On
    RewriteRule ^index.php$ – [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . index.php [L]

    can anybody help to fix my issue?

    • Your WordPress admin will now always have /wp in the URL. To remove it from the front site make sure the following options are set in your options table in the database:

      siteurl: http://example.com/wp (with /wp)
      home: http://example.com (without /wp)

      • thanks! That worked for me!

      • Andy Eblin

        I’m having a similar issue here, but the change you’ve suggested doesn’t seem to work. For whatever reason, navigating to a bare domain (Using MAMP right now, so just localhost:8888) returns an index of / page. Admin is living at localhost:8888/public/wp/wp-admin and the actual pages are at localhost:8888/public/wp

        Any idea how to get the front site at a bare domain?

  • Maybe I missed something but how is the new file in public/wp-config.php loaded? The new index.php is including ./wp/wp-blog-head.php and this is using wp-load.php to look for an existing wp-config.php file. However the code from WordPress is not using public/wp-config.php because we are missing a public/wp-settings.php.

    So how do you include wp-config.php in the public dir?

    • Ok / I fixed it. It wasn’t your or WordPress’ fault ^^ Everything is fine now.

    • I’m running into the same issue, how do you solve it?

  • Julie Johnstone Reyes

    Thanks for this article. The comments are very helpful too. If I install WordPress as a submodule, is auto updating turned off? Am I sill able to update the plugins from WordPress. I really only want to version control my theme files…

  • Jonathan Hodgson

    Could this be a problem for plugins? The includes folder, for example, is now in a different place relative to the plugins folder. Could this be rectified by making symlinks or is that not neccesary?

  • Really late to the party here but if we don’t keep config info (db details and salt keys) on the git repo, what would be the preferred solution? Would it simply be a local file? Surely that isn’t the best method in a studio environment

    • dan

      Unless you change db names and details regularly, manually adding those files doesn’t feel like a pain to me. Feels better and les prone to “push” mistakes.

  • Liore

    @gilbertpellegrom:disqus I made a little package based on this article to quickstart WP projects in Git. Let me know what you think. Thanks for posting this. https://github.com/aaisol/wp-composer

  • marijan raicevic

    I’m trying to run WP with submodule in the dir (wordpress), in the root I have wp-content and index.php, wp-config.php. On my local machine running WAMP I’m getting re-direct to wordpress/wp-admin.install.php error 404 not found. Also php_erro.log ([04-Nov-2016 07:06:02 Europe/Paris] PHP Warning: require(C:Program Files (x86)wampwwwmvfl/wordpress/wp-blog-header.php): failed to open stream: No such file or directory in C:Program Files (x86)wampwwwmvflindex.php on line 17
    [04-Nov-2016 07:06:02 Europe/Paris] PHP Fatal error: require(): Failed opening required ‘C:Program Files (x86)wampwwwmvfl/wordpress/wp-blog-header.php’ (include_path=’.;C:phppear’) in C:Program Files (x86)wampwwwmvflindex.php on line 17).
    My core index.php is only re-directing to /wordpress/wp-blog-header.php to start the WP installation. So I can’t even start to install WP. Same project and many others like this one work perfectly fine on other machines, as matter of fact I tested and copy-past the wordpress dir to another project and it worked, started the installation process.

    Any help will be appreciated.

  • Wesley Guirra

    I’v done as you say above but now /wp-admin is in redirect loop, i’ve tried to set siteurl to myurl.dev/wp but not worked.

  • Josh Stauffer

    I really appreciate all four articles here, Gilbert. I had to read them a couple times each but after that I feel like my understanding is improving.

    While it might only throw a PHP notice, it seems that WP_DEBUG_DISPLAY and WP_DEBUG are defined twice. Once in public/wp-config.php and once in local-config.php.

    Thanks, again, for the articles. Still useful even a few years down the line.