Restic: Sleep Soundly, Your Servers Are Backed Up
restic
In my production environment, even for personal use, I consider backing up my data to be paramount. Losing everything following an incident would be, for me, a massive waste of precious personal time. In this article, I will share how I orchestrate my backups from my main server (OVH) to a second storage server hosted at my home using Restic.
Why I Chose Restic
Restic is the ideal tool for me: it is modern, fast, encrypted by default, and natively handles deduplication. It is exactly what I needed for my self-hosted infrastructure to optimize disk space while ensuring security.
1. My Data Selection Strategy
Do not backup /var/lib/docker as is.
In a Docker environment, backing up the live data directory is a bad practice. Database files (MySQL/Postgres) copied “hot” will likely be corrupted as they are in use by the Docker engine.
Here is what I backup:
- Docker-compose & Configs: Everything that defines my infra (
.yml,.env, nginx/traefik config files). - Database Dumps: Compressed SQL exports, created via the
databasusservice. - Data Volumes: My static files (images, documents, etc.).
- Home: User configurations (
/home/user).
Automating SQL Dumps with Databasus
To ensure my databases are backed up consistently, I use Databasus. It is a containerized tool I integrated to create dumps of my databases (PostgreSQL, MySQL, etc.) at regular intervals without “complex” scripts that would need maintenance.
I simply configure it to drop the dumps into a shared volume, which Restic then picks up.
Here is my databasus configuration in my compose.yml:
services:
databasus:
container_name: databasus
image: databasus/databasus:latest
volumes:
- ./databasus-data:/databasus-data # This is where I store my dumps
restart: unless-stopped
2. Preparing the Environments
Target (Storage Server)
I make sure the storage directory is ready and accessible via SSH:
# On the destination machine
sudo mkdir -p /var/restic-backup
sudo chown <SSH_USER>:<SSH_USER> /var/restic-backup
Source (My OVH Server)
I am securing my repository password in a file with restricted permissions :
sudo mkdir -p /etc/restic
sudo vim /etc/restic/password
sudo chmod 400 /etc/restic/password
Then, I initialize my repository (one-time setup):
restic -r sftp:<SSH_USER>@<SERVER-IP>:/var/restic-backup \
--password-file /etc/restic/password init
3. Orchestration Script: run-backup.sh
I designed this script to perform three key actions: file backup, applying my retention policy, and sending alerts in case of failure.
#!/bin/bash
set -e
set -o pipefail
# --- MY CONFIGURATION ---
REPO="sftp:<SSH_USER>@<SERVER-IP>:/srv/restic-backup"
PASS_FILE="/etc/restic/password"
BACKUP_PATHS="/home /opt/docker /var/backups/db-dumps"
RETENTION_DAYS="7"
ALERTMANAGER_URL="http://<ALERTMANAGER_IP>:9093/api/v2/alerts"
# My Alertmanager notification function
notify_alertmanager() {
local status=$1
local message=$2
local severity="info"
[ "$status" == "failure" ] && severity="critical"
curl -s -X POST "$ALERTMANAGER_URL" \
-H "Content-Type: application/json" \
-d "[{
\"labels\": {
\"alertname\": \"ResticBackupStatus\",
\"severity\": \"$severity\",
\"instance\": \"bracloud-ovh\",
\"job\": \"restic-nightly\"
},
\"annotations\": {
\"summary\": \"Backup $status\",
\"description\": \"$message\"
}
}]" || true
}
echo "=== Starting my Backup: $(date) ==="
# 1. Running Restic Backup
echo ">>> Restic Backup..."
if restic -r "$REPO" --password-file "$PASS_FILE" backup $BACKUP_PATHS \
--exclude-file /etc/restic/excludes.txt \
--tag nightly; then
echo "Backup Success."
else
echo "Backup Failed."
notify_alertmanager "failure" "Restic backup command failed on bracloud-ovh."
exit 1
fi
# 2. My Retention Policy (7 days)
echo ">>> Restic Forget & Prune..."
restic -r "$REPO" --password-file "$PASS_FILE" forget \
--keep-daily $RETENTION_DAYS \
--prune
# 3. My Light Integrity Check
restic -r "$REPO" --password-file "$PASS_FILE" check --read-data-subset=5%
echo "=== My Backup completed successfully: $(date) ==="
4. Automation with Systemd
I chose to move away from Crontab in favor of Systemd Timers. This provides much better visibility within journalctl.
The Service (/etc/systemd/system/restic-backup.service)
[Unit]
Description=My Restic Backup Service
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/run-backup.sh
StandardOutput=journal
The Timer (/etc/systemd/system/restic-backup.timer)
I have scheduled it to trigger every day at 3:00 AM:
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
5. Weekly Integrity Check (run-check.sh)
For me, a backup is only reliable if I am certain I can restore it. In addition to the light daily check, I automated a full check every Sunday at 5:00 AM. This is a resource-intensive operation, which is why I chose this specific window.
I use a dedicated Service and Timer duo (restic-check.service / restic-check.timer) to ensure that my data stored on the second server is not corrupted.
6. Restoration Tests
I make it a rule: regularly test the restoration. Here are the commands I use to ensure everything is working:
- List my snapshots:
restic snapshots - Check global integrity:
restic check - Restore a test file:
restic -r <REPO> restore latest --target /tmp/restore-test --include /home/user/important.txt
Conclusion
By combining Restic, Systemd, and SQL Dumps, I have managed to set up a backup solution that I consider reliable, while remaining free and under my total control. My final advice: don’t wait for a disaster to test your restoration procedure!
Actuellement ingénieur DevOps/Architecte Cloud, j’étais initialement interessé par l’administration système et grâce aux entreprises dans lesquelles j’ai pu travailler Oxalide et maintenant Claranet j’ai eu la chance de découvrir l’univers du Cloud et de l’automatisation.
Je me suis décidé a publier ce blog pour vous faire partager ma passion mais également pour enrichir avec modestie tout ce que l’on peut trouver sur internet. Bonne lecture !