
Scheduled Database and File Backups to S3-Compatible Storage with rclone
Before you run this
This sets up an unattended nightly job that dumps your MySQL/MariaDB databases and tars up a couple of directories, uploads both to an S3-compatible bucket with rclone, and then deletes backups in that bucket older than a retention window. Its purpose is a hands-off off-site copy that prunes itself so the bucket doesn't grow forever.
- Privileges: the backup script reads
/etcand web roots and writes to/var/backups, so it runs as root via a systemd service.rclone configand the timer commands also need root (orsudo). - This deletes data, and that part is irreversible. The retention step runs
rclone delete --min-age, which permanently removes objects from the bucket. Object versioning on the S3 side is your only safety net; without it, a deleted backup is gone. Read the whole script, and run the delete command with--dry-runfirst so you can see exactly what it would remove before it removes anything. - Test first. Point the remote at a throwaway bucket, run the script by hand once, and confirm the objects land and restore cleanly before you enable the timer or trust it on a production box.
- Credentials matter. The MySQL password lives in a file and the S3 keys live in
rclone.conf. Lock both down tochmod 600and root ownership. A world-readable backup config is a breach waiting to happen.
I'm assuming Debian 12 / Ubuntu 22.04, systemd, MariaDB or MySQL on the same host, and rclone v1.60 or newer already installed (rclone version to check). If yours is older, install a current build from the downloads page at rclone.org rather than an ancient distro package. On RHEL/Alma the paths and package names are the same idea; the service manager is identical.
1. Configure the rclone remote
Run this as root so the config lands in root's config directory (/root/.config/rclone/rclone.conf) — that's where the systemd service will look for it.
sudo rclone config
Choose n for a new remote, name it mybackup, pick storage type s3, and select your provider from the list (AWS, Wasabi, Backblaze B2's S3 endpoint, MinIO, or Other for a generic S3-compatible service). You'll be prompted for the access key, secret key, region, and endpoint. Enter them for your provider — the exact endpoint URL comes from your storage vendor's docs, not from me.
When you're done, the config it writes looks roughly like this:
[mybackup]
type = s3
provider = Other
access_key_id = YOUR_ACCESS_KEY
secret_access_key = YOUR_SECRET_KEY
endpoint = s3.example-provider.com
region = us-east-1
Verify it can see the bucket (replace my-bucket with your real bucket):
sudo rclone lsd mybackup:
sudo rclone ls mybackup:my-bucket
2. Store the database credentials
Don't put the DB password on the mysqldump command line. Put it in a defaults file that mysqldump reads with --defaults-extra-file:
sudo mkdir -p /etc/backup
sudo tee /etc/backup/mysql.cnf >/dev/null <<'EOF'
[client]
user=backup
password=REPLACE_WITH_DB_PASSWORD
EOF
sudo chmod 600 /etc/backup/mysql.cnf
Use a dedicated MySQL user with read rights for backups, not root.
3. The backup script
Save this as /usr/local/bin/backup-to-s3.sh. Replace the obvious placeholders: my-bucket, the paths in FILES_SRC, and the retention window if 30 days isn't right.
#!/usr/bin/env bash
set -euo pipefail
# --- settings ---
STAMP="$(date +%Y-%m-%d_%H%M%S)"
WORKDIR="/var/backups/rclone-stage"
REMOTE="mybackup:my-bucket/backups" # bucket + prefix
RETENTION="30d" # delete objects older than this
DB_CNF="/etc/backup/mysql.cnf"
FILES_SRC=(/var/www /etc) # directories to archive
RCLONE_CONF="/root/.config/rclone/rclone.conf"
mkdir -p "$WORKDIR"
DB_DUMP="$WORKDIR/db_${STAMP}.sql.gz"
FILES_TAR="$WORKDIR/files_${STAMP}.tar.gz"
# --- database dump (consistent snapshot for InnoDB) ---
mysqldump --defaults-extra-file="$DB_CNF" \
--single-transaction --routines --triggers \
--all-databases | gzip > "$DB_DUMP"
# --- files archive ---
tar czf "$FILES_TAR" "${FILES_SRC[@]}"
# --- upload today's two files ---
rclone --config "$RCLONE_CONF" copy "$WORKDIR" "$REMOTE" \
--include "*_${STAMP}.*"
# --- clean local staging so the disk doesn't fill ---
rm -f "$DB_DUMP" "$FILES_TAR"
# --- prune old objects in the bucket (irreversible) ---
rclone --config "$RCLONE_CONF" delete "$REMOTE" --min-age "$RETENTION"
Make it executable:
sudo chmod 700 /usr/local/bin/backup-to-s3.sh
A note on --single-transaction: it gives a consistent snapshot for InnoDB tables. If you still run MyISAM tables, that flag doesn't protect them — check the MySQL/MariaDB docs for your engine before trusting the dump.
Before you schedule anything, run it once by hand and watch it:
sudo /usr/local/bin/backup-to-s3.sh
Test the prune separately first. Comment out the last line, run the script for real for a day or two, then run the delete on its own with --dry-run so you can read the list of what it would remove:
sudo rclone --config /root/.config/rclone/rclone.conf \
delete mybackup:my-bucket/backups --min-age 30d --dry-run
Only once that list looks correct should you re-enable the delete line in the script.
4. Schedule it with a systemd timer
Two unit files. First the service, /etc/systemd/system/backup-to-s3.service:
[Unit]
Description=Database and files backup to S3-compatible storage
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-to-s3.sh
Then the timer, /etc/systemd/system/backup-to-s3.timer:
[Unit]
Description=Run S3 backup nightly
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true means if the box was off at 02:30, the job runs at next boot. Enable and start the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now backup-to-s3.timer
I'm using a systemd timer here rather than cron because the logging and status are cleaner; a /etc/cron.d entry calling the same script works fine if you prefer cron.
5. Verify it worked
Check the timer is armed and see when it fires next:
systemctl list-timers backup-to-s3.timer
Trigger the service on demand and read its output:
sudo systemctl start backup-to-s3.service
journalctl -u backup-to-s3.service --no-pager -n 50
Confirm the objects arrived in the bucket, newest first:
sudo rclone --config /root/.config/rclone/rclone.conf \
lsl mybackup:my-bucket/backups
Prove the backup restores — an untested backup is a guess. Pull the latest DB dump to a temp dir and inspect it:
sudo rclone --config /root/.config/rclone/rclone.conf \
copy mybackup:my-bucket/backups /tmp/restore-test \
--include "db_*.sql.gz"
zcat /tmp/restore-test/db_*.sql.gz | head -n 20
Restore into a scratch database or a test host, never straight over production, with gunzip < file.sql.gz | mysql testdb.
Undo and rollback
- Stop the schedule:
sudo systemctl disable --now backup-to-s3.timer. - Remove it entirely: delete the two unit files in
/etc/systemd/system/, runsudo systemctl daemon-reload, then remove/usr/local/bin/backup-to-s3.shand/etc/backup/mysql.cnf. - Remove the remote:
sudo rclone config→ delete themybackupremote. - The bucket contents are the one thing the OS can't undo for you. Deleting objects there is done through
rclone delete/rclone purgeor your provider's console, and it's permanent unless the bucket has versioning enabled. Turn on versioning on the S3 side if you want a recovery path.
For the exact meaning of any flag, the rclone docs at rclone.org cover the s3 backend, the --min-age filter, and the copy/delete commands; consult the MySQL or MariaDB manual for mysqldump options specific to your engine.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
