Building a WordPress Cloud, Cluster Setup

This how-to will explain how to build a WordPress site on 2, 3, or more Rackspace cloud servers, with full load-balancing and redundancy.

To accomplish this, you will setup multiple web-servers and one or more mySQL servers, behind two Rackspace cloud load-balancers. One load-balancer will server all you normal user internet traffic from all the web-servers.  The other load-balancer will server only your secured traffic to your admin sites (Dashboard) from a single, master, server.  You will then set WordPress to only server the admins sites threw a secure connections, this way all uploads will be saved to a single server and may be distributed from there. This also insures that you will be able to see the newly uploaded file, even before it has a chance to propagate to the other servers. The flaw with this configuration is that if the Master server goes down, no posts may be created until the issue is resolved.

The Setup

There are meny different ways we can accomplish this.  Here I am going to show a two server setup, but you can easily expand this into as many servers as you wish.

  • Server1 – Master Database Server, Master Web Server
  • Server2 – Slave Database Server, Slave Web Server or only Slave Web Server

Building Server1

Server1 is our main server, if Server2+went down, the site would still be fully up, just slower.

This How-To assumes your using Fedora hosts for these setup’s.  To start out, we need Apache, php, mySQL installed on the server.

WordPress Auto Backup Script

A quick backup script for WordPress. If you pass a config file containing the block of variables, the script will auto run for your install of WordPress.

# /bin/bash
BKNAME=
DIR=
DBHOST=
DBNAME=
DBUSER=
DBPASS=

# Backup Varibles
BKDIR=

# Restore Variables
RSDIR=
RSARDIR=
RSUSER=
RSGROUP=

OFFSITE=

# Offsite Rsync
RSYNCHOST=
RSYNCUSERNAME=
RSYNCPASSWORD=

# Offiste SCP
SCPUSER=
SCPHOST=
SCPDIR=

if [ `date +%e` =  1 ]; then
 DAY=`date +%b`
 DATE=$DAY.`date +%Y-%m-%d`
else
 DAY=`date +%w`
 DATE=$DAY.`date +%Y-%m-%d`
fi

if [ -e $1 ]; then
 source $1
 CONFIGSTATUS="Using config file found at $1"
else
 echo "No Config file found"
 exit
fi

if [ ! -e $DB_HOST ]; then
  DBHOST=`grep "DB_HOST" $DIR/wp-config.php |sed "s/define('DB_HOST', '//" |sed "s/');//"`
  DBNAME=`grep "DB_NAME" $DIR/wp-config.php |sed "s/define('DB_NAME', '//" |sed "s/');//"`
  DBUSER=`grep "DB_USER" $DIR/wp-config.php |sed "s/define('DB_USER', '//" |sed "s/');//"`
  DBPASS=`grep "DB_PASS" $DIR/wp-config.php |sed "s/define('DB_PASSWORD', '//" |sed "s/');//"`
fi
BKDIRNAME=$BKNAME

if [ `date +%e` = 1 ]; then
 ARCHIVE=1
else
 if [ `date +%w` = 0 ]; then
  ARCHIVE=1
 else
  ARCHIVE=0
 fi
fi

if [ ! -e $BKDIR ];then
 mkdir -p $BKDIR
fi

chown -R root:root $DIR/
chown -R apache:apache $DIR/wp-content $DIR/wp-admin/update.php
chown apache:apache $DIR

if [ -e $DIR/sitemap.xml ]; then
  chown apache:apache $DIR/sitemap.xml
fi

if [ -e $DIR/sitemap.xml.gz ]; then
  chown apache:apache $DIR/sitemap.xml.gz
