Backup files with hdup

Introduction

This article details how backups work at sbrk.org this is a reminder but it may be helpful for anyone needing backups with similar needs ; critics are welcomed.

Requirements

The server has the following characteristics:

I've splitted backups into two parts:

SQL backups

I'm aware of two ways to backup MySQL databases:

Here that's quite simple, a daily cron simple performs a mysqldump of all databases, the result is stored in an archive. Archives older than 30 days can be removed.

A trick to easily handle old files is to name files with the current number of the month, so there are automatically overwritten:

mysqldump --all-databases -uroot -ppassword | gzip > $(date +%m).tar.gz

We don't have huge databases so this works fine (with big databases it's more efficient to have differencial backups, but I prefer to keep things simple and easy, when the FTP will run out of space maybe I'll consider a better solution).

These backups are stored on the 100go ftp which is only accessible from the server itself, users are trusted, the ftp is trusted as much as the server, and the access to the ftp is only known to root users, so I don't consider encryption relevant.

File backups

This is more tricky as many things have to be handled here: encryption for user backups, monthly snapshots, incremental weekly and daily backups. This imply the usage of a tool, I've chosen hdup as it's simple to use.

Because of the 100go ftp backup and of the need of different encryption policies, I've decided to split backups into three parts:

To prevent non-root users to access backups that aren't encrypted we have a nologin user "backup" whose home directory will store backups:

# useradd -d /home/srv/backup -s /usr/sbin/nologin backup

Currently the backups aren't stored on a separated partition, but if you can, you clearly should do it (the backup partition is then only mounted during the backup, so even a rm -rf / won't affect your backups if there's no backup running).

Configuration

Now the hdup configuration is quite straightforward, I've omitted some parts:

[global]
# where to put the tar archives
archive dir = /home/srv/backup
date spec = iso
always backup = on
force = no
overwrite = on
proto = /usr/bin/ssh
proto option =  -q -oProtocol=2
user = backup
compression = lzop
compression level = 1
nobackup = .nobackup

# backup for files required to ensure the integrity of the server
[sbrk-global]
dir = /etc,/usr,/boot,/bin,/lib,/lib32,/lib64,/opt,/root,/sbin
exclude = lost\+found
user = backup

# backup for everything else
[sbrk-sensitive]
dir = /var,/home/srv,/home/users,/root
exclude = lost\+found
user = backup

# backup for user mxs
dir = # omitted, that's not public :-)
exclude = lost\+found/
algorithm = gpg
key = FDEFA783
user = mxs

Some important notes here, the user directive tells hdup to chown the backup to the given user. The key and algorithm tell hdup to encrypt the backup with gpg using the key whose identifier is FDEFA783 (this can be seen with gpg --list-keys). Another note about hdup, every folder that contains the file .nobackup won't be backed up, so it's a good idea to create this file in your backup directory to avoid backuping backups. Monthly snapshots take a lot of time an I don't care much as I have free space so I use lzop (one of the fastest compression algorithm around) with the minimal compression settings.

The last thing we need is a cron.

Cron

To make it easy to administrate, I've written a daily cron that handles everything (sql backup, monthly snapshot, weekly and daily incremental backups, push to the ftp, deletion of old backups).

#!/usr/bin/env bash
# s. rannou <mxs@sbrk.org>
# a script to perform backups with hdup, manages daily/weekly/monthly backup
#
# - $HDUP contains entries that must be daily,weekly and monthly backed, see /etc/hdup/hdup.conf
# - $HDUP_FTP contains entries that must be backed on $FTP_HOST (which is only 100go in our case)
# - all sql databases are backed for 30 days and pushed on $FTP_HOST
#
# this script needs lftp, gzip, hdup, tar, mysqldump, gpg

# mysql access (mysqldump --all-databases is used)
SQL_LOGIN="root_sql_login"
SQL_PW="root_sql_password"

# ftp access
FTP_LOGIN="ftp_login"
FTP_PW="ftp_password"
FTP_HOST="ftp_host"

# hdup sections
HDUP=(sbrk-global sbrk-sensitive sbrk-mxs)
HDUP_FTP=(sbrk-global)

