WordPress core symlinked

What is a symlink?

More properly a symbolic link, it’s a pointer from one location on the file system (in UNIX or Linux) to another. It’s like a magic tunnel that lets a set of files in one directory pretend that they also live in other directories without having to make a bunch of copies of them.

Why would you symlink WordPress core?

The thing about having a bunch of WordPress environments for development, testing and staging is that when you update your version of WordPress you wind up copying WordPress core files around a lot. Copy, copy, copy – waiting for a deploy to happen is tedious and a great opportunity to get all distracted on Twitter or something.

There is also a lot more risk of winding up with a corrupted WordPress install when you are doing lots of copying, should the network or file system have a failure during this lengthy process. Changing a symlink is nearly instantaneous and much less risky.

I can’t take credit for this clever idea; I heard about it from a WordCamp talk by Rami Sayer.

What kinds of weird configurations are necessary to make this work?

So glad you asked! Bear in mind that to begin with, WordPress core needs to be running in its own directory, separate from your content area where your plugins and themes are housed.

Additionally, you also need to have a wp-config.php where you are storing your WP core files, and this has to load your real wp-config.php, then the wp-settings.php file (that lives in WP core) (and I’ll get to the WP-CLI shenanigans below):

suchserver> ll
 drwxr-xr-x    4096 Nov  6 16:04 ./
 drwxr-xr-x   12288 Dec 10 15:07 ../
 drwxr-xr-x    4096 Nov  5 15:23 wp.4.2.3/
 drwxr-xr-x    4096 Jul 24 10:10 wp.4.3.1/
 -rwxr-xr-x     370 Nov  2 16:17 wp-config.php*
suchserver> cat wp-config.php

