How To Back Up Forge WordPress Sites to Amazon S3

I’ve had my personal WordPress site and all of my clients’ sites hosted with Laravel Forge for a little over a year now. If you haven’t heard of Forge yet, you definitely need to check it out. Yes, it is mainly for PHP applications, but it will also host WordPress sites like a champ. Forge is great, but the one thing it lacks is a way to back up your sites.

I wrote a script to handle backing up each WordPress site to Amazon S3 for safe-keeping as a part of my own backup plan.

Directory Structure

Before walking through the script, I want to take a moment to outline the directory structure. I’ve created a backups folder where all of this lives.

backups
|--- .env
|--- backup.sh
|--- client-sites/
|--- logs/
|    |--- 09-2018/
|    |--- 10-2018/
|--- sites

Within the backups directory, there’s an .env I added so the mysql\_user and password are not embedded directly in the script.

The client-sites/ folder is where the script does all of the file work — things like copying current site files and compressing them happen there.

The logs/ folder is just a place to store all of the logs for the backup processes.

Finally, the sites file which lists out all the databases and domains of the sites that are hosted on the server. Don’t get hung up on the fact that it’s using the domains. By default, Forge creates each site in its own home directory named after the domain of the site.

Here is what the sites file could look like:

# domain_name site_root_folder_name
trecord tannerrecord.com
site1 my-crazy-awesome-site.net
site2 site2.io
view raw sites hosted with ❤ by GitHub

Here’s the entire script:

#!/bin/bash
do_backup() {
DATABASE_NAME=$1
SITE_FILES_DIR=$2
SITE_FILES_FILENAME="${SITE_FILES_DIR}-`date +%Y_%m_%d`.tar.gz"
FILE_NAME="${DATABASE_NAME}.sql.gz"
SAVE_DIR="client-sites"
S3BUCKET="my-backup-bucket"
printf "==============================================\n";
printf " Starting Backup - ${2}\n";
printf "==============================================\n";
printf "Copying the site files into ~/backups/client-sites...\n"
cp -R ~/${SITE_FILES_DIR} ${SAVE_DIR}/${SITE_FILES_DIR}
# Get MYSQL_USER and MYSQL_PASSWORD
source .env
# Dump the site SQL into site files copy
printf "Dumping MySQL database into the site files directory...\n"
mysqldump -u ${MYSQL_USER} -p${MYSQL_PASSWORD} ${DATABASE_NAME} | gzip > ${SAVE_DIR}/${SITE_FILES_DIR}/${FILE_NAME}
# Compress the directory
printf "Compressing the site files...\n"
tar -zcf ${SAVE_DIR}/${SITE_FILES_FILENAME} -C ${SAVE_DIR}/${SITE_FILES_DIR} .
#remove the uncompressed site files
printf "Removing the uncompressed site files...\n"
rm -r ${SAVE_DIR}/${SITE_FILES_DIR}
if [ -e ${SAVE_DIR}/${SITE_FILES_FILENAME} ]; then
# Upload to AWS
aws s3 cp ${SAVE_DIR}/${SITE_FILES_FILENAME} "s3://${S3BUCKET}/client-sites/`date +%Y_%m_%d`/${SITE_FILES_FILENAME}" --no-progress
# Test result of last command run
if [ "$?" -ne "0" ]; then
echo "Upload to AWS failed"
else
# If success, remove backup file
rm ${SAVE_DIR}/${SITE_FILES_FILENAME}
fi
else
echo "Backup file not created"
fi
}
{
for file in sites
do
IFS=$'\n'
for line in $(cat $file)
do
IFS=' '
do_backup $line
done
done
} 2>&1 | tee "logs/$(date +%Y_%m_%d).log"
view raw backup.sh hosted with ❤ by GitHub

Walking Through The Script

The whole first part of this script is the actual do_backup() function which handles the backup process. If you skip down towards the bottom of the file, you’ll see a couple of loops:

for file in sites
do
IFS=$'\n'
for line in $(cat $file)
do
IFS=' '
do_backup $line
done
done

This section of the script is responsible for actually kicking off the backup function for each line in the sites file. We loop through the file and do an action for each line. While we’re working with that line we change the [Internal Field Separator] to break up the line by spaces into two parts.

The next line then kicks off the backup function passing the two parts as parameters. The first one passed ($1) will be the database name for the site and the second ($2) will be the home directory where the site lives.

Here’s an example of what that line would look like if we could step through the execution of the loop:

$ do_backup trecord staging.tannerrecord.com

The backup function looks like it does a lot, but really only does 3 things:

  • Copy the current site files into client-sites/
  • Compress the copied site files into something like staging.tannerrecord.com_2018_10_30.tar.gz
  • Upload the compressed file to Amazon S3

The beginning of the function is just defining some variables and outputting a nice header to the log file.

DATABASE_NAME=$1
SITE_FILES_DIR=$2
SITE_FILES_FILENAME="${SITE_FILES_DIR}-`date +%Y_%m_%d`.tar.gz"
FILE_NAME="${DATABASE_NAME}.sql.gz"
SAVE_DIR="client-sites"
S3BUCKET="my-backup-bucket"
printf "==============================================\n";
printf " Starting Backup - ${SITE_FILES_DIR}\n";
printf "==============================================\n";

Next, it copies the current site files into the client-sites/ folder so it’s not working directly on the actual live site files.

printf "Copying the site files into client-sites/...\n"
cp -R ~/${SITE_FILES_DIR} ${SAVE_DIR}/${SITE_FILES_DIR}

Next, it dumps the mysql database into the copy of the site files.

# Get MYSQL_USER and MYSQL_PASSWORD
source .env
# Dump the site SQL into site files copy
printf "Dumping MySQL database into the site files directory...\n"
mysqldump -u ${MYSQL_USER} -p${MYSQL_PASSWORD} ${DATABASE_NAME} | gzip > ${SAVE_DIR}/${SITE_FILES_DIR}/${FILE_NAME}

Compresses the file:

# Compress the directory
printf "Compressing the site files...\n"
tar -zcf ${SAVE_DIR}/${SITE_FILES_FILENAME} -C ${SAVE_DIR}/${SITE_FILES_DIR} .

When uploading to S3, you will need to have awscli installed on the server for this script to work. You can install it with pip through ssh with: $ pip install awscli

# Upload to AWS
aws s3 cp ${SAVE_DIR}/${SITE_FILES_FILENAME} "s3://${S3BUCKET}/client-sites/`date +%Y_%m_%d`/${SITE_FILES_FILENAME}" --no-progress

The --no-progress parameter just suppresses some of the extra information that shouldn’t be included in the log file.

In conclusion, it only took a few hours to develop this backup script and most of that time was actually debugging some issues with an old version of awscli.

Before I wrote this script, I was paying for a backup service that was $50 a month (and that only includes 5GB of storage). When all of my sites back up, they end up being about 2GB total and I’m going to keep 30 days of backups. When you look at the pricing of S3 ($0.023 per GB), which amounts to $1.38/mo, it’s not very hard to see why I wrote this script. I hope that it helps you get a better understanding of bash scripts and the power of doing some things yourself.