◀ Back to blog
Linux

Automated server backups: MySQL, files and restic

Published on 11 Aug 2026· 7 min read
#Linux#Sauvegarde#MySQL#restic#systemd

A backup that has never been restored does not exist

Everyone "has backups", until the day they need to restore: the dump has been empty for three months, the archive sits on the same disk as the server, or nobody knows the encryption password. A good strategy fits in one rule, the 3-2-1 rule: 3 copies of the data, on 2 different media, 1 of them off-site. And in one discipline: test the restore.

This guide sets up the following on a Linux server hosting a web application:

  • a consistent MySQL dump, without blocking the application;
  • an encrypted, deduplicated and incremental backup with restic to S3 object storage;
  • a retention policy, systemd scheduling and an alert on failure.

1. The MySQL dump

Copying the files in /var/lib/mysql while the server is running gives an inconsistent backup. Use mysqldump with --single-transaction: for InnoDB tables, the dump is taken inside a transaction, so it is consistent, without locking writes.

First, a dedicated backup user with only what it needs:

CREATE USER 'backup'@'localhost' IDENTIFIED BY 'long-password';
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES, PROCESS ON *.* TO 'backup'@'localhost';

Its credentials go in a file readable by root only, so they never show up in the process list:

# /root/.my-backup.cnf  (chmod 600)
[client]
user=backup
password=long-password
mysqldump --defaults-extra-file=/root/.my-backup.cnf \
  --single-transaction --quick --routines --triggers --events \
  --databases app | gzip > /var/backups/mysql/app.sql.gz

--quick reads rows one by one instead of loading each table into memory: essential for large tables. Beyond a few tens of GB, switch to a physical backup (Percona XtraBackup or MySQL Enterprise Backup).

2. Why restic

A daily tar.gz archive copies everything, every day. restic splits files into chunks, stores each chunk only once, and encrypts everything client-side (AES-256) before upload. The result: daily backups that only cost the size of the changes, remote storage that never sees your data in clear text, and the ability to restore any retained point in time.

sudo apt install -y restic

The repository settings go in a protected environment file, /etc/restic/env (chmod 600). Any S3-compatible storage works (AWS, Scaleway, OVH, Backblaze B2, Cloudflare R2…):

RESTIC_REPOSITORY=s3:https://s3.fr-par.scw.cloud/my-backup-bucket/web-server
RESTIC_PASSWORD_FILE=/etc/restic/password
AWS_ACCESS_KEY_ID=xxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxx
sudo sh -c 'openssl rand -base64 48 > /etc/restic/password && chmod 600 /etc/restic/password'
sudo sh -c 'set -a; . /etc/restic/env; restic init'

Keep the restic password off the server, in a password manager. Without it, the repository cannot be recovered, and that is by design. If the server burns down with the only copy of the password, your backups burn with it.

3. The backup script

/usr/local/bin/backup.sh:

#!/usr/bin/env bash
set -euo pipefail

set -a; . /etc/restic/env; set +a

DUMP_DIR=/var/backups/mysql
mkdir -p "$DUMP_DIR"

# 1. MySQL dump (temporary file, then rename: never a half-written dump)
mysqldump --defaults-extra-file=/root/.my-backup.cnf \
  --single-transaction --quick --routines --triggers --events \
  --databases app | gzip > "$DUMP_DIR/app.sql.gz.tmp"
mv "$DUMP_DIR/app.sql.gz.tmp" "$DUMP_DIR/app.sql.gz"

# 2. Back up the dumps, the uploaded files and the configuration
restic backup \
  "$DUMP_DIR" \
  /var/www/app/shared \
  /etc/nginx /etc/php \
  --tag daily \
  --exclude-caches

# 3. Retention: 7 days, 4 weeks, 6 months
restic forget --tag daily --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

# 4. Verify a sample of the data
restic check --read-data-subset=5%

set -euo pipefail is crucial: without pipefail, a failing mysqldump followed by a successful gzip goes unnoticed, and you back up an empty file for months.

sudo chmod 700 /usr/local/bin/backup.sh

4. Scheduling with a systemd timer

A systemd timer has two advantages over cron: logs go to journald, and Persistent=true catches up on a missed run if the server was off.

# /etc/systemd/system/backup.service
[Unit]
Description=MySQL + files backup to restic
After=network-online.target mysql.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Nice=10
IOSchedulingClass=idle
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=15min
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer      # next run
sudo systemctl start backup.service     # first manual run
journalctl -u backup.service -e         # logs

5. Getting notified when it fails

A backup that fails silently is the worst-case scenario. The simplest solution is a "dead man's switch" service (Healthchecks.io, Uptime Kuma, Better Stack…): the script sends a ping on every success, and the service alerts you if nothing arrives within the expected window. Add this at the end of backup.sh:

curl -fsS -m 10 --retry 3 https://hc-ping.com/your-uuid > /dev/null

Thanks to set -e, the ping is only sent if every previous step succeeded.

6. Testing the restore (for real)

Schedule a regular restore test, for example every month, on another machine:

set -a; . /etc/restic/env; set +a

restic snapshots --tag daily                       # list the backups
restic restore latest --target /tmp/restore        # latest version
restic restore latest --target /tmp/restore --include /var/www/app/shared/uploads

# Restore the database into a test database
mysql -e 'CREATE DATABASE app_restore_test'
zcat /tmp/restore/var/backups/mysql/app.sql.gz \
  | sed 's/`app`/`app_restore_test`/g' | mysql
mysql -e 'SELECT COUNT(*) FROM app_restore_test.user'

Time the whole operation: that is your real RTO (the time it takes to get back into service). The age of the last successful backup is your RPO (the amount of data you accept to lose). If these two values do not suit the business, now is the time to find out, not during an incident.

In short

  • A consistent dump with --single-transaction, credentials kept off the command line
  • restic: encrypted, deduplicated, off-site, with a clear retention policy
  • systemd scheduling with Persistent=true, and an alert if the ping does not arrive
  • The repository password stored somewhere other than the server
  • Restores tested regularly and timed