I have a web server with a number of clients’ websites on it. It’s necessary to backup these websites every day, since clients use a content management system to make changes regularly. These changes can be updates to a website’s MySQL database, or they can be changes to the files stored within these websites. What I’d like is to backup the MySQL database and the filesystem for each website, every day, at a specific time. The backups must rotate: when there are, say, five backups, I want the oldest one to be removed as the newest one is written. Also, I’d like the backup solution to send me an email every day after it’s completed the backups with a summary of the procedure.

So, in summary, my needs are these:

  • Define a list of websites to back up
  • For each site, backup (dump) the MySQL database
  • For each site, backup the website’s file structure
  • Send an email to one or more people with a summary of the backup process.

It’s possible to do this with a shell script (like AutoMySQLBackup does). However, AutoMySQLBackup does not backup file systems or send email. Also, shell scripting tends to be messy code, so I decided to use Ruby.

Configuration file

First off, I’d like to store the list of websites to backup in a separate configuration file so that I can edit this list easily. Also, for reusability, I’ll store database access credentials and email addresses there too. The simplest way of making a configuration file to be read by Ruby is to actually write the configuration file in Ruby, like so:

BACKUPDIR = "/backup/webserver"
ROTATE = 5
DBUSER = "root"
DBPASSWORD = "myrootpassword"
EMAILS = [ "alex@email.com", "john@email.com" ]
WEBSITES = {
 "sample.com" => {
   "path" => "/usr/local/www/apache22/data/sample.com",
   "database" => "sampledb"
 },
 "example.net" => {
   "path" => "/usr/local/www/apache22/data/example.net",
   "database" => "exampledb"
 }
}

This file stores a variable ROTATE which indicates the number of backups to keep before throwing away the oldest one. For each website, I specify the path to the files to be backed up, and the name of the MySQL database. The configuration file will be included and parsed automatically by the backup script, since it is plain Ruby code.

Backup script

The backup script begins by requiring SMTP support, so that we can send emails later. It also starts an output buffer (“output”) where we will store all messages generated by the script to be included in the email. Before starting the backup procedure, we start a begin...rescue block so that me may catch any exceptions thrown by Ruby, in order to include these in the email as well.

require "net/smtp"
output = "Webserver backup script"
begin
  # Load config file:
  require "/usr/home/alex/backup-script/config.rb"
  # Does the backup directory exist?
  if not FileTest::exists?(BACKUPDIR)
    raise "Backup directory #{BACKUPDIR} does not exist."
    exit
  end

The script now loops through the list of websites defined in the configuration file, creating a backup directory with the name of the website for each if it doesn’t already exist:

  WEBSITES.each do |name, website|

    output << "rnrnBacking up #{name}:"

    # Establish backup dir
    path = BACKUPDIR + "/" + name

    # If website dir does not exist, create it.
    if not FileTest::exists?(path)
      Dir.mkdir path
      output << "rn  Directory #{path} created."
    end

Next, the script enumerates the subdirectories that already exist in the website’s backup directory. This is because we will create a subdirectory with date backup’s date for each backup (e.g. 20110810-105535, for 10 August 2011, 10:55:35). These directories are then sorted alphabetically, so that the least recent backup of the website is first in the list.

    # Get entries inside dir with modification times (sorted first to last)
    entries = []
    Dir.entries(path).each do |entry|
      next if entry == "." or entry == ".."
      mtime = File.mtime(path + "/" + entry).to_f
      entries << [ mtime, entry ]
    end
    entries.sort! { |x,y| x[0]  y[0] }
    output << "rn  #{entries.length} backups found (max #{ROTATE-1})."

The total number of backups found is compared to the value of ROTATE. If there are too many backups, the latest one(s) (first in the list) are removed.

    # Remove least recent entries if more than ROTATE available:
    while entries.length > ROTATE - 1
      entry = entries.shift
      cmd = "rm -R -f #{path}/#{entry[1]}"
      `#{cmd}`
      output << "rn  Removed #{path}/#{entry[1]}"
    end

Having cleaned up excess backups, the script now creates a fresh folder, naming it with the current date and time:

    # Create new folder for backup:
    subdir = Time.now.strftime("%Y%m%d-%H%M%S")
    Dir.mkdir path + "/" + subdir
    output << "rn  Created directory #{path}/#{subdir}"

If a website has a database defined in the configuration file, the script now calls mysqldump to create a backup of the database inside the newly created backup subdirectory. The backup is gzipped as well. Note that a full path to mysqldump must be provided, since cron, which we will use later to run our script at specific times, does not include a path to mysqldump in the shell that it runs in.

    # Dump database (if required)
    if website.has_key? "database"
      # Dump db
      cmd = "/usr/local/bin/mysqldump -u#{DBUSER} -p#{DBPASSWORD} #{website["database"]} | gzip > #{path}/#{subdir}/#{name}.sql.gz"
      `#{cmd}`
      output << "rn  Dumped database #{website["database"]} to #{path}/#{subdir}/#{name}.sql.gz"
    end

If a website has a path to files defined in the configuration file, the script now uses tar/gzip to create a tarball of the entire website file structure, recursing into subdirectories.

    # Dump code (if required)
    if website.has_key? "path"
      # Copy code
      `cd #{website["path"]}; tar -czf #{path}/#{subdir}/#{name}.tar.gz *`
      output << "rn  Created zipped tarball of code in #{path}/#{subdir}/code"
    end
  end

This completes the loop that backs up all the websites. We now end our rescue clause in order to catch any exception thrown by Ruby during this process. The exception text is appended to the running log (output) as well as written to standard output.

rescue StandardError =&gt; error
  output &lt;&lt; "Error occurred: " + error
  puts "Error occurred: " + error
end

All that is left to do is to send the output off through email. This is easy to do (any one reason we’re using Ruby):

# Mail output:
Net::SMTP.start('127.0.0.1') do |smtp|
  output = "Subject: Webserver backup procedurern" + output
  EMAILS.each do |email|
    smtp.send_message output, "alex@email.com", email
  end
end

Adding the script to cron

We can now add the script to the system’s crontab in order to run at regular times. We’ll write a small shell script that launches the script using the bash shell, to make sure that cron has access to a powerful shell to run in:

#/usr/local/bin/bash
/usr/local/bin/ruby /usr/home/alex/backup-script/backup.rb

The following entry is added to the system crontab (/etc/crontab). This will make sure that the script runs every day at 22:00.

# Run webserver backup script
00      22      *       *       *       root    /usr/home/alex/backup-script/backup.sh