Skip to content
Hogin Hogin
Go back

Backing Up Databases and Files to S3 with Restic + Healthchecks

Updated:
4 мин чтения

In this article we’ll cover a practical and reliable backup approach:

This solution is a good fit for:

Why Restic and Not rclone sync

rclone sync is mirroring, not a backup.

CapabilityResticrclone 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

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

Create a check at https://healthchecks.io

You’ll get a URL like:

https://healthchecks.example.com/ping/UUID

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:

Summary

✔ encrypted backups

✔ S3 / MinIO

✔ deduplication

✔ restore to any date

✔ monitoring via healthchecks

✔ production-ready

What You Can Add Next

Restic is one of the most reliable backup methods available today.

If you use Docker and S3, it’s a nearly perfect option.


Share this post:

Previous Post
Container Security: A Practical Checklist for Implementation
Next Post
A Lightweight Proxy