# common settings
BACKUP_DIR=/home/srv/backup
BACKUP_SQL_DIR=/home/srv/backup/sql

echo "--- $(date) backup started"

if [ ! -d $BACKUP_SQL_DIR ]; then
    mkdir -p $BACKUP_SQL_DIR
fi

if [ ! -f $BACKUP_DIR/.nobackup]; then
    touch $BACKUP_DIR/.nobackup
fi

# -- hdup
# monthly backup
for SECTION in ${HDUP[@]}; do
    # if we are the 1st of the month or if there isn't any monthly backup, start a monthly backup
    if [ $(echo "$(date +%d)" | bc) -eq 1 ] || [ $(find "${BACKUP_DIR}/${SECTION}" -name "*monthly.tar*" 2>/dev/null | wc -l) -eq 0 ]; then
    echo "--- $(date) started monthly backup for ${SECTION}"
    hdup -P monthly ${SECTION}
    echo "--- $(date) ended monthly backup for ${SECTION}"
    else
    echo "--- $(date) no monthly backup needed for ${SECTION}"
    fi
done
# weekly backup
for SECTION in ${HDUP[@]}; do
    # if we are the 1,8,15,22 or 29 of the month or if there isn't any weekly backup, start a weekly backup
    if [ $(echo "($(date +%d) + 6) % 7" | bc) -eq 0 ] || [ $(find "${BACKUP_DIR}/${SECTION}" -name "*weekly.tar*" 2>/dev/null | wc -l) -eq 0 ]; then
    echo "--- $(date) started weekly backup for ${SECTION}"
    hdup -P weekly ${SECTION}
    echo "--- $(date) ended weekly backup for ${SECTION}"
    else
    echo "--- $(date) no weekly backup needed for ${SECTION}"
    fi
done
# daily backup
for SECTION in ${HDUP[@]}; do
    echo "--- $(date) started daily backup for ${SECTION}"
    hdup -P daily ${SECTION}
    echo "--- $(date) ended daily backup for ${SECTION}"
done

# -- sql
echo "--- $(date) - backing up all sql databases"
# trick here, the backup is made for 28/29/30 or 31 days using the number of the day
# (1st december will result in /home/srv/backup/sql/backup-1.sql, and so on)
SQL_DEST="${BACKUP_SQL_DIR}/backup-`date +%d`.sql.tar.gz"
mysqldump --all-databases -u${SQL_LOGIN} -p${SQL_PW} | gzip > ${SQL_DEST}
chown backup:backup ${SQL_DEST}
echo "--- $(date) - sql done"

# -- some cleaning
echo "--- $(date) - mr proper started"

# - monthly snapshots older than 6 months
find ${BACKUP_DIR} -name "*monthly.tar*" -mtime +180 -exec rm {} \;
# - weekly snapshots older than 2 months
find ${BACKUP_DIR} -name "*weekly.tar*" -mtime +60 -exec rm {} \;
# - daily snapshots older than 2 weeks
find ${BACKUP_DIR} -name "*daily.tar*" -mtime +14 -exec rm {} \;

echo "--- $(date) - mr proper ended"

# -- ftp
# push a subset of backups on the ftp
# this is a 100go ftp, we only push sbrk-global (should be small) and sql for 30 days
echo "--- $(date) - ftp sql put started"
echo "mirror -e -R ${BACKUP_SQL_DIR} /sql" | lftp ftp://${FTP_LOGIN}:${FTP_PW}@${FTP_HOST}
echo "--- $(date) - ftp sql put ended"

for SECTION in ${HDUP_FTP[@]}; do
    BACKUP_SECTION_DIR=${BACKUP_DIR}/${SECTION}
    echo "--- $(date) - ftp ${SECTION} put started"
    echo "mirror -e -R ${BACKUP_SECTION_DIR} /${SECTION}" | lftp ftp://${FTP_LOGIN}:${FTP_PW}@${FTP_HOST}
    echo "--- $(date) - ftp ${SECTION} put ended"
done

echo "--- $(date) backup ended"

Be sure to understand it before using it, then to install it:

$EDITOR backup.sh
mv backup.sh /etc/cron.daily
chmod 700 /etc/cron.daily/backup.sh

Et voilà!