Last updated March 27, 2011. Created by conchur on May 29, 2009.
Edited by silverwing. Log in to edit this page.
Building on comments from this forum topic, I've implemented a shared code-base system which has the following features:
- keeps user data separate - no user can see the files of another user, no browser client can access information from another site
- contributed modules/themes can be shared or single-site - commonly used modules can be shared across all sites, but custom themes or unusual modules can be installed for a single site
- core updates can be applied selectively - one site can be updated to a new core version, another can wait (e.g. for a contributed module to to be upgraded) and upgrade later (or even revert completely using database snapshots). Multiple code-base versions can run concurrently, shared by an arbitrary number of sites as required.
- works with PHP user_base security - this needs php.ini or httpd.conf modification to whitelist the shared code-base directory
- prevents users modifying shared code or symlinks - all shared files and links are not owned by the site user, they have read-only access
- works with user control panels like cPanel - installs into a user's public_html or www directory (or a sub-directory)
- works with any file transfer method - users have full control of their own files, but no-one elses
Each Drupal core version is owned by a system user ('cms' in my case) and installed in a sub-directory of:
/usr/local/drupal/So for example 6.12 would be downloaded and extracted to:
/usr/local/drupal/drupal-6.12
Each site is deployed with 2 sets of symlinks:
/home/[username]/drupal_homewhich contains the reference to the version of drupal to be used:
/home/[username]/drupal_home/current -> /usr/local/drupal/drupal-6.12And the site content, which is usually in a sub-directory of the user's home directory:
/home/[username]/public_htmlSymlinks in here reference the drupal_home symlink above:
.htaccess -> ../drupal_home/current/.htaccess
cron.php -> ../drupal_home/current/cron.php
includes/ -> ../drupal_home/current/includes
index.php -> ../drupal_home/current/index.php
misc/ -> ../drupal_home/current/misc
modules -> ../drupal_home/current/modules
profiles/ -> ../drupal_home/current/profiles
scripts/ -> ../drupal_home/current/scripts
themes-> ../drupal_home/current/themes
update.php -> ../drupal_home/current/update.php
xmlrpc.php -> ../drupal_home/current/xmlrpc.phpShared contributed modules and themes are placed in the sites/all directory of the shared code-base, then linked from each site:
sites/all-> ../drupal_home/current/sites/allAll other files and sub-directories are unique to the site:
sites/default/...
sites/[sitename]/...
files/...
robots.txtSo the 'settings.php' file and 'files' directory particularly are only accessible by the site's user. If an unusual .htaccess file is required this can replace the symlink to the shared one. Themes and modules unique to one site can be placed in the sites/default directory, or the sites/[sitename] if using multiple domains with one installation.
To update this site to a new version of drupal, download the new core files and extract as above. Shared modules need to be copied from the previous version's sites/all directory, and tested to confirm compatibility using a sandpit site. This may seem like an unnecessary duplication of code, but it allows different versions of modules to be used with different version of the core. Once you're ready simply change the drupal_home link for each site:
/home/[username]/drupal_home/current -> /usr/local/drupal/drupal-6.13All the public_html linked files/sub-directories (listed above) will be instantly updated to point to the updated files. Themes and modules unique to each site are preserved across updates.
To simplify the setup of each site I've written (or modified from Geary's contributions in the forum thread) scripts to automate each stage of the process:
- drupal_install.sh - initial deployment of a new site
- drupal_update.sh - change or rebuild the links (e.g. when moving Drupal to a different sub-directory)
- drupal_link.sh - points the site to a new version
The install script produces a comprehensive starting website including initial module configuration and admin user. It does this by dumping a snapshot of a skeleton installation and using this to create a new database for the user's site. A skeleton file structure is copied to the public_html directory when which contains the basic initial layout for the site's files and sub-directories. The access privileges for the installation directory are then set to allow both the user and drupal (running under Apache as 'nobody' group) to read and write as appropriate.
The update script supports moving the drupal_home directory or the site's installation directory (e.g. promoting it from a sub-directory to the root) while maintaining or rebuilding the links. The versions changer simply updates the current version to a different specified one.
The scripts allow you to install in sub-directories of the public_html directory (or anywhere else), and optionally specify the owning user, drupal_home location, database name, database user (if different) and various other options. Full listings and instructions are detailed below.
The skeleton installation copies a directory containing the following files and sub-directories:
/usr/local/drupal/skeleton
|- public_html
| |- robots.txt
| |- logo.gif
| |- favicon.ico
| |- files
| | |- images
| | |- temp
| |- sites
| | |- default
| | |- settings.php
| |- uploads
| |- images
|- drupal_home
|- current -> /usr/local/drupal/drupal-6.12The graphics and images sub-directories are there to allow me to deploy the initial site ready-branded and with image upload support configured, but any shared files you want can be put here and will be copied to each new installation.
drupal_install.sh
#!/bin/sh
if [ $# = "0" ]; then
echo "drupal_install: usage: drupal_install username db_pwd [drupal_home] [database] [db_user]"
exit 1;
fi
#default settings
USER=$1
DB_USER=$USER'_'public
DB_PWD=$2
DB=$USER'_'drupal
DIR=../drupal_home
if [ -n "$3" ]; then
DIR=$3
fi
if [ -n "$4" ]; then
DB=$USER'_'$4
fi
if [ -n "$5" ]; then
DB_USER=$USER'_'$5
fi
echo
echo Drupal installation script for $USER
check() {
test -e $1 && {
echo
echo "$PWD/$1 exists, Drupal installation canceled"
echo
exit 1
}
}
backup() {
test -e $1 && {
echo
echo "$PWD/$1 exists, removing."
echo
mv $1 $1.bak
}
}
backup index.html # not part of Drupal, but suspicious
backup .htaccess
check index.php
check cron.php
check index.php
check xmlrpc.php
check database
check includes
check misc
check modules
check scripts
check themes
echo
echo "Installing Drupal in $PWD"
echo Will create a new database $DB accessed by $DB_USER using password $DB_PWD
echo
echo "Type OK to install:"
read ok
case $ok in
[Oo][Kk] )
echo
echo "Installing now..."
echo
;;
* )
echo
echo "Installation canceled"
echo
exit 2
;;
esac
echo Creating database and MySQL user...
echo "CREATE DATABASE IF NOT EXISTS $DB;" > /usr/local/drupal/scripts/mysql.tmp
echo "GRANT ALL PRIVILEGES ON $DB.* TO '$DB_USER' IDENTIFIED BY '$DB_PWD';" >> /usr/local/drupal/scripts/mysql.tmp
mysql < /usr/local/drupal/scripts/mysql.tmp
rm -f /usr/local/drupal/scripts/mysql.tmp
echo Populating database with initial settings...
mysqldump cms_drupal > /usr/local/drupal/scripts/cms_content.sql
mysql $DB < /usr/local/drupal/scripts/cms_content.sql
echo Checking drupal_home in $DIR...
test ! -d $DIR && {
echo Creating drupal_home in $DIR...
mkdir $DIR
}
cp -rp /usr/local/drupal/skeleton/drupal_home/* $DIR
echo Copying skeleton file structure to $PWD...
cp -rp /usr/local/drupal/skeleton/public_html/* .
chown -R $USER:nobody .
sed -i s#username:password@localhost/databasename#$DB_USER:$DB_PWD@localhost/$DB# sites/default/settings.php
cp -p sites/default/settings.php $DIR/settings.php
echo Creating code-base symlinks in $PWD to $DIR...
ln -sf $DIR/current/.htaccess .htaccess
ln -sf $DIR/current/xmlrpc.php xmlrpc.php
ln -sf $DIR/current/cron.php cron.php
ln -sf $DIR/current/index.php index.php
ln -sf $DIR/current/update.php update.php
ln -sf $DIR/current/includes includes
ln -sf $DIR/current/misc misc
ln -sf $DIR/current/modules modules
ln -sf $DIR/current/profiles profiles
ln -sf $DIR/current/scripts scripts
ln -sf $DIR/current/themes themes
ln -s ../$DIR/current/sites/all sites/all
echo Done! Log in now to set site information and theme, create users etc.
echo Remember to add a cron job to call cron.php too!
exit 0;drupal_update.sh
#!/bin/sh
if [ $# = "0" ]; then
echo "drupal_update: usage: drupal_update username drupal_home"
exit 1;
fi
#default settings
USER=$1
DIR=../drupal_home
if [ -n "$2" ]; then
DIR=$2
fi
echo
echo "Drupal update script for $USER"
echo "creating Drupal code-base links in $DIR"
echo "And over-writing Drupal in $PWD"
echo
echo "Type OK to proceed:"
read ok
case $ok in
[Oo][Kk] )
echo
echo "Updating now..."
echo
;;
* )
echo
echo "Update canceled"
echo
exit 2
;;
esac
backup() {
test -e $1 && {
echo
echo "$PWD/$1 exists, renaming."
echo
mv $1 $1.bak
}
}
backup $DIR
cp -rp /usr/local/drupal/skeleton/drupal_home $DIR
DB=$USER'_'drupal
DUMPTO=$DIR/$USER'_'drupal.sql
mysqldump $DB > $DUMPTO
chown -R $USER:nobody $DIR
cp -p sites/default/settings.php $DIR
ln -sf $DIR/current/.htaccess .htaccess
ln -sf $DIR/current/xmlrpc.php xmlrpc.php
ln -sf $DIR/current/cron.php cron.php
ln -sf $DIR/current/index.php index.php
ln -sf $DIR/current/update.php update.php
rm -f includes misc modules profiles scripts themes
ln -sf $DIR/current/includes includes
ln -sf $DIR/current/misc misc
ln -sf $DIR/current/modules modules
ln -sf $DIR/current/profiles profiles
ln -sf $DIR/current/scripts scripts
ln -sf $DIR/current/themes themes
rm -f sites/all
ln -s ../$DIR/current/sites/all sites/alldrupal_link.sh
#/bin/bash
if [ $# = "0" ]; then
echo "link_update: usage: link_update drupal_version [sub-directory=current]"
exit 1;
fi
OLD=current
NEW=/usr/local/drupal/drupal-$1
if [ -n "$2" ]; then
OLD=$2
fi
echo
echo "Drupal directory update script"
echo "Changing Drupal code-base link to $NEW"
echo "And over-writing $OLD in $PWD/$OLD"
echo
echo "Type OK to proceed:"
read ok
case $ok in
[Oo][Kk] )
echo
echo "Updating now..."
echo
;;
* )
echo
echo "Update canceled"
echo
exit 2
;;
esac
echo "Current link:"
ls -l $OLD
rm -f $OLD
ln -s $NEW $OLD
echo "New link:"
ls -l $OLDThis has been tested and tweaked for several months on a server hosting over 20 Drupal sites, upgrading each site from 6.10 > 6.11 > 6.12 without serious issues.
Corrections and suggestions gratefully received!
Edit 2009-11-08: Thanks to HonorsGrad (http://drupal.org/user/300598) for pointing out that I left settings.php out of the skeleton directory listing. It's added now.
Comments
Thank you for your research
Thank you for your research on this, I've been looking for something like this for awhile. A few questions if you have the time:
1) is this easy to apply to an existing install, or does it need to be a fresh install? Reading it seems to indicate that the update shell script would do it, but I'm nervous about messing up my site.
2) sorry if this sounds obtuse if already described above, but how is a new "sibling" site created once the database is ready?
I can probably just experiment myself, but any advice that would save me a few hours of figuring it out as well as eliminating potention for accidentally leaving a large security hole by my own blundering would be appreciated.
Thanks.
Jeff
You really need a clean
You really need a clean install to do this, since the drupal files you are using only exist in the shared installation. It is relatively simple to test this method first by taking a dump of the current database, renaming the current working directory and then creating a new directory in its place to run the scripts on. The new shared install would then be reached at the same url, and if you can't get it to work just delete the folder and rename the old one back to the original name.
I'm not sure what you mean by the second question. The new site and its database are both created in the install script, using a database dump from an already working install (the template installation, which should be the first site you make).
HTH.
Doesn't work with 6.14 and Fedora 10
I am trying to set up a server using this technique with drupal-6.14 and Fedora 10. I am not using the data base dump/load to set it up as I don't have a working setup to dump from. When I browse to the host, the installation dialog comes up. I enter the data base name and user credentials and click save and continue. The system trundles for a second and returns the empty installation form. No error messages any where.
I'm enough of a drupal novice to really not have a clue as to how to begin trouble shooting this. Has anyone had similar issues and found a solution?
Update: I figured this out after I posted this. It seems that, at least, in 6.14 you need a copy of the original settings.php called default.settings.php in sites/default. If it's not there and read only, you get the symptom I described above.
-Smitty
No install required
Try doing a normal Drupal installation first, then using the resulting database as the template which will be used as the starting point for each new site. This will save you a lot of time in deploying each new site, and avoids any potential issues with the install script. After running the install script above the new site will be installed and ready to configure - the install.php script is not required, so it is not included in the linked files.
php includes
This bridge thingie is a nifty trick. Thanks.
I've done a similar setup before.
Justin Hileman there said:
Instead he says to make local versions of index.php, update.php etc and put in a redirect and include to the original. Like so:
<?phpchdir('/path/to/drupal/');
include('./index.php');
?>
I don't know why I wouldn't want this. Both aproaches seem to work. Any idea?
...of je stopt de stekker erin.
Avoid multiple edits/keep in sync
The main reason is to avoid having to edit multiple files when you do an upgrade. With the symlink option, you run the link or upgrade script to change the link to the new Drupal core which is stored in just one location, and no further changes are necessary. With the PHP redirect method you would need to manually edit each PHP redirect to change to the new Drupal core directory, or modify the scripts to edit these files for you which would add unnecessary complexity.
The symlink option also avoid the possibility of having incorrect links or different versions of Drupal being referenced (i.e. updating some but not all to a new version). Either they all work or none work!
HTH.
Actually...
I think Justin's kludge just isn't necessary in your case because you're symlinking over the entire includes directory. In his approach he tries to keep as little available from the linked root directory for various security reasons -- as a result, if you were just to symlink over to any of the php files rather than changing the directory, you wouldn't be able to include anything from the includes directory because ../includes wouldn't exist. In your setup that isn't necessary because you're providing a link to the includes directory in your users simulated drupal root directory.
The change directory approach was intended for isolating all server side resources from public accessibility (currently the only thing blocking somebody from seeing the contents of an inc file by going to httq://yoursite/includes/actions.inc is the .htacess file).
That isn't something you're trying to accomplish here so it's unnecessary, and might even break off access to the users serverside code.
Thank you and a note on 'No input file specified' issue
Also a warm thank you for the contributor of this research! I've also been looking for something like this for awhile.
A note from me for those running into the issue 'No input file specified' like I did.
If your shared code base (e.g.
/usr/local/drupal/drupal-6.12) isn't in PHP's open_basedir your installation won't work. You only see this message on a white screen and no Drupal screen or Drupal installation page will appear.I didn't test the scripts so far.
Tested with Drupal 7.8.
Drupal 7
Solution for Drupal 7?
This looks great but I was just wondering if this will work with Drupal 7 without modification. Any thoughts on this?