Drupal multisite, clean URLs and lighttpd

Like I mentioned previously, this site is now being served up by lighttpd; it took a while to get clean URLs working right, but what right now is making my server tick is a very carefully laid-out filesystem and set of bash scripts and lighty config files, wrapped around a Drupal multisite installation to make installing modules and upgrading the entire system as intuitive and painless as possible. Upgrading Drupal (from 6.2 to the brand-new 6.3 for example) takes no more than running a single command. Here's an overview of how my own site is set up, generalized to make it applicable to almost any other system.

As a general disclaimer, my server is running Ubuntu Server 8.04 so your filesystem layout may vary slightly, and I'm assuming you have a general knowledge of Linux terminology and bash commands, and that lighttpd with PHP and FastCGI are already set up.

To start off, here's how the web server file system root (/var/www for me) looks:

  1. /var
  2. |- www
  3. |- drupal -> drupal-6.3
  4. |- drupal-6.3
  5. | ...
  6. |- includes
  7. | ...
  8. |- image.imagemagick.inc -> /var/www/drupal-sites/all/modules/image/image.imagemagick.inc
  9. | ...
  10. | ...
  11. |- sites -> /var/www/drupal-sites
  12. | ...
  13. |- drupal-sites
  14. |- all
  15. |- modules
  16. |- themes
  17. |- default
  18. |- example1.com -> /var/www/example1.com/drupal
  19. |- example2.com -> /var/www/example2.com/drupal
  20. |- example1.com
  21. |- drupal
  22. |- files
  23. |- htdocs -> /var/www/drupal
  24. |- sub
  25. |- htdocs
  26. |- example2.com
  27. |- drupal
  28. |- files
  29. |- htdocs -> /var/www/drupal

