Earlier, I used a WordPress plugin to do periodic backups of the sites I’m hosting, but I came to a conclusion that it would be more reliable, flexible and fun to do the backups via a system script instead, sending the resulting backup archives to a remote server. Here’s how I ended up doing it:
First, I wanted to create a separate mariadb/mysql user with only just enough privileges to be able to make a mysqldump (using the “Principle of Least Privilege“; for the same reason the backup script is executed by an user, not by root). I chose the username ‘dumpo’ because… well. he just dumbly dumps stuff:
MariaDB [(none)]> CREATE USER 'dumpo'@'localhost' IDENTIFIED BY '<dumpopasswd>'; MariaDB [(none)]> GRANT SELECT, SHOW VIEW, LOCK TABLES, RELOAD, REPLICATION CLIENT ON *.* TO 'dumpo'@'localhost'; MariaDB [(none)]> FLUSH PRIVILEGES;
Then, the mysqldump user’s credentials must be stored in the ~/.my.cnf
file within the backup-executing user’s home directory. Afterwards, run chmod 600 ~/.my.cnf
to set the correct permissions.
[mysqldump]
user=dumpo
password=<dumpopasswd>
The actual backup script below is executed by an user’s cronjob once a week. I added a lot of comments to the example script to explain how it works.
#!/bin/bash
# The device acting as a fileserver for WP backups is not always powered up,
# so let's use Wake-On-LAN to make sure it is up when it is actually needed.
wol <mac-address-of-fileserver>
# I happen to have the same username on both the webserver and the fileserver,
# that simplifies some settings.
USER=<user-running-the-script>
WPUSER=dumpo
BFILENAME=backupwp-$(date +%d%m%Y)
# I'm mailing the backup messages somewhere.
MAILADR=someone@somewhere.com
# Let's make a temporary working directory on the users home directory.
cd /home/${USER}
mkdir -p backupdb
# I have three Wordpress sites on the server.
# Dump the mariadb databases of those sites to the working directory.
mysqldump -u ${WPUSER} SITE1 > backupdb/SITE1.sql
mysqldump -u ${WPUSER} SITE2 > backupdb/SITE2.sql
mysqldump -u ${WPUSER} SITE3 > backupdb/SITE3.sql
mysqldump -u ${WPUSER} mysql > backupdb/MYSQL.sql
echo -e "Database dump: done"
# Copy the Wordpress installations to the working directory.
cp -a /var/www/site1.com backupdb/
cp -a /var/www/site2.com backupdb/
cp -a /var/www/site3.com backupdb/
echo -e "Website copy: done"
# Make a tar.gz archive of the contents of the working directory.
tar -czf ${BFILENAME}.tar.gz backupdb
echo -e "Archive: done"
# Check whether the fileserver is up and running. I'm checking the port 22
# (ssh) because I'll be using rsync with ssh for upload. The loop waits for
# 5 minutes for the connection, and then gives up and mails an error message.
# Waiting for 5 minutes may actually be useless, because the fileserver usually
# boots up within a minute, and making the backup archive already takes up some
# 3-4 minutes. But just in case...
CHECK=0
until [[ `nmap <fileserver-ip-addr> -p 22 | grep "Host is up"` ]]
do
if [ $CHECK -ge 60 ]; then
echo -e "File server unreachable, aborting!!! $(date '+%d-%m-%Y %T')" | mail -s "Backup" ${MAILADR}
exit 1
fi
sleep 5
let CHECK++
done
# Send the backup archive to the fileserver using rsync with ssh.
rsync -e ssh ${BFILENAME}.tar.gz ${USER}@<fileserver-ip-addr>:/home/${USER}/wpbackup/
# If the upload was succesful, clean up by removing the local archive and
# temporary working directory, and mail a success message; otherwise, leave
# everything as it is and mail a failure message.
if [[ $? == 0 ]]; then
rm ${BFILENAME}.tar.gz
rm -rf backupdb
echo -e "Wordpress and mariadb backup completed successfully at $(date '+%d-%m-%Y %T')" | mail -s "Backup" ${MAILADR}
else
echo -e "Archive upload failure!!! $(date '+%d-%m-%Y %T')" | mail -s "Backup" ${MAILADR}
exit 1
fi
Finally, at the fileserver end, a script – executed by the cron some time after the upload – rotates and cleans up the backups, leaving only the 4 latest backup archives:
#!/bin/bash
cd /home/<user>/wpbackup
echo -e "Rotating Wordpress backups, removing following files:\n"
ls -1t | tail -n +5 | xargs rm -v # That is 'ls dash one t'.