Guides

Back up a Linux server to object storage with cron

A backup script takes ten minutes. A backup you can actually restore from takes more thought. Here is a setup with the failure modes handled.

, Product Manager, dosya.dev team 4 min read

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.