To explain briefly in words:

  • The symlink drupal points to the folder of the current version of Drupal, so that the only thing that has to change when upgrading to a new version of Drupal is the one symlink.
  • The drupal/sites directory points to the drupal-sites directory, which contains the all (and all/modules, where you should install modules for all sites to access them, and all/themes, the same as with modules except for themes) and default directories (which you'll never put anything in or do anything with, but need to keep around; copy it over from a freshly-downloaded Drupal).
  • example1.com and example2.com have their own directories in /var/www; each one has three folders:
    • drupal, which has a symbolic link (ln -s) pointing to it from /var/www/drupal-sites as indicated above;
    • files, which is used as the upload directory with the private download method (if you use the public method, you can just leave the files directory in the site's drupal directory alone and omit this one);
    • htdocs, which is the document root for the site pointed to by the site's lighttpd configuration file (explained later), and which points back to the Drupal directory symlink for Drupal's multisite handling to work.
  • If you install Image.module and want to use the ImageMagick library, make a symlink in the drupal/includes directory pointing to the image.imagemagick.inc file in drupal-sites/all/modules/image (the Image.module directory) so that upgrading Image.module also upgrades the ImageMagick include.

Next up is configuring lighttpd. Only a couple changes need to be made in /etc/lighttpd/lighttpd.conf (the global lighttpd configuration file):

  1. Uncomment the lines with "mod_rewrite", "mod_redirect", and "mod_evhost" to enable those modules.
  2. Further down, look for a commented line beginning with #evhost.path-pattern. Uncomment it or start a new line below it, and write the following:
    1. evhost.path-pattern = "/var/www/%0/htdocs/"
    Or if you're like me, and want (a) to forcibly remove "www." from the beginning of URLs and (b) handle subdomains for each site:
    1. $HTTP["host"] =~ "(^|^www\.)[^.]+\.[^.]+$" {
    2. evhost.path-pattern = "/var/www/%0/htdocs/"
    3. }
    4. $HTTP["host"] !~ "(^|^www\.)[^.]+\.[^.]+$" {
    5. evhost.path-pattern = "/var/www/%0/%3/htdocs/"
    6. }
    and for each subdomain sub.example1.com you want to have, make the directory /var/www/example1.com/sub/htdocs.

To handle Drupal's clean URLs we'll use lighttpd's mod_magnet, which lets you use Lua scripts to handle requests in lighttpd. Put the following in /etc/lighttpd/drupal.lua (or save the attached drupal.lua file to /etc/lighttpd):

  1. -- /etc/lighttpd/drupal.lua
  2. -- Based on <http://www.morphir.com/Lighttpd-Install-and-configuration-for-Drupal-with-clean-url>
  3. -- Taken from <http://pub.jbhannah.net/scripts/drupal.lua>
  4. -- little helper function
  5. function file_exists(path)
  6. local attr = lighty.stat(path)
  7. if (attr) then
  8. return true
  9. else
  10. return false
  11. end
  12. end
  13. function removePrefix(str, prefix)
  14. return str:sub(1,#prefix+1) == prefix.."/" and str:sub(#prefix+2)
  15. end
  16.  
  17. -- prefix without the trailing slash
  18. local prefix = ''
  19.  
  20. -- the magic ;)
  21. if (not file_exists(lighty.env["physical.path"])) then
  22. -- file still missing. pass it to the fastcgi backend
  23. request_uri = removePrefix(lighty.env["uri.path"], prefix)
  24. if request_uri then
  25. lighty.env["uri.path"] = prefix .. "/index.php"
  26. local uriquery = lighty.env["uri.query"] or ""
  27. lighty.env["uri.query"] = uriquery .. (uriquery ~= "" and "&amp;" or "") .. "q=" .. request_uri
  28. lighty.env["physical.rel-path"] = lighty.env["uri.path"]
  29. lighty.env["request.orig-uri"] = lighty.env["request.uri"]
  30. lighty.env["physical.path"] = lighty.env["physical.doc-root"] .. lighty.env["physical.rel-path"]
  31. end
  32. end
  33. -- fallthrough will put it back into the lighty request loop
  34. -- that means we get the 304 handling for free. ;)

Ubuntu's lighttpd comes with a neat system for handling the configuration for separate modules: additional configuration files can be placed in /etc/lighttpd/conf-available with the naming convention ##-NAME.conf (the lower the two-digit number ##, the earlier it gets loaded), then enabled (or, symlinked in the /etc/lighttpd/conf-enabled directory) with the command (as root or sudo)

  1. ~# lighty-enable-mod NAME

replacing NAME with the NAME part of the filename of the configuration file. After that you have to make lighttpd reload its configuration and see the new symlink in the conf-enabled directory:

  1. ~# /etc/init.d/lighttpd force-reload

For example, do the following to install mod_magnet on Ubuntu:

  1. ~# apt-get install lighttpd-mod-magnet
  2. ~# lighty-enable-mod magnet
  3. ~# /etc/init.d/lighttpd force-reload

Installing mod_magnet creates the file /etc/lighttpd/conf-available/10-magnet.conf, and the lighty-enable-mod command creates a symlink to that file in the conf-enabled directory; then lighttpd picks up the newly enabled configuration file upon reloading.

Another use for this configuration layout is creating and keeping separate configuration files for each domain. Drupal will install and work up to the point before installing mod_magnet and creating drupal.lua, but clean URLs won't work because lighttpd can't handle directory-specific configuration files (like Apache's .htaccess files, which Drupal is set up to handle out of the box). Instead, we'll create a file in /etc/lighttpd/conf-available for each domain, and in there tell lighttpd to use drupal.lua when handling requests for that domain. For example, the file /etc/lighttpd/conf-available/20-example1-com.conf might look like:

  1. # The following redirects all non-existing subdomains and the "www." prefix to
  2. # the top domain. Change 'sub' below to a vertical-bar (|)-delimited list of
  3. # subdomains.
  4. $HTTP["host"] !~ "^((sub)\.)?example1\.com" {
  5. $HTTP["host"] =~ "^(.+\.)example1\.com" {
  6. url.redirect = ( "^/(.*)" => "http://example1.com/$1" )
  7. }
  8. }
  9.  
  10. # If you don't have any subdomains you want to be web-accessible, comment out
  11. # the above and uncomment the following:
  12. #$HTTP["host"] =~ "^(.+\.)example1\.com" {
  13. # url.redirect = ( "^/(.*)" => "http://example1.com/$1" )
  14. #}
  15.  
  16. $HTTP["host"] == "example1.com" {
  17. index-file.names = ( "index.php" )
  18.  
  19. url.rewrite += ( "^/frontpage$" => "/" )
  20. # A couple examples of rewrite rules
  21. # url.rewrite += ( "^/story/[0-9]{4}/[0-9]{2}/[0-9]{2}/(.*)$" => "/blog/$1" )
  22. # url.rewrite += ( "^/archive/([0-9]{4})([0-9]{2})$" => "/archive/$1/$2" )
  23.  
  24. # Change /etc/lighttpd to the path of the drupal.lua file
  25. magnet.attract-physical-path-to = ( "/etc/lighttpd/drupal.lua" )
  26. }

The naming convention of the file, 20-example1-com.conf, may need a little explaining. Actual module configurations have a priority of 10; you want your site's configuration to load after mod_magnet has loaded, so giving it a priority of 20 will have it load after all of the default modules you have enabled (but most importantly mod_magnet). Also, periods (.) are not allowed in the filenames of configuration files, so use a dash instead between the domain name and top level domain. Next, enable the module and reload lighttpd:

  1. ~# lighty-enabled-mod example1-com
  2. ~# /etc/init.d/lighttpd reload

And there you have it: a Drupal multisite installation served up by lighttpd, with clean URLs fully functional (remember to enable them from the Drupal administration page). Install modules in /var/www/drupal-sites/all/modules; or put the following shell script in /usr/local/bin and make it executable (chmod a+x; this and the next script are available for download at the bottom of this post):

  1. #! /bin/sh
  2.  
  3. # /usr/local/bin/drupal-install-mod
  4. # Downloads and extracts a Drupal module to the Drupal sites/all/modules
  5. # directory.
  6.  
  7. # Copyright (C)2008 Jesse B. Hannah <jesse@jbhannah.net>
  8. # Available under the GNU General Public License
  9. # <http://www.gnu.org/licenses/gpl.html>. Taken from
  10. # <http://pub.jbhannah.net/scripts/drupal-install-mod>.
  11.  
  12. # Must run as root or sudo!
  13. if [ "$(id -u)" != "0" ]; then
  14. echo "Must be run as root!"
  15. exit 1
  16. fi
  17.  
  18.  
  19. if [ "$1" == "" ]; then
  20. echo "Usage: drupal-install-mod NAME-N.n-VERSION"
  21. exit 1
  22. fi
  23.  
  24. cd /var/www/drupal-sites/all/modules
  25. wget http://ftp.drupal.org/files/projects/$1.tar.gz
  26. tar xzf *.gz
  27. rm *.gz

and then, to install (for example) version 6.x-1.0-alpha2 of Image.module (Drupal project name "image"; run as root or sudo):

  1. ~# drupal-install-mod image-6.x-1.0-alpha2

then go to the Drupal modules administration page and enable the newly-downloaded module.

Now, say you've had this site running nicely for a while, then a new version of Drupal comes out. Luckily, thanks to the filesystem layout that keeps all of your site-specific configuration out of the Drupal codebase directory, you don't have to manually back everything up elsewhere; simply download the new version of Drupal, give it the symlinks to the sites directory and ImageMagick include, update the /var/www/drupal symlink to point to the new version, and delete the old version. Or, save the following script as /usr/local/bin/upgrade-drupal and make it executable:

  1. #! /bin/sh
  2.  
  3. # /usr/local/bin/upgrade-drupal
  4. # Upgrades from one Drupal version to a newer one.
  5. # Copyright (C)2008 Jesse B. Hannah <jesse@jbhannah.net>
  6. # Available under the GNU General Public License
  7. # <http://www.gnu.org/licenses/gpl.html>. Taken from
  8. # <http://pub.jbhannah.net/scripts/upgrade-drupal>.
  9.  
  10. # Must run as root or sudo!
  11. if [ "$(id -u)" != "0" ]; then
  12. echo "Must be run as root!"
  13. exit 1
  14. fi
  15.  
  16. # SAFETY! BACK UP /var/www BEFORE BEGINNING!
  17. cd /var
  18. tar cjf www-`date +%Y%m%d`-`date +%H%M`.tar.bz2 www
  19.  
  20. # First argument: old version; second argument: new version (both X.x)
  21. OLD=$1
  22. NEW=$2
  23.  
  24. # Get and unpackage the new version and get rid of the package
  25. cd /var/www
  26. wget http://ftp.osuosl.org/pub/drupal/files/projects/drupal-${NEW}.tar.gz
  27. tar xzf drupal-${NEW}.tar.gz
  28. rm drupal-${NEW}.tar.gz
  29.  
  30. # symlink to sites directory
  31. cd drupal-${NEW}
  32. rm -rf sites
  33. ln -s /var/www/drupal-sites sites
  34.  
  35. # symlink to ImageMagick
  36. cd includes
  37. ln -s /var/www/drupal-sites/all/modules/image/image.imagemagick.inc
  38.  
  39. # Update /var/www/drupal symlink and remove old folder
  40. cd /var/www
  41. rm drupal
  42. ln -s drupal-${NEW} drupal
  43. rm -rf drupal-${OLD}

Modify the above for any differences in your setup (comment out the ImageMagick section, for example), then to upgrade (for example) from 6.2 to 6.3:

  1. ~# upgrade-drupal 6.2 6.3

then visit update.php for each domain using Drupal in your web browser to update the database schema. If everything borks spectacularly after the upgrade, delete the /var/www directory and decompress the bzipped tar file created by the upgrade script in /var to restore your file tree to its prior state.

And there you have it. That's pretty much a full rundown of how my server is set up; leave a comment if these instructions work for you, or if you try it and encounter any issues, or if you get it working on another platform besides Ubuntu and want to share what you did differently. drupal.lua script and basic clean URL setup instructions taken from http://www.morphir.com/Lighttpd-Install-and-configuration-for-Drupal-wit.... All scripts indicated as such are copyright ©2008 Jesse B. Hannah, and are available with no warranty or guarantee for fitness for a particular purpose under the terms of the GNU General Public License version 3 or later; also available for download at http://pub.jbhannah.net/scripts.

AttachmentSize
/usr/local/bin/drupal-install-mod (remove trailing .)654 bytes
/usr/local/bin/upgrade-drupal (remove trailing .)1.12 KB
/etc/lighttpd/drupal.lua1.27 KB

Comments

Post new comment

  • Web page addresses and e-mail addresses turn into links automatically.
CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Copy the characters (respecting upper/lower case) from the image.