fi
case "$2" in
restore)
  rm -rf $RSARDIR/$BKNAME.$DAY.*
  rm -rf $RSDIR/$BKNAME
  rm -rf $DIR/$BKNAME.$DAY.*
  rm -rf $DIR/$DBNAME.*
  if [ ! -e $RSARDIR ]; then
   mkdir -p $RSARDIR
  fi
  export RSYNC_PASSWORD=$RSYNCPASSWORD
  rsync -rvzht --delete --stats $RSYNCUSERNAME@$RSYNCHOST::ibackup/odin/$BKNAME/$BKNAME.$DAY.* $RSARDIR/ --port=45873 >> $DIR/$BACKUPNAME.log 2>&1
  cd $RSARDIR
  md5sum -c $BKNAME.$DATE.tgz.md5 > /dev/null 2>&1
  RESTOREMD5=`echo $(($?))`
  if [ $RESTOREMD5 = 0 ]; then
   cd $RSDIR/
   cp $RSARDIR/$BKNAME.$DATE.tgz $RSDIR/$BKNAME.$DATE.tgz
   tar -xzf $RSDIR/$BKNAME.$DATE.tgz
   mv $RSDIR$DIR $RSDIR
   cd $RSDIR/$BKNAME
   md5sum -c $DBNAME.$DATE.sql.md5 > /dev/null 2>&1
   RESTORESQLMD5=`echo $(($?))`
   if [ $RESTORESQLMD5 = 0 ]; then
    cd $RSDIR
    mysql -u $DBUSER -p$DBPASS $DBNAME > $DIR/$BKNAME.$DATE.log 2>&1
  done
  mysqldump -h $DBHOST -u $DBUSER -p$DBPASS $DBNAME > $DIR/$DBNAME.$DATE.sql 2> $DIR/$BKNAME.$DATE.log
  a=$?
  mysqldump -h $DBHOST -u $DBUSER -p$DBPASS $DBNAME --xml > $DIR/$DBNAME.$DATE.xml 2> $DIR/$BKNAME.$DATE.log
  a2=$?
  cd $DIR/
  md5sum $DBNAME.$DATE.sql $DBNAME.$DATE.xml > $DBNAME.$DATE.sql.md5 2>> $DIR/$BKNAME.$DATE.log
  b=$?
  SQLERROR=`echo $(($a+$a2+$b))`
  if [ $SQLERROR != 0 ]; then
   BKNAME=$BKNAME-NOSQL
   echo "Exiting on SQL error"
   echo "DBHOST $DBHOST"
   echo "DBNAME $DBNAME"
   echo "DBUSER $DBUSER"
   echo "DBPASS $DBPASS"
   exit 1
  fi
  tar -czf $BKDIR/$BKNAME.$DATE.tgz --totals $DIR >> $DIR/$BKNAME.$DATE.log 2>&1
  c=$?
  cd $BKDIR/
  md5sum $BKNAME.$DATE.tgz > $BKNAME.$DATE.tgz.md5 2>> $DIR/$BKNAME.$DATE.log
  d=$?
  TOTAL=`ls -lh $BKDIR/$BKNAME.$DATE.tgz |awk '{ print $5 }'`
  ERROR=`echo $(($a+$a2+$b+$c+$d))`
  if [ $ERROR = 0 ]; then
   STATUS=Good
  else
   STATUS=Failed
  fi

  echo "" >> $DIR/$BKNAME.$DATE.log
  echo "Starting GPG encryption" >> $DIR/$BKNAME.$DATE.log
  for a in $GPGKEY
  do
   GPGKEYD="$GPGKEYD -r $a"
  done
  #gpg -e -r $GPGKEY $BKDIR/$BKNAME.$DATE.tgz
  echo "" >> $DIR/$BKNAME.$DATE.log
  if [ $ARCHIVE = '0' ]; then
   case "$OFFSITE" in
    rsync|RSYNC)
      export RSYNC_PASSWORD=$RSYNCPASSWORD
      rsync -rvzht --delete --stats $BKDIR/ $RSYNCUSERNAME@$RSYNCHOST::ibackup/odin/$BKDIRNAME --port=45873 --exclude=Archive/ >> $DIR/$BACKUPNAME.log 2>&1
    ;;

    scp|SCP)
      echo "" >> $DIR/$BKNAME.$DATE.log
      echo "Starting SCP Transmition" >> $DIR/$BKNAME.$DATE.log
      ssh $SCPUSER@$SCPHOST "mkdir -p $SCPDIR"
      ssh $SCPUSER@$SCPHOST "rm -f $SCPDIR/$BKNAME.$DAY.*"
      scp -q $BKDIR/$BKNAME.$DATE.tgz $SCPUSER@$SCPHOST:$SCPDIR/ 2>> $DIR/$BKNAME.$DATE.log
      scp -q $BKDIR/$BKNAME.$DATE.tgz.md5 $SCPUSER@$SCPHOST:$SCPDIR/ 2>> $DIR/$BKNAME.$DATE.log
      #ssh $SCPUSER@$SCPHOST "chmod 666 $SCPDIR/$BKNAME.$DAY.*"
      echo "" >> $DIR/$BKNAME.$DATE.log
    ;;
   esac
  fi

  echo "final error status is: $ERROR" >> $DIR/$BKNAME.$DATE.log
  sed '/tar: Removing leading */d' $DIR/$BKNAME.$DATE.log > $BKDIR/backuptmp.log
  #echo "INSERT INTO Backup_Log ( System, Backup_Job, Label, Output, Bytes, Status, Log ) VALUES ('`hostname -s`', 'Wiki', '$DIR', '$BKNAME.$DATE', '$TOTAL', '$STATUS', '`cat $BKDIR/backuptmp.log`');" |mysql -t -h localhost -u backup Status
  rm -rf $BKDIR/backuptmp.log
  rm -rf $DIR/$DBNAME.$DAY.*
  rm -rf $DIR/$BKNAME.$DAY.*
  ;;
