restic-backup-suite is a production-ready backup toolkit for Linux servers built on top of restic. It automates the full backup lifecycle: database dumps, file backup, retention policy enforcement, and integrity verification — all in a single configurable shell script.
Automates the entire backup lifecycle on Linux servers — from database dumps to integrity checks — with a single command.
The toolkit consists of four components:
| File | Purpose |
|---|---|
backup.sh |
Automated backup with retention and verification |
restore.sh |
Interactive restore menu from any snapshot |
inspect.sh |
Browse and inspect snapshot contents without restoring |
install.sh |
System-wide installation with aliases and optional cron job |
set -euo pipefail) throughout all scriptsinstall.sh with shell aliases and optional cron job| Requirement | Version | Notes |
|---|---|---|
| Bash | 4.4+ | Standard on all modern Linux distributions |
| restic | any | Must be in PATH |
| Root access | — | Scripts require sudo |
| sftp | — | Only for SFTP remote backends (e.g. Hetzner Storage Box) |
| docker | — | Only for Docker database dumps and Docker service stops |
| mysqldump / mariadb-dump | — | Only for native MySQL/MariaDB dumps. Binary is auto-detected. |
| mysql / mariadb | — | Only for native dump (database listing) and restore. Binary is auto-detected. |
The install.sh script sets everything up system-wide:
git clone https://github.com/markus-michalski/restic-backup-suite.git
cd restic-backup-suite
sudo ./install.sh
After installation:
/etc/restic/config.sh/usr/local/bin/restic-backup, /usr/local/bin/restic-restore and /usr/local/bin/restic-inspect/etc/profile.d/restic-aliases.shWith optional cron job (daily at 03:00):
sudo ./install.sh --cron
git clone https://github.com/markus-michalski/restic-backup-suite.git
cd restic-backup-suite
cp config.example.sh config.sh
chmod 600 config.sh
Run scripts directly:
sudo ./backup.sh
sudo ./restore.sh
sudo ./inspect.sh
echo "your-strong-passphrase" > /etc/restic/password.txt
chmod 400 /etc/restic/password.txt
Point RESTIC_PASSWORD_FILE in config.sh to this file:
RESTIC_PASSWORD_FILE="/etc/restic/password.txt"
For SFTP backends (e.g. Hetzner Storage Box), create a dedicated key pair:
ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519_backup -N ""
Copy the public key to the remote server, then set in config.sh:
SSH_KEY_FILE="/root/.ssh/id_ed25519_backup"
backup.sh writes the SSH config entry automatically on the first run.
Configuration lives in config.sh (copied from config.example.sh). This file is gitignored and will never be committed.
config.shcontains sensitive credentials. Set permissions tochmod 600— only root should be able to read it.
| Variable | Description | Default |
|---|---|---|
RESTIC_PASSWORD_FILE |
Path to password file | /etc/restic/password.txt |
RESTIC_REPOSITORY |
Repository URL | — |
RESTIC_CACHE_DIR |
Local cache directory | ~/.cache/restic |
GOMAXPROCS |
CPU cores for restic | 2 |
SSH_HOST |
SSH alias (SFTP backend) | — |
SSH_HOSTNAME |
Real remote hostname | — |
SSH_PORT |
SSH port | 22 |
SSH_USER |
SSH username | — |
SSH_KEY_FILE |
SSH private key path | — |
LOG_DIR |
Log directory | /var/log/restic |
BACKUP_PATHS |
Array of paths to back up | — |
BACKUP_EXCLUDES |
Array of exclude patterns | — |
RETENTION_KEEP_DAILY |
Daily snapshots to keep | 7 |
RETENTION_KEEP_WEEKLY |
Weekly snapshots to keep | 4 |
RETENTION_KEEP_MONTHLY |
Monthly snapshots to keep | 6 |
REPO_CHECK_SUBSET |
Fraction of data to verify | 5% |
MYSQL_BACKUP_ENABLED |
Enable native MySQL/MariaDB dump | false |
DOCKER_MARIADB_CONTAINERS |
MariaDB/MySQL containers to dump. Format: "container:user:db[:PW_ENV]" — optional 4th field for password env var (default: MYSQL_ROOT_PASSWORD) |
() |
DOCKER_POSTGRES_CONTAINERS |
PostgreSQL containers to dump | () |
SERVICES_TO_STOP |
Systemd services to pause | () |
DOCKER_SERVICES_TO_STOP |
Docker Compose services or standalone containers to stop before backup. Format: "/path/compose.yml:service-name" or "container-name". No DB containers. |
() |
DOCKER_STOP_WAIT |
Seconds to wait after stopping Docker services | 2 |
DOCKER_START_WAIT |
Seconds to wait after starting Docker services | 3 |
SUCCESS_MARKER_FILE |
Path to the marker file written after a successful backup. Set to empty string to disable. | /var/backup/.last_restic_success |
# SFTP (Hetzner Storage Box)
RESTIC_REPOSITORY="sftp:myhost:/backup/restic"
# Local
RESTIC_REPOSITORY="/mnt/backup/restic-repo"
# Amazon S3
RESTIC_REPOSITORY="s3:s3.amazonaws.com/bucket-name"
# Backblaze B2
RESTIC_REPOSITORY="b2:bucket-name:/path"
DOCKER_MARIADB_CONTAINERS=(
"wikijs-db:root:mm_faq" # uses MYSQL_ROOT_PASSWORD
"mmkreativ-db:root:mm_live:DB_ROOT_PASS" # custom env var name
"another-db:root:shop_db:MARIADB_ROOT_PASSWORD" # newer official images
)
Format: "container_name:db_user:db_name[:password_env_var]"
The optional 4th field specifies the env var name inside the container that holds the root password. Default is MYSQL_ROOT_PASSWORD. Newer official MariaDB images use MARIADB_ROOT_PASSWORD; custom setups may use DB_ROOT_PASS or similar.
backup.shautomatically detects whethermariadb-dumpormysqldumpis available — both inside Docker containers and natively on the host.
Use DOCKER_SERVICES_TO_STOP to stop app containers before the file backup and restart them automatically afterwards. This prevents inconsistent snapshots when a container writes to its data directory during the backup.
Do not add DB containers to this list — database dumps run against the live container. Only stop app containers.
DOCKER_SERVICES_TO_STOP=(
"/srv/seafile/docker-compose.yml:seafile" # Compose-managed service
"my-standalone-container" # standalone container
)
Supported formats per entry:
"/path/to/docker-compose.yml:service-name" — Compose-managed service (docker compose stop/start)"container-name" — standalone container started via docker run (docker stop/start)The feature is opt-in: an empty array (the default) leaves existing behavior unchanged. Stopped containers are reliably restarted even on backup failure or Ctrl+C — the restart is wired into the cleanup trap.
After each successful restic backup, backup.sh writes an ISO timestamp to SUCCESS_MARKER_FILE (default: /var/backup/.last_restic_success). The directory is created automatically if it does not exist. The file is written with permissions 600.
External monitoring tools check the file age against your backup interval:
# Alert if file is older than 24 h (for daily backups)
find /var/backup/.last_restic_success -mmin +1440 && echo CRITICAL || echo OK
Behavior:
--dry-run: file is not written--dump-only: file is not written (no restic backup ran)A stale marker file means either the backup failed or the marker could not be written. Check the log for details.
To disable, set in config.sh:
SUCCESS_MARKER_FILE=""
sudo restic-backup --dry-run
# or directly:
sudo ./backup.sh --dry-run
Shows what would be backed up — without running restic. Services and containers are not stopped during a dry run.
sudo restic-backup
# or:
sudo ./backup.sh
sudo ./backup.sh --config /etc/restic/config.sh
sudo restic-backup --dump-only
# or directly:
sudo ./backup.sh --dump-only
Runs all configured database dumps (native MySQL, Docker MariaDB/PostgreSQL) and prints results with file sizes — no restic, no SFTP, no service stops. Ideal for verifying new container configurations.
sudo restic-restore
# or:
sudo ./restore.sh
The menu appears:
What do you want to restore?
----------------------------
1) Full backup (all paths)
2) Web files (/var/www)
3) Configuration (/etc)
4) Home directories (/home)
5) Database dumps (SQL files from backup)
6) SSL certificates (/etc/letsencrypt)
7) Custom path
q) Quit
sudo ./restore.sh --snapshot abc12345
Restored files always land in a temporary directory first (/tmp/restic-restore-XXXXXX). You review them before manually moving anything into place.
sudo restic-inspect
# or directly:
sudo ./inspect.sh
On startup, all available snapshots are listed for selection. Press Enter to use latest, or type a snapshot ID.
Then the interactive menu appears:
Snapshot: latest
================================
1) Show snapshot details
2) Show statistics (size)
3) List files (with optional path filter)
4) Search files by name/pattern
5) Diff against another snapshot
s) Select a different snapshot
q) Quit
sudo restic-inspect latest
sudo restic-inspect abc12345
# or:
sudo ./inspect.sh --snapshot abc12345
| Choice | Function |
|---|---|
1 |
Show snapshot details and metadata |
2 |
Show statistics: restore size of the snapshot |
3 |
List files — with optional path prefix filter (e.g. /var/www) |
4 |
Search file names by substring (e.g. wp-config, .sql) |
5 |
Diff between this snapshot and another |
s |
Switch to a different snapshot without quitting |
q |
Quit |
inspect.shdoes not restore any files — it is read-only inspection only. Userestore.shto actually recover files.
After sudo ./install.sh, the following aliases are available after next login (or source /etc/profile.d/restic-aliases.sh):
| Alias | Function |
|---|---|
restic-snapshots |
List all snapshots |
restic-stats |
Repository size and dedup statistics |
restic-check |
Integrity check (5% sample) |
restic-ls |
List files in the latest snapshot |
restic-mount |
Mount repository via FUSE |
restic-unlock |
Remove stale lock |
restic-rawstats |
Raw on-disk size |
Aliases automatically load the configuration from
/etc/restic/config.sh— no manualexportrequired.
| Symptom | Check | Solution |
|---|---|---|
Config file not found |
Does config.sh exist? |
cp config.example.sh config.sh |
Permission denied |
Script executable? | chmod +x backup.sh |
SFTP connection failed |
SSH config correct? | Test with sftp myhost manually |
Wrong password |
Password file correct? | Check: cat /etc/restic/password.txt |
Repository not found |
Repo initialized? | First backup inits automatically, or run restic init |
Lock exists |
Another process running? | Run restic-unlock |
No such snapshot |
Snapshot ID correct? | Run restic-snapshots for the list |
mysqldump: not found in Docker dump |
Newer MariaDB image without legacy binary | No fix needed — mariadb-dump is detected automatically |
Access denied (using password: NO) |
Container uses a different env var than MYSQL_ROOT_PASSWORD |
Add 4th field in config.sh: "container:root:db:DB_ROOT_PASS" |
Neither mariadb-dump nor mysqldump found |
No dump binary installed on host? | apt install mariadb-client or apt install mysql-client |
docker compose file not found in Docker stop |
Path in DOCKER_SERVICES_TO_STOP wrong? |
Check absolute path to docker-compose.yml |
| Monitoring alert: marker file stale | Backup failed or write error? | Check log: /var/log/restic/backup-YYYY-MM-DD.log |
On the first run, the restic repository is initialized automatically — no manual
restic initneeded.
# Check SSH config
grep -A5 "Host myhost" /root/.ssh/config
# Test manual SFTP connection
sftp myhost
# Populate known hosts
ssh-keyscan -p 22 storage.example.com >> /root/.ssh/known_hosts_backup
# Last backup log
tail -100 /var/log/restic/backup-$(date +%Y-%m-%d).log
# Cron log
tail -100 /var/log/restic/cron.log
All scripts follow the same pattern:
set -o errexit -o nounset -o pipefail — Strict modetrap cleanup EXIT ERR INT TERM — Reliable cleanupmain "$@" — Entry point at the end of the filemainconfig.sh — no hardcoded valuesThe script writes SSH configuration to ~/.ssh/config.d/restic-backup and automatically prepends Include ~/.ssh/config.d/* to ~/.ssh/config — without overwriting any existing configuration.
| Exit Code | Meaning |
|---|---|
| 0 | Backup/restore successful |
| 1 | General error (config missing, permission denied, etc.) |
Can I back up multiple servers with one configuration?
No — one config.sh corresponds to one server and one repository. For multiple servers, use separate configs and cron jobs.
What happens if the backup is interrupted?
restic leaves a lock. After restarting, run restic-unlock then restart the backup. Stopped Docker containers are automatically restarted by the cleanup trap — even on error or Ctrl+C.
Are database backups consistent?
Yes — database dumps are created before the backup and deleted afterward. The SQL files are included in the backup set.
Why stop Docker containers instead of just dumping the database?
DB dumps handle databases. But app containers like Seafile also write to blob storage directories (e.g. seafile-data) that aren't SQL. A clean container stop is the only way to guarantee a consistent snapshot of those directories.
How much overhead does restic deduplication add?
Very little. Incremental backups typically transfer only 1–5% of the total data size.
Can I mirror the repository to multiple targets?
restic doesn't support native mirroring. Alternative: use rclone as an additional sync step after the backup.
How do I uninstall everything?
sudo ./install.sh --uninstall
Configuration and logs are not deleted (asked separately).
MIT — Free to use, modify, and redistribute. No warranty.