In this article we’ll cover a practical and reliable backup approach:
- databases (PostgreSQL / MariaDB)
- configuration and user files
- storing them in S3 / MinIO
- with encryption
- with failure notifications via Healthchecks
This solution is a good fit for:
- a single server
- Docker / Docker Compose
- cron or systemd
- production
Why Restic and Not rclone sync
rclone sync is mirroring, not a backup.
| Capability | Restic | rclone sync |
|---|---|---|
| File versions | ✅ | ❌ |
| Deletion protection | ✅ | ❌ |
| Ransomware protection | ✅ | ❌ |
| Encryption | ✅ | ⚠️ manual |
| Retention | ✅ | ❌ |
Bottom line:
Restic is a backup system; rclone is transport.
Solution Architecture
PostgreSQL / MariaDB
↓ (dump)
Dump file
↓
Restic
↓
S3 / MinIO
↓
Healthchecks (OK / FAIL)
What We’ll Be Backing Up
- Database (via dump)
- Files and configs
- Storage in S3
- Status notifications
1 Installing Restic
Ubuntu / Debian
apt update
apt install -y restic
Check:
restic version
2 Preparing the Restic Password
The password is never stored in plaintext — only in a file.
mkdir -p /root/.config/restic
openssl rand -base64 48 > /root/.restic-pass
chmod 600 /root/.restic-pass
Losing the password = losing all your backups.
3 Configuring Access to S3 / MinIO
Create an env file:
/root/.config/restic/backup.env
RESTIC_PASSWORD_FILE=/root/.restic-pass
RESTIC_REPOSITORY=s3:https://minio.example.com/backups/project-name
AWS_ACCESS_KEY_ID=XXXXXXXX
AWS_SECRET_ACCESS_KEY=YYYYYYYY
AWS_DEFAULT_REGION=us-east-1
chmod 600 /root/.config/restic/backup.env
4 Initializing the Repository (once)
set -a
source /root/.config/restic/backup.env
set +a
restic init
5 Healthchecks (Optional, but Highly Recommended)
Create a check at https://healthchecks.io
You’ll get a URL like:
https://healthchecks.example.com/ping/UUID
- Success → the plain URL
- Failure → /ping/UUID/1
6 The Complete Backup Script (PostgreSQL + Restic + Healthchecks)
Path: /opt/project/backup.sh
#!/usr/bin/env bash
set -euo pipefail
### CONFIG
BASE_DIR="/opt/project"
RESTIC_ENV="/root/.config/restic/backup.env"
COMPOSE_FILE="${BASE_DIR}/docker-compose.yml"
COMPOSE_ENV="${BASE_DIR}/.env"
DB_SERVICE="db"
BACKUP_DIR="/var/backups/project"
KEEP_LOCAL_DAYS=7
HC_URL="https://healthchecks.example.com/ping/UUID"
### ERROR HANDLER
fail() {
curl -fsS --retry 3 --max-time 10 "${HC_URL}/1" >/dev/null 2>&1 || true
}
trap fail ERR
### LOAD RESTIC ENV
set -a
source "$RESTIC_ENV"
set +a
### READ DB CREDS SAFELY
env_get() {
grep -E "^$1=" "$2" | tail -n1 | cut -d= -f2- | tr -d '"'
}
POSTGRES_USER="$(env_get POSTGRES_USER "$COMPOSE_ENV")"
POSTGRES_PASSWORD="$(env_get POSTGRES_PASSWORD "$COMPOSE_ENV")"
POSTGRES_DB="$(env_get POSTGRES_DB "$COMPOSE_ENV")"
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
DATE="$(date +%F_%H%M%S)"
HOST="$(hostname -s)"
DUMP_FILE="${BACKUP_DIR}/postgres_${POSTGRES_DB}_${HOST}_${DATE}.dump"
### DATABASE DUMP
docker compose -f "$COMPOSE_FILE" --env-file "$COMPOSE_ENV" exec -T \
-e PGPASSWORD="$POSTGRES_PASSWORD" \
"$DB_SERVICE" \
sh -lc "pg_dump -U '$POSTGRES_USER' -d '$POSTGRES_DB' -Fc --no-owner --no-acl" \
> "$DUMP_FILE"
### RESTIC BACKUP (relative paths)
(
cd "$BACKUP_DIR"
restic backup . \
--tag project \
--tag postgres \
--tag prod \
--tag "$HOST"
)
### RETENTION
restic forget \
--keep-within 7d \
--keep-daily 14 \
--keep-weekly 8 \
--keep-monthly 12 \
--prune
### LOCAL CLEANUP
find "$BACKUP_DIR" -type f -mtime +"$KEEP_LOCAL_DAYS" -delete
### SUCCESS
curl -fsS --retry 3 --max-time 10 "$HC_URL" >/dev/null
echo "[OK] backup successful"
chmod 700 /opt/project/backup.sh
7 Cron: Running Every 6 Hours
0 */6 * * * root cd /opt/project && ./backup.sh >> /var/log/project-backup.log 2>&1
7.1 Cron vs systemd timer: Why a Timer Is Better and How to Set It Up
Create the file:
/etc/systemd/system/backup.service
[Unit]
Description=Backup job via restic
Wants=network-online.target
After=network-online.target docker.service
Requires=docker.service
[Service]
Type=oneshot
WorkingDirectory=/opt/project
ExecStart=/opt/project/backup.sh
# don't limit the execution time
TimeoutStartSec=0
User=root
Group=root
[Install]
WantedBy=multi-user.target
The timer defines the run schedule.
Create the file:
/etc/systemd/system/backup.timer
[Unit]
Description=Run backup every 6 hours
[Timer]
# every 6 hours: 00:00, 06:00, 12:00, 18:00
OnCalendar=*-*-* 00,06,12,18:00
# if the server was off — run it after startup
Persistent=true
# a small delay after the system boots
OnBootSec=5min
[Install]
WantedBy=timers.target
Enable the timer:
systemctl daemon-reload
systemctl enable --now backup.timer
Check the schedule:
systemctl list-timers | grep backup
Run manually (for testing):
systemctl start backup.service
systemctl status backup.service
View the execution logs:
journalctl -u backup.service -n 100 --no-pager
A systemd timer is a more reliable and manageable replacement for cron when it comes to backups.
8 Checking the State
List of Snapshots
restic snapshots
Inspect the Contents
restic ls latest
9 Restoring the Database
restic restore latest --target /restore
pg_restore -d mydb /restore/postgres_mydb_*.dump
Retention Policy
--keep-within 7d # all backups from the last 7 days (every 6 hours)
--keep-daily 14
--keep-weekly 8
--keep-monthly 12
This is the ideal balance between:
- granularity
- saving space
- the ability to roll back
Summary
✔ encrypted backups
✔ S3 / MinIO
✔ deduplication
✔ restore to any date
✔ monitoring via healthchecks
✔ production-ready
What You Can Add Next
- a second off-site S3 (3-2-1+)
- immutable bucket / object lock
- a systemd timer instead of cron
- automated restore testing
- backing up volumes and configs
Restic is one of the most reliable backup methods available today.
If you use Docker and S3, it’s a nearly perfect option.