Writing a script that copies files to object storage takes ten minutes. Making it something you can rely on at 3am, eight months from now, when you are stressed, takes rather more.
Most of what follows is about the second part.
The script
Assuming an rclone remote named backup:
#!/usr/bin/env bash
set -euo pipefail
REMOTE="backup:server-backups/$(hostname)"
STAMP="$(date -u +%Y-%m-%d)"
RCLONE_CONF="/root/.config/rclone/rclone.conf"
exec >>/var/log/backup.log 2>&1
echo "=== backup started $(date -u +%FT%TZ) ==="
# Database first, into a directory the file step then picks up.
pg_dump -Fc mydb > "/var/backups/mydb-${STAMP}.dump"
# Keep 14 days of dumps locally; the remote keeps its own history.
find /var/backups -name 'mydb-*.dump' -mtime +14 -delete
rclone sync /var/backups "$REMOTE/db" \
--config "$RCLONE_CONF" \
--transfers 8 --checkers 16 --fast-list \
--max-delete 5
rclone sync /srv/data "$REMOTE/data" \
--config "$RCLONE_CONF" \
--transfers 8 --checkers 16 --fast-list \
--exclude "*.tmp" --exclude "cache/**" \
--max-delete 100
echo "=== backup finished $(date -u +%FT%TZ) ==="
Three lines are doing disproportionate work.
set -euo pipefail. Without it, a failing pg_dump lets the script continue
and exit successfully. You then have a backup that appears to work and contains
yesterday’s data forever.
--max-delete. sync mirrors deletions. If a mount fails and /srv/data
appears empty, sync will faithfully empty the destination. This flag makes the job
refuse rather than comply.
--config. Cron does not have your HOME. This is the single most common
reason a script that works interactively fails at 2:30am.
Schedule it
30 2 * * * /usr/local/bin/backup.sh
Use an absolute path. Cron runs with a minimal PATH and a different working
directory than your shell.
If the job matters, prefer a systemd timer over cron: you get systemctl status,
proper logging in the journal, Persistent=true to catch up after downtime, and
failure states you can query rather than infer.
The parts people skip
Know when it fails
A silent backup failure is worse than no backup, because you believe you are covered. The belief is the dangerous part.
Emailing yourself works until the mail lands in a folder nobody opens. The reliable pattern is a dead man’s switch: the script pings a monitoring URL on success, and the monitor alerts you when the ping stops arriving. That catches the cases a failure-only alert misses entirely - the server being off, cron not running, the disk being full before the script even starts.
curl -fsS --retry 3 https://your-monitor.example/ping/abc123
as the last line, after everything else has succeeded.
Restore, at least once
An untested backup is a hypothesis.
Restore onto a scratch machine and actually start the service against it. This is where you discover the database dump was missing an extension, that file permissions did not survive, that the thing you needed lived in a directory nobody included, or that the restore takes eleven hours and you had assumed one.
Put a recurring reminder in the calendar. Twice a year is enough to catch drift.
Keep history, not just the latest copy
A sync that mirrors current state also mirrors this morning’s accidental deletion, and ransomware encrypting your files is a change like any other - it will be faithfully replicated.
Either keep dated archives, or enable object versioning so an overwritten object can be recovered. Without one of those, you have a mirror rather than a backup, and the difference only becomes apparent on the worst day.
Watch the size
Log the transferred size each run and compare against the previous one. A large unexplained drop means something upstream broke, and it is much better to hear about it from a log than from a restore attempt.
Cost and sizing
Server backups pass a free tier immediately - ours is 5 GB of total storage, which a single database dump can exceed - so this is a paid plan workload. Estimate from your data volume multiplied by how many historical copies you keep, using our sizing guide as a starting point.
One thing to check before committing: whether restores are billed. A backup target that charges egress is cheap to write to and expensive at precisely the moment you are having a catastrophe. Calculate the full-restore cost now, while it is a hypothetical. We do not meter downloads, for reasons that apply generally.
Credentials
Give the backup job its own credential, scoped to the bucket it needs, rather than reusing an admin key.
If the server is compromised, that credential is what the attacker gets. Being able to revoke it without disturbing anything else is the difference between an incident and a catastrophe. Where the storage supports it, a credential that can write but not delete limits the damage further - an attacker can add junk but not destroy your history.
The same logic applies to any protocol you expose; our SFTP guide covers credential scoping for file transfer specifically.