Shore Up
A row of dated wooden crates being sealed and loaded onto a delivery truck bound for a distant warehouse, while a worker removes the oldest crates from a shelf and discards them.
Linux

Scheduled Database and File Backups to S3-Compatible Storage with rclone

Ketan Aagja8 min read
No ratings yet

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 /etc and web roots and writes to /var/backups, so it runs as root via a systemd service. rclone config and the timer commands also need root (or sudo).
  • 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-run first 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 to chmod 600 and 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/, run sudo systemctl daemon-reload, then remove /usr/local/bin/backup-to-s3.sh and /etc/backup/mysql.cnf.
  • Remove the remote: sudo rclone config → delete the mybackup remote.
  • The bucket contents are the one thing the OS can't undo for you. Deleting objects there is done through rclone delete/rclone purge or 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.

Written by
Ketan Aagja

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.

Automate MySQL/MariaDB Backups With Rotation and a Restore Test

Two scripts here. The first ( mysql-backup.sh ) makes a gzipped mysqldump of every database on the server, drops the file in /var/backups/mysql , and deletes any dump older than a retention window. The second ( mysql-restore-test.sh ) proves a backup is usable by loading the newest dump for one database into a throwaway database, counting the tables, and dropping that throwaway again.

9 min read

Automate pfSense and OPNsense Config Backups Off the Box

This guide sets up a read-only pull of the firewall's config.xml to a separate Linux host, scheduled nightly with cron, so you have off-box, dated copies of every configuration. The backup script itself never writes to the firewall — it only copies one file off it over SSH.

9 min read

Automate a Weekly Patch-and-Report Routine for a Small Server Fleet

This sets up a weekly, unattended package upgrade on each server, then emails you a plain-text report of what was upgraded and whether a reboot is now pending. The upgrade step runs apt-get upgrade non-interactively, so it changes installed software on the host. Package upgrades are not cleanly reversible: apt has no "undo the last upgrade" button, so treat this with the same caution as any change to a running server.

8 min read

Automate TLS Cipher and Protocol Scanning of Your Own Services

This guide sets up an automated, repeatable scan of the TLS protocols and cipher suites your own services advertise — the sort of thing you'd otherwise do by hand before a PCI review or after a config change. It uses testssl.sh , a widely-used bash script that connects to a service and negotiates handshakes to see what's on offer (TLS 1.0–1.3, weak ciphers, known issues like ROBOT/BEAST reporting, cert details).

9 min read