if ( strpos( $_SERVER['DOCUMENT_ROOT'], 'foo_core') === false ) {
    require "${_SERVER['DOCUMENT_ROOT']}/wp-config.php";
} else {
    //error_log( "WP-CLI IN DA HOUSE: " . print_r( $_SERVER, true ) . "\n", 3, '/foo_lisa/content/debug.log'  );
    require "${_SERVER['WP_UPDATE_CONFIG']}";

require_once( ABSPATH . 'wp-settings.php' );

and in the real wp-config.php that lives with the files for each instance, you have to prevent it from loading wp-settings.php for the instances that are using a symlink to get to WP core.

// ===================
// Bootstrap WordPress
// ===================
if ( !defined( 'ABSPATH' ) )
	define( 'ABSPATH', dirname( __FILE__ ) . '/wp/' );

if (
		$_SERVER['HTTP_HOST'] == 'doge.foo.myco.com'
) {
	require_once(ABSPATH . 'wp-settings.php');
} //only do this on sites that have not yet switched to a symlinked wp core

As you may surmise from the commented out debug line, WP-CLI didn’t like this configuration. It couldn’t get to the real wp-config.php. So in bash scripts where I need to run WP-CLI commands, I now export a var in the UNIX environment with the location of that file. That’s where _SERVER['WP_UPDATE_CONFIG'] comes from.

Maintenance mode

I like to put sites in maintenance mode before I do a deploy, particularly when updating core. The “native” WP maintenance mode, wp_maintenance(), can only really be triggered if you put a file called .maintenance in with your WP core files. Well, that’s cool unless all of your WP environments are using the same WP core directory. In that case, putting a .maintenance file in WP core puts all environments into maint mode at once, which is not really what we want to do most of the time.

I thought about putting some logic in that file to only trigger the timestamp that it has to contain for the environment currently being worked on but in the end decided it would be better if I could trigger maint mode with files in the environment rather than in core.

StackExchange to the rescue; replicate maint mode with a mu-plugin and set up the trigger for it that you want. Fortunately maint mode is pretty simple since unfortunately we are having to replicate it. [1]

 * put an instance into maintmode by defining the constant IN_MAINTENANCE for just that instance
 * this gets around the need to create a .maintenance file in wp core, which would put ALL of
 * our symlinked instances into maintmode at once - not what we want.
 * build / deploy scripts will set the constant
 * from http://wordpress.stackexchange.com/questions/169600/redirect-visitors-to-a-temporary-maintenance-page/169610#169610
 * LLA 12/8/15

add_action( 'wp_loaded', function() {
        defined( 'IN_MAINTENANCE' )
    ) {
        header( 'HTTP/1.1 Service Unavailable', true, 503 );
        header( 'Content-Type: text/html; charset=utf-8' );
        if ( file_exists( WP_CONTENT_DIR . '/maintenance.php' ) ) {
            require_once( WP_CONTENT_DIR . '/maintenance.php' );
suchserver> cat ./deploy/sww-maintmode.php

 * put a WP instance in maintenance mode
 * set constant IN_MAINTENANCE
 * do this outside of wp core b/c we only want to affect one install at a time
 * moving to a model where every install symlinks back to the same copy of wp core
 * adding a .maintenance file to wp core would put all into maintmode at once.
 * the file that is written, in_maintenance.php, is included into wp-config.php
 * if it exists

$instance = $argv[1];
$maintenance_string = "?php define( 'IN_MAINTENANCE', true ); ";
$maintenance_file = '/thing/stuff/foo_' . $instance . '/in_maintenance.php';

Automate “Update Network”

It is theoretically possible to update your network after a WP version change by using WP-CLI. I have even written a script that I dutifully run as part of the Bamboo deploy job for updating WP core. It doesn’t work. I mean, it runs happily but it does not actually update the network and I don’t know why. So I click the button manually in each instance after deploy. Soooo… yes, I’ll figure out that problem some day. Or maybe it’s a bug in WP-CLI which will magically be fixed. [2] Yes, let’s do that last one. Okay.

# update database after a WP core update
$WP_CLI_LOC core update-db \
    --network \
    --debug \
    --path=$WP_UPDATE_PATH \

A separate Bamboo build and deploy workflow for core updates

Because the set of tasks needed for a WP core update are now quite different from those needed for typical daily code deploys, I have created a separate build plan and associated deploy environments in Bamboo for this workflow.

After refreshing the deploy scripts, the Bamboo jobs put the instance into maint mode, then run a script to change the symlink:


# ================================================== #
# symlink to current WP core
# set WP_VERSION number - must match what is in
# composer.json for the upload dir for WP
# ================================================== #

# SWWHOME-376 inagural run, WP 4.3.1

# set the instance to update
# expects first cmd line argument to be the instance we are updating, such as lisa or autotest

# test for $1 and if it exists assign its value to WP_UPDATE_INST
# use autotest as a default b/c we can always alter autotest safely

if [ -z "$1" ]


# cd to root level of our instance

# create symlink to new wp core version in /foo_core/
# version is passed in as an argument from the calling script
if [ -z "$WP_VERSION" ]
    echo "no version number argument, did not change symlink."
    rm -fR wp
    ln -s ../inside_core/${WP_VERSION} wp

Because WP-CLI doesn’t work while the site is in maint mode, Bamboo then takes it out of maint mode and fruitlessly runs the update network script. Then I click the update network button.


Would an infrastructure post be complete without at least mentioning Composer? No, of course not.

Composer was already putting WP core into its own directory within the directory where composer update was being run. It will very cooperatively put it in a completely different location if you give it a relative path, which is what I wanted. This part just worked beautifully with no difficulty that I can recall. [3]

suchserver> pwd

suchserver> ll ../foo_core
drwxr-xr-x    4096 Nov  6 16:04 ./
drwxr-xr-x   12288 Dec 10 15:07 ../
drwxr-xr-x    4096 Nov  5 15:23 wp.4.2.3/
drwxr-xr-x    4096 Jul 24 10:10 wp.4.3.1/
-rwxr-xr-x     370 Nov  2 16:17 wp-config.php*

suchserver> cat composer.json
  "extra"       : {
    "wordpress-install-dir": "../foo_core/wp.4.3.1",
    "installer-paths" : {
      "content/plugins/{$name}" : ["type:wordpress-plugin"]

Yay Composer.



  1. I think there’s a whole post about “why is it so hard to do infrastructure stuff and why doesn’t WordPress give us a more sane file structure for deployment” to be written maybe one day if I feel like spitting in the wind.
  2. It’s probably not a bug and it’s probably a problem on my end and it will probably be months before I have time to actually figure it out.
  3. Because many things happened at work, and I also got sick a lot this fall, it took me months to do all of this and I don’t really remember if Composer just worked magically or not but I think it did.

2 thoughts on “WordPress core symlinked

  1. Very nice write up, thank you. I found your article and flow process answered a lot of decisions and problems I have been running into.. been considering getting bamboo for managing multiple client websites that require staging and testing.

    In regards to mysql databases have you considered a bash script to do a current database dump from production into the current build test before pre-deployment?

    1. thanks, i’m glad it’s helpful!

      i actually have a note on my desk that we need to add a db dump step to the wp core update process. for our dev and test instances, we actually have a script to copy all content from the content staging site, and once that script is perfected i’ll add it to the deploy jobs. right now i run it manually.

      for stage, however, we do need to do a backup.

      i also immediately found the problem with having a single copy of wp core that composer can alter – oops. so some changes are coming to protect the copy that’s in use from being nuked. i’ll write those up as well some time in the new year.

Comments are closed.