*)
  echo "The valid commands are backup & restore"
  exit 1
  ;;
esac

WordPress Sitemap Generator

This plugin will create a sitemap for your WordPress site (http://example.com/sitemap.xml). This was setup for a multisite install, but should work fine on a single install.

First, create a file named sitemap-generator.php in your mu-plugins directory, (or your plugins directory if your using a single install WordPress setup) and past the below into it.

After create a new directory named templates and past the below in a file feed-sitemap.php in it.

WordPress Favicon Plugin

One more quick plugin setup for multi site builds of WordPress. Just drop this in your mu-plugins folder. This plugin will first look for a local favicon, if there is none, it will go to Gravatar to see if the Admin_Email as a gravator, if it doesn’t, it will use the default from the main install.  If a user must have a special favicon you may drop it in there files directory, for example blogs.dir/5/files.

Disabling Plugin Stylesheet

You must have seen that WordPress plugins can slow your site down with additional HTTP Requests such as adding their own stylesheet. For advanced users, who are adding custom styles for the announcement, you do not need to have an additional HTTP request for a useless stylesheet. Then add the following function in your theme’s functions.php file:

Adding a Twitter feed

Playing with adding a twitter feed directly into my theme.

<?php
function twitter_import() {
  require "twitter.lib.php";
  $username = "mdrude";
  $password = "<password>";
  $twitter = new Twitter($username, $password);
  $old_id = get_option('milly_twitter_old_id');
  $xml = $twitter->getUserTimeline("max_id=$old_id");
  $twitter_status = new SimpleXMLElement($xml);
  global $user_ID;

  foreach($twitter_status->status as $status){
    if ( $status->id > $old_id ) {
      echo "running";
      $tweet_text = $status->text;
      $created_at = $status->created_at;
      $tweet_date = date("Y-m-d H:i:s", strtotime($created_at));
      $tweet_id = $status->id;
      $new_post = array(
        'post_title' => $tweet_text,
        'post_content' => $tweet_text,
        'post_status' => 'publish',
        'post_date_gmt' => $tweet_date,
        'post_author' => $user_ID,
        'post_type' => 'twitter'
      );
      $post_id = wp_insert_post($new_post);
      add_post_meta($post_id, 'aktt_twitter_id', "$tweet_id");
      add_post_meta($post_id, 'twitter_id', "$tweet_id");
      update_option('milly_twitter_old_id', "$tweet_id");
    }
  }
}
add_filter('cron_schedules', 'cron_add_milly');
function cron_add_milly( $schedules )
{
        $schedules['milly'] = array(
                'interval' => 300,
                'display' => __('Every 5 Mins')
        );        return $schedules;}
if (!wp_next_scheduled('twitter_import_hook')) {
  echo "Updating schedule";
  wp_schedule_event( time(), 'milly', 'twitter_import_hook' );
}
add_action( 'twitter_import_hook', 'twitter_import' );
?>

You may also download this snip from it’s gist.

WordPress 2.9 Post Thumbnail Function

post_thumbnailWordPress 2.9 brings a new function for users of gallery’s the add_theme_support(‘post-thumbnails’) function. with this you will get a new item on your new post window named Post Thumbnail (see right).  After you have added a post Thumbnail to a post, you need to display it.  By using a “gallery” category, creating the below two files (or adding the code to files) and editing your category.php file, you may display a custom gallery page.

To start, create a new file named gallery-function.php and add the following code to it, this file should live in the root of your current theme’s directory.

gallery-function.php

<link rel="stylesheet" href="<?php bloginfo('template_url'); ?>gallery-function.css" type="text/css" media="screen" />
<div id="gallerypost-<?php the_ID(); ?>">
    <div id="gallerypost_main-<?php the_ID(); ?>">
	<div id="gallerypost_thumbnail-<?php the_ID(); ?>">
		<?php post_thumbnail(); ?>
	</div>
	<div id="gallerypost_body-<?php the_ID(); ?>">
		<?php $images =& get_children( 'post_type=attachment&post_mime_type=image' ); ?>
		<h2><a rel="bookmark" href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
		<div><small>Posted by <?php the_author_posts_link(); ?> on <?php the_time('F jS, Y') ?></small></div>
		<div>
			<?php the_excerpt(); ?>
		</div>
	</div>
    </div>
    <div id="gallerypost_sub-<?php the_ID(); ?>">
	<div id="gallerypost_sub_left-<?php the_ID(); ?>">
		<p><?php echo get_the_term_list( $post->ID, 'people', 'Who: ', ', ', '<br />' ); ?></p>
		<p><?php echo get_the_term_list( $post->ID, 'events', 'What: ', ', ', '<br />' ); ?></p>
             	<p><?php echo get_the_term_list( $post->ID, 'places', 'Where: ', ', ', '' ); ?></p>
	</div>
	<div id"gallerypost_sub_right-<?php the_ID(); ?>">
		This Album contains <?php echo $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_parent = '$post->ID' AND post_type = 'attachment'" ); ?> items.
	</div>
    </div>
</div>

Next create a file named gallery-function.css and also add it to our current theme’s root directory.
gallery-function.css

.gallerypost {
        background-color:#fff;
        display: block;
        hNeight: 300px;
        margin: 0px 0px 10px 0px;
        padding: 20px 10px 10px 10px;
        overflow: hidden;
        border-bottom: 1px solid #D2C4A2;
        }
.gallerypost_body {
        float: right;
        width: 430px;
        margin: 0 20px 0 0;
        }
.gallerypost_body p {
        margin: 50px 0px 0px 0px;
        }
.gallerypost_body .entry p {
        margin: 0px 0px 0px 0px;
        }
.gallerypost_body .entry {
        margin: 0px 0px 0px 0px;
        }
.gallerypost_main {
        width: 690px;
        }
.gallerypost_sub {
        display: block;
        padding: 210px 0px 0px 10px;
        }
.gallerypost_sub {
        display: block;
        padding: 210px 0px 0px 10px;
        }
.gallerypost_sub p {
        vertical-align: bottom;
        margin: 0;
        }
.gallerypost_sub_left {
        width: 220px;
        display: block;
        float: left;
        }
.gallerypost_sub_right {
        width: 220px;
        display: block;
        float: right;
        }
.gallerypost_thumbnail {
        float: left;
        }
.gallerypost_thumbnail img {
        margin: 0 0 0 10px;
        color: #000;
        }

And lastly you will need to add the following to your functions.php file in your current theme’s root directory (you may add it anywhere in functions.php).

add_theme_support('post-thumbnails');
set_post_thumbnail_size(200, 200);

Now that we have our functions built, we need to add some code to your category.php file.

Right after:

<?php while (have_posts()) : the_post(); ?>

Add the following code.  This will display the gallery-function.php code when a post is in the “gallery” category.

<? if ( is_category( 'gallery' )) {
                    include('gallery-index.php');
               } else { ?>

Next Before:

<?php endwhile; ?>

Add the following to close the if statement.

<?php } ?>

And that should do it, you should now see the post image on the gallery category page similar to below.

Gallery-screenshot