One WordPress Install, Multiple Sites

Last modified on May 7th, 2009

A while ago I started messing around with trying to get a single WordPress installation to host multiple blogs. If you read that and think WordPress MU, you’re not far off. What I don’t like about WordPress MU though is that the system administrator chooses the themes and the plugins that are available to the end user — ideally, I would want each user to control that themselves, such that it basically has the same functionality as a normal WordPress installation.

There are a lot of other reasons why you might want a single WordPress install for multiple blogs. First, if you make backups of each blog’s data from time to time, you might end up with a complete WordPress package for each website you host, even though ultimately 90% of those files are identical (basically only themes, plugins and custom content vary). Second, if you run a hosting server with a PHP caching engine (which most do), it’s likely that the cache keeps track of data using the complete path to the file, which ultimately means the cache effectiveness will decrease proportional to the number of sites (aka WordPress installations). If all the installations on a server shared one common WordPress install, you’d only have to cache that one set of PHP files — effectively you could keep WordPress in a compiled state in memory for all of your sites.

The first thing I did was install a fresh WordPress into a directory on my server called wp_install. Normally an end user would go through the installation process and configure that one installation, and then they’d have one blog. We’re trying to make one installation work for many blogs.

Next, I created a wp_domains directory at the same level as wp_install. Inside wp_install, I then created a symbolic link called “domains” which points to the wp_domains directory (you could also just put everything in the wp_install/domains directory, but I want to keep them separated slightly). So we have wp_install/domains symbolically linking to wp_domains.

In order to make one installation work for many sites, you basically need to have a dynamic wp-config.php file that can change depending on which site is being accessed. So I started messing with wp-config.php to try and come up with something that would work. The end goal was to change the parameters in the wp-config.php ssuch that we can use site-specific content in the wp_domains directory.

I ended up with something like this:

$website = strtolower( str_replace( "www.", "", $_SERVER["SERVER_NAME"] ) );
$website = preg_replace('[^a-z0-9\.-]', '', $website );

define( 'WP_CONTENT_DIR', dirname(__FILE__) . '/domains/' . $website . '/wp-content' );
define( 'WP_CONTENT_URL', 'http://' . $_SERVER["SERVER_NAME"] . '/domains/' . $website . '/wp-content' );

if ( file_exists( dirname(__FILE__) . "/../wp_domains/$website/db.config" ) ) {
require_once( dirname(__FILE__) . "/../wp_domains/$website/db.config" );
} else {
echo "Sorry, no configuration defined."
die;
}

Several things happen in the code, so let me try to explain. First, the destination website is determined using the HTTP server name which is passed in the address. For this website, that becomes www.duanestorey.com. I strip off the www just so that there’s a bit of consistency between sites. Next, I tell WordPress to change its default wp-content directory to the domains/WEBSITE_NAME/wp-content directory, which in my case is domains/duanestorey.com/wp-content.

In the next section of code, I check for domains/WEBSITE_NAME/db.config, which is a site-specific file that will set the database information on a per-site basis. If it exists, we load it up such that the username, password, hostname, and database name for that particular site.

An example of db.config for my site is:

<?php
define( 'DB_NAME', 'duanestorey-com' );
define( 'DB_HOST', 'localhost' );
define( 'DB_PASSWORD', 'user' );
define( 'DB_USER', 'password' );
?>

At this point, everything worked fine, and I had a base WordPress installation where the wp-content directory and database information could be split out dynamically. From a hosting perspective, you could conceivably only grant access to that one directory (i.e. wp_domains/duanestorey.com), which would allow the user to modify their plugins and themes for their site only (even though the core WordPress files are being shared).

It’s at this point I actually decided to do a Google search to see if anyone else had tried this approach before. I found this article on Virtual Multiblogs which is based on a similar approach. It looks like he used a bunch of different directories all mapped to one WordPress directory. In the approach I’ve taken, I point everything to the same directory (in the HTTP virtualhost section), and simply rely on the HTTP_HOST/SERVER_NAME that’s sent with each request. In addition, I’m doing the wp-content remapping, which I don’t think was done with the Virtual Multiblogs approach. I also did a bit more, as you’ll see next.

You might be asking why I didn’t just do away with the wp-content directory although, and simply remap it to wp_domains/duanestorey.com — it’s a good question.

There are a few problems with the current approach. First, remapping the wp-content directory often breaks plugins because many plugin developers hardcode the URLs as /wp-content in the code (it’s sloppy, but I’ve done it previously as well). So as soon as you move your wp-content directory, all those plugins start showing errors. Second, some javascript libraries and statistics programs (the popular Mint program, for example) must be installed in the root of the website. With this model, you really wouldn’t want users to mess with any core WordPress files or directories, so they really shouldn’t have access to the root installation. So what do we do?

Well, I came up with a little .htaccess hack such that anything that isn’t found in the core WordPress path is automatically redirected to the domains/WEBSITE_NAME directory. That means if http://www.migratorynerd.com/test.html isn’t found on disk in wp_install/test.html (which is the base WordPress installation, so it definitely won’t be there), the .htaccess redirect will send it to wp_domains/WEBSITE_NAME/test.html (or in my case, wp_domains/duanestorey.com/test.html). The redirect is internal, which means that from the user’s perspective they are really looking at http://www.migratorynerd.com/test.html (but on disk they are actually viewing the file in wp_domains/duanestorey.com/test.html).

