Skip to content

How to backup databases on a server

In order to ensure that our applications are resilient in the case of an catastrophic bug, we regularly back up databases on both staging and production servers. Code should not need to be backed up, as it is held on GitLab, and can be redeployed by re-running a pipeline.

Create a new user for backups

For security reasons, we don't want to use the root user to perform back-ups. So, we create a new user with minimal permissions that will be called backup. The $HOME directory for the new user should be /var/backups and its shell should be set to /usr/sbin/nologin.

Start by generating a random password for the new user:

date | md5sum

Make sure you write the password down somewhere safe -- do NOT put it online anywhere!

Creating a new user

On Ubuntu you should be able to find a user called backup, which has no $HOME, in /etc/passwd.

If you are not using Ubuntu, you may need to create a new user:

sudo adduser --system --disabled-password backup

Make sure that the new home directory has the correct ownership and permissions:

chmod +rx /var/backups
chown root.root /var/backups

Create a read-only mysql user

On Ubuntu, you should find that MySQL uses UNIX sockets to authenticate. This means that you should be able to run sudo mysql -uroot and use the MySQL CLI without a password.

If you are not using Ubuntu, you may need to create a separate MySQL account for backups:

mysql> GRANT LOCK TABLES, SELECT ON *.* TO 'backup'@'localhost' IDENTIFIED BY 'PASSWORD';
Query OK, 0 rows affected (0.01 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)

mysql> Bye

where PASSWORD is the plain text password that you generated in the previous step.

Create a .my.cnf file in /root/

So that the new user can run mysqldump to write out a database to disk, you need to place a configuration file in the home directory of the root user:

sudo su
cd
umask 0577
sudo tee .my.cnf << EOF
[mysqldump]
user=backup
password=PASSWORD
EOF

where PASSWORD is the plain text password that you generated in the steps above.

Before you move on, check that the file has the file has the right permissions, then go back to your own user:

# ls -lh .my.cnf
-r--------. 1 root root 67 Jan 25 18:53 .my.cnf
# # Press Ctrl+d
$

Create a cron job to performs the backups

Databases should be backed up regularly. We have many servers, and so we don't have time to log into each one and back up all the databases manually. Instead, we use a cron job to automate the backups on each machine. By convention, we back up all databases on each server daily.

Before you create a cron job, you need to know the name of the database for your web application. Find out where the current version of your app is on disk. Try /var/www/SITE.DOMAIN.com/APP/TOKEN/ on Ubuntu. Go to the relevant directory and look inside its .env file, which specifies the environment variables that the application needs (similar to the variables that GitLab pipelines use). The name of the database will be on a line that starts DB_DATABASE=....

Go to the /etc/cron.daily/ directory, and add this file:

#!/bin/sh

# Rotation is done through logrotate.
# Username and password can be found in /root/.my.cnf
umask 0577
mysqldump --no-tablespaces DB_DATABASE | gzip -9c > /var/backups/DB_DATABASE.sql.gz

# Add similar lines here if there is more than one database on
# the server (e.g. production, staging).

Note that newer versions of MySQL require --no-tablespaces.

Then make the file executable:

sudo chmod +x backup

Before you move on, check that the file permissions are correct:

$ ls -lh backup
-rwxr-xr-x. 1 root root 169 Jan 25 18:56 backup
$

and run the script to check that it works:

./backup
ls -lh /var/backups/

If you want to download the resulting backup file, copy it to your home directory on the server and run chmod +r DB_NAME.sql.gz before downloading with scp.

Rotating logs

We will be creating one backup file every day, for every database on the server. If we don't take care to manage our back-up files, we will eventually run out of disk space. To take care of this, we use a tool called logrotate, which was originally intended to manage log files.

Imagine you have a log file called /var/myservice/error.log. logrotate would move the file every day, week, month (or whatever schedule you set), so that you might end up with files: error.log, error.log.1, error.log.2, error.log.3, error.log.4. Each time the logs are rotated, error.log.4 will be deleted, error.log.3 will become error.log.4, and so on. The new error.log will be empty.

Create a file in /etc/logrotate.d/ called backups and place the rotate script inside:

cd /etc/logrotate.d
sudo tee backups << EOF
/var/backups/*.sql.gz
{
        create 440 root root
        rotate 21
        daily
        missingok
        notifempty
        nocompress
        dateext
        sharedscripts
}
EOF

Before you move on, check that the file permissions are correct:

$ ls -lh backups
-rw-r--r-- 1 root root 167 Oct 25 11:57 backups

and check that the rotation completes without raising an error:

sudo logrotate backups

Checking the backup has worked

Now you have set up your database backups, you should wait a day or two and check that the backup worked.

On the server, check the contents of the /var/backups directory, and copy the latest backup to your home directory:

cd
sudo cp /var/backups/DB_NAME.sql.gz .
sudo chmod +r DB_NAME.sql.gz

From your development machine (i.e. not on the server) copy the backup over, and unzip it:

scp USERNAME@SERVER_HOSTNAME:~/DB_NAME.sql.gz .
gunzip DB_NAME.sql.gz

Next, start mysql and create the database, if it doesn't already exist:

$ sudo mysql -u root
Welcome to the MySQL monitor.  Commands end with ; or \g.

...

mysql> CREATE DATABASE DB_NAME;
Query OK, 1 row affected (0.00 sec)

mysql> Bye

Then import the SQL dump:

sudo mysql -u root DB_NAME < DB_NAMEsql

Then start mysql and check the imported data:

$ sudo mysql -u root
Welcome to the MySQL monitor.  Commands end with ; or \g.

...

mysql> USE DB_NAME;
mysql> SHOW TABLES;

...

Troubleshooting

Debugging cron

If you have edited /etc/crontab be sure to check whether /etc/anacrontab might also need editing.

Remember that scripts inside /etc/cron.daily and similar directories are run in alphabetical order. If you want to be certain that scripts are running in a particular order (e.g. backups run before logrotate) then rename the scripts with numbers at the start of their names:

$ ls /etc/cron.daily
0yum-daily.cron  1backup.cron  2logrotate.cron

If you are not sure whether cron.daily is running correctly, debugging is much easier if you move your scripts to /etc/cron.hourly to get more frequent debug output.

You should also set up Postfix and make sure that /etc/crontab and /etc/anacrontab contain the line:

MAILTO=tech@beautifulcanoe.com

so that debug output is read by an administrator and does not just sit on the server.

Debugging logrotate

To debug logrotate, start by checking the status file in /var/lib/logrotate/logrotate.status which will tell you when each log was last rotated.

The file /etc/cron.daily/logrotate.cron calls the logrotate script directly. Editing that file to call the script with the -v switch will cause verbose output to be generated. If cron is set up to send an external email (see above), this will give you useful output each time the logs are rotated.

logrotate options: nocompress and delaycompress

Note that we have used the nocompress option in the logrotate script above. If you do choose to compress logs, make sure that you also turn on the delaycompress option. That way you will avoid this bug, which causes error messages such as:

/etc/cron.daily/logrotate:

gzip: stdin: file size changed while zipping