So I recently went through a very frustrating experience that involved a hard drive failure on a reseller hosting account that we have. The account houses many domains for us as we don’t yet have the need for a dedicated server, all our sites tick away nicely so we like to try and keep costs down as low as possible without compromising on quality of service. Our hosting provider is very reliable and although I rate their support very highly, fast response times and always extremely helpful, I briefly questioned that belief and whether we should keep our business with them when disaster struck.
cPanel / WHM Backups + Server Hard Drive Failure = Developer Disaster
Now we all know that hardware can fail, usually not at the most convenient times, so we can forgive the odd hiccup here and there. But on this occasion there was a hard drive failure that pushed the server offline and unfortunately also was extensive enough that the data was unrecoverable. For the majority of our clients this should not have posed much of a problem as they have largely static sites, they would have suffered some downtime on their websites and not have any access to their POP email. Those with dynamic sites and those that rely on IMAP or Webmail were not so lucky. You might be thinking that all should be OK with the power of the server backup, but unfortunately due to the timing of the failure and what looks like a spacious backup routine, about 2 days worth of code, mail, and sql data had gone astray!
So that leads me on to the point of this post. Although your hosting provider may state that your data is backed up, unless you run a dedicated server, you have no control over the schedule of the backup routine. Well, that is the case unless you take matters into your own hands.
I wanted to control what data we had backed up and when the backup was created, allowing us control over how old a backup can be. Minimising the data loss is key to keeping our clients happy and making sure we don’t waste time having to rewrite code that gets fried along with a hard drive! I have put together a couple of PHP scripts that can be run via cron, one to backup a list of sites to a remote server and the other runs on the remote server cleaning up out of date backups. I have kept them simple so that with very little effort they can be dropped in place and up and running without having to setup databases or fiddle around with other configs to get them working.
The first script is what backs up the site, credit to Mikehiggenbottom.com for the code this is based on. This file sits outside of the public_html, i.e. /home/someusername/backup_scripts/backup.php
<?php
ob_start();
// Automatic cPanel FTP backup
// This script contains passwords. Do not put it in a public folder.
// idea originally conceived by http://mikehigginbottom.com/content/automatic-cpanel-backups
// modified to run from cron and only backup each site after X hours.
// I did this to account for some sites we host that heavily use IMAP mail or update their content
// often, sometimes many times a day and may need a more rigorous backup schedule.
/*
domain to backup => domain name without www
backupdelay => number of hours in between backups
cpaneluser => username to login to cpanel for the domain
cpanelpass => password to login to cpanel for the domain
cpanelskin => the name of the current graphical frontend to cpanel for that domain. This can be worked out from your cpanel URL when you log in e.g. http://mycompany.com:2082/frontend/x3/index.html
*/
$sites[] = array('domaintobackup'=>'domain1.com','backupdelay'=>'12','cpaneluser'=>'username','cpanelpass'=>'password','cpanelskin'=>'x3');
$sites[] = array('domaintobackup'=>'domain2.com','backupdelay'=>'36','cpaneluser'=>'username','cpanelpass'=>'password','cpanelskin'=>'x3');
/* Remote Host Details */
// FTP host details
$ftpmode = "ftp";
// "ftp" for active,
// "passiveftp" for passive,
// "scp" for scp - most secure
$ftpuser = "remoteuser";
$ftppass = "remotepass";
$ftphost = "remoteftphost.com"; // Full hostname or IP address for FTP host
$ftpport = "21";
$ftpfold = "/backup_files/"; // Destination folder for backup files
$notifyemail = "emailto@notify.com";
$sendfromcpanel = 0;
$sendfromcron = 1;
$headers = 'From: YourCompany Backups <backups@yourcompany.com>' . "\r\n" .
'Reply-To: backups@yourcompany.com' . "\r\n" .
'X-Mailer: PHP/' . phpversion();$secure = 0; // Set to 1 for SSL (requires SSL support)
$debug = 0; // Set to 1 to have web page result appear in your cron log
$docRoot = "/home/yourusername/";
foreach ($sites as $site) {
$files = array();
$ftpconn = ftp_connect($ftphost);
if (@ftp_login($ftpconn, $ftpuser, $ftppass)) {
ftp_pasv($ftpconn, true);
// check backup directory exists, if not, create it then traverse into it
if (!@ftp_chdir($ftpconn,$ftpfold.$site['domaintobackup'])) {
ftp_mkdir($ftpconn,$ftpfold.$site['domaintobackup']);
ftp_chdir($ftpconn,$ftpfold.$site['domaintobackup']);
}
// list all files / directories within directory
$ftpfiles = ftp_nlist($ftpconn, '.');
foreach ($ftpfiles as $file) {
// get the last modified time of each file
$ftime = ftp_mdtm($ftpconn, $file);
if ($ftime > 0) { // directories come back as -1
$files[$file] = $ftime;
}
}
arsort($files);
reset($files);
// get the newest modified file
$firstfile = key($files);
$firstfiletime = current($files);
ftp_close($ftpconn);
} else {
echo "Ftp connection failed.... Bailing out!\n";
$output = ob_get_contents();
mail($notifyemail, "Server Backup Routine", $output, $headers);
}
// has X hours passed since the timestamp of the last backup file
if ($firstfiletime < strtotime("-".$site['backupdelay']." hours")) {
if ($secure) {
$url = "ssl://".$site['domaintobackup'];
$port = 2083;
} else {
$url = $site['domaintobackup'];
$port = 2082;
}
$socket = fsockopen($url,$port);
if (!$socket) {
echo "Failed to open socket connection… Bailing out!\n";
exit;
}
$authstr = $site['cpaneluser'] . ":" . $site['cpanelpass'];
$pass = base64_encode($authstr);
$params = "dest=$ftpmode".($sendfromcpanel ? "&email_radio=$sendfromcpanel&email=$notifyemail" : "").
"&server=$ftphost&user=$ftpuser&pass=$ftppass&port=$ftpport".
"&rdir=$ftpfold{$site['domaintobackup']}&submit=Generate Backup";
fputs($socket,"POST /frontend/".$site['cpanelskin'].
"/backup/dofullbackup.html?".$params." HTTP/1.0\r\n");
fputs($socket,"Host: {$site['domaintobackup']}\r\n");
fputs($socket,"Authorization: Basic $pass\r\n");
fputs($socket,"Connection: Close\r\n");
fputs($socket,"\r\n");
while (!feof($socket)) {
$response = fgets($socket,4096);
if ($debug) {
echo $response . "\n";
}
}
echo $site['domaintobackup']." complete.\n";
$output = ob_get_contents();
mail($notifyemail, "Server Backup Routine", $output, $headers);
fclose($socket);
exit;
} else {
echo "The site, ".$site['domaintobackup']." was last backed up at ".strftime("%H:%M %a %e %b %Y", $firstfiletime).". The earliest it will be backed up will be ".strftime("%H:%M %a %e %b %Y", strtotime("+".$site['backupdelay']." hours", $firstfiletime)).".... Bailing out!\n";
}
}
ob_end_clean();
?>
I have set this to run every 5 minutes via cron,
*/5 * * * * php /home/yourusername/backup_scripts/backup.php >/dev/null 2>&1
It uses cPanels built in backup routine, triggered by a form post, to generate a full backup and send it via ftp to a remote server. This is a native feature built into cpanel that you can do manually, but by automating the form posts through the php functions fsockopen() and fputs(), we can simulate logging into cpanel, posting the form complete with ftp details of where to send the generated backup file. The ftp login credentials used allow login into the /home/username/ folder, above the public_html folder meaning that backup files cannot be directly accessed via a browser. If you want this functionality, check your ftp login credentials default/initial directory.
The script will check for the existence of the backup folder on the remote filesystem and create it should it not exist. It also checks to see if a previous backup has been made and if one has been made within the last X hours then no new backup will be performed. It checks this based on the timestamp of the last time the file was modified.
The second script deals with the cleanup of the remote filesystem. It checks to see if there are multiple backup files from the same domain and removes them leaving only the most recent backup file. This script is run on the remote host again using cron.
<?php
// This is the companion script to the backup routine. This has to run locally to where the backups are stored. It is advisable to save this file
// outside your public directory and run it by cron.
$notifyemail = "emailto@notify.com";
$sendemail = true;
$headers = 'From: YourCompany Backups Cleanup <backups@yourcompany.com>' . "\r\n" .
'Reply-To: backups@yourcompany.com' . "\r\n" .
'X-Mailer: PHP/' . phpversion();
$docRoot = "/home/remoteusername/backup_files/";
if (is_dir($docRoot)) {
$files = array();
$dp = opendir($docRoot);
while (false !== ($dir = readdir($dp))) {
if (is_dir($docRoot.$dir) && $dir != "." && $dir != "..") {
$fdp = opendir($docRoot.$dir);
$dirfiles = array();
while (false !== ($file = readdir($fdp))) {
if (!is_dir($docRoot.$dir.'/'.$file)) {
$ftime = filemtime($docRoot.$dir.'/'.$file);
$dirfiles[$ftime] = array(
'name' => $file,
'fpath' => $docRoot.$dir.'/'.$file,
'size' => filesize($docRoot.$dir.'/'.$file),
'mod' => $ftime,
'status' => 'OK',
);
}
}
arsort($dirfiles);
if (sizeof($dirfiles) > 1) {
reset($dirfiles);
next($dirfiles);
$currdirfile = key($dirfiles);
$dirfile = current($dirfiles);
if (file_exists($dirfile['fpath'])) {
unlink($dirfile['fpath']);
$dirfiles[$currdirfile]['status'] = "Deleted";
}
}
$files[] = array(
'name' => $dir,
'contents' => $dirfiles,
);
}
}
if ($sendemail) {
$mail = "Backup Folders Cleanup\n".
"---------------------------\n\n";
foreach ($files as $dvalue) {
$mail .= $dvalue['name']."\n"."=====================================\n";
foreach ($dvalue['contents'] as $fvalue) {
$mail .= "\t".$fvalue['name']." => ".$fvalue['status']."\n";
}
$mail .= "\n\n";
}
mail($notifyemail, "Backup Folders Cleanup", $mail, $headers);
}
}
?>
This file is set to run every hour via cron;
20 * * * * php /home/remoteusername/backup_scripts/backup.php >/dev/null 2>&1
So there you have it, I hope that these snippets of code are of use to somebody else, my only regret is that I did not have something like this in place before.