This approach should fix most of the plugin problems since it uses URL remapping to trick the website into thinking everything actually is in the http://somedomain.com/wp-content directory, even though there’s nothing really there. It also should allow users to install whatever they want in their directory (mint, javascript, whatever), since anything not actually found in wp_install will redirect to their site-specific domain files in wp_domains.

In fact, once you do the .htaccess change, there’s no need to define WP_CONTENT_URL in wp-config.php anymore, since the htaccess will take care of all of that automagically. So, feel free to remove:

define( 'WP_CONTENT_URL', 'http://' . $_SERVER["SERVER_NAME"] . '/domains/' . $website . '/wp-content' );

Internally, http://www.migratorynerd.com/wp-content/themes/duane-apr09/style.css is actually mapping to http://www.migratorynerd.com/domains/duanestorey.com/wp-content/themes/duane-apr09/style.css, but since .htaccess is hiding all of that, plugins with hardcoded elements should work normally.

I’ve currently moved this website over to this new approach, and will probably move a few more tomorrow and test it out further. Ultimately I can stop backing up whole directories with WordPress installations, and simply backup my wp_domains directory nightly. So far, things appear to be working quite smoothly (even old images that were inserted into posts using relative /wp-content/ URLs work properly thanks to the .htaccess trick).

The current structure of my wp_domains directory is:

  • /duanestorey.com
  • /duanestorey.com/mint/*
  • /duanestorey.com/wp-content/*
  • /duanestorey.com/db.config

The .htaccess hack I came up with is below:

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !(.*)/$
RewriteRule ^(.*)$ http://%{HTTP_HOST}/$1/ [L,R=301]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ domains/%{HTTP_HOST}/$1 [L]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress

I’m by no means a .htaccess expert, so if you see anything wrong with it, please let me know. It seems to be working though (I have Mint working).

15 responses to “One WordPress Install, Multiple Sites”

  1. Boris Mann says:

    I remember that Scott Hadfield told me at one point that he had made a WP install multisite. Drupal does this, you MIGHT have a vulnerability from just using SERVER_NAME as is – this is the issue from Drupal.

    I’m looking forward to wp-content being moved out of the root as part of core, to enable this automatically.

  2. Duane Storey says:

    You can already move wp-content out of root, but that doesn’t give you multiple blog support — it just allows you to put it somewhere else 🙂

  3. Duane Storey says:

    And yah, I can see how that might be a security risk there. I added that line from the Drupal fix up above — thx.

  4. Stephen R says:

    Hi — Thanks for the nice mention (I’m the Virtual Multiblog guy).

    First off, I like your /domains/ system. The idea that anything not in the root moves to a site-specific folder is a good one. I’ll definitely look at it closer. 🙂

    Regarding Virtual Multiblog — it does what your system does — allow sites on different domains. What it *also* does is allow sites in directories. You could for example run duanestorey.com and duanestorey.com/blog and duanestorey.com/blog2 as completely separate WordPress blogs. You could then throw in someotherdomain.com/blog3 as well.

    Still — your system is a lot cleaner if you’re just doing domains, and in code, clean is very nice. 😉

  5. Boris Mann says:

    duaner.org got broken by your hack

  6. Duane Storey says:

    @Stephen – thanks for the info. That’s cool about the domains in directories. Let me know if you make any changes.

  7. Duane Storey says:

    @Boris – thx. That’s fixed. Just needs a symbolic link. Needs a better solution though.

  8. Andrea_R says:

    😀 I knew about the other method, although I haven’t had a chance to play with it. I *am* impressed though.

  9. Funy says:

    Some nice work you have done. I’m looking at starting a second blog and this would get around me having to pay again for another hosting. Assuming they let me point two domains to the same site.

  10. […] One WordPress Install, Multiple Sites | the duane storey — 1:33am via […]

  11. […] Here’s an interesting experiment: how would you like to run a single WordPress installtion of multiple webistes? Duane Storey tried just that and the details are all here. […]

  12. […] One wordpress install, multiple sites […]

  13. icabod says:

    Hi. Nice hack there – I’m considering implementing a single-install/multi-site thing myself.

    One thing I would note tho’ (I confess I’ve not checked if your .htaccess would solve this)… it could be fairly insecure to store your database password information in a file called “db.config”, unless your server is set up to parse .config files as if they’re PHP.

    It would be better to call the file “db_config.php”, and perhaps protect it using .htaccess.

    Just a thought – otherwise, nice stuff :]

  14. Glenn says:

    I’m working on a script (not a plugin) that will do the same thing with a nice install and site management tool. Let me know if you’re interested in testing this out.

  15. Schell says:

    Hey, thanks for your help, it’s a great idea. On another note, your db.config file lists the user and password lines as:

    define( ‘DB_PASSWORD’, ‘user’ );
    define( ‘DB_USER’, ‘password’ );

    Which should be:

    define( ‘DB_PASSWORD’, ‘password’ );
    define( ‘DB_USER’, ‘user’ );

Leave a Reply

Your email address will not be published. Required fields are marked *