1. Set Up rclone for the Bucket
Create a bucket (for example db-backups) in your dashboard, then as root:
sudo -i curl https://rclone.org/install.sh | bash rclone config create backups s3 provider=Other endpoint=https://s3.us-west.virteche.com region=us-west \ access_key_id=YOUR_KEY secret_access_key=YOUR_SECRET force_path_style=true no_check_bucket=true rclone lsd backups:
2. The Backup Script
Save this as /usr/local/sbin/db-backup-to-s3. Keep the block for your database and delete the other:
#!/bin/bash set -euo pipefail STAMP=$(date +%F-%H%M) DIR=/var/backups/db DEST="backups:db-backups/$(hostname)" RCLONE="rclone --config /root/.config/rclone/rclone.conf" mkdir -p "$DIR" # MySQL / MariaDB: every database in one file mysqldump --all-databases --single-transaction --routines --triggers --events \ | gzip > "$DIR/mysql-$STAMP.sql.gz" # PostgreSQL: one file per database for db in $(runuser -u postgres -- psql -Atc "SELECT datname FROM pg_database WHERE NOT datistemplate"); do runuser -u postgres -- pg_dump -Fc "$db" > "$DIR/pg-$db-$STAMP.dump" done $RCLONE copy "$DIR" "$DEST" $RCLONE delete "$DEST" --min-age 30d # keep 30 days in the bucket find "$DIR" -type f -mtime +2 -delete # and 2 days on the server
chmod 700 /usr/local/sbin/db-backup-to-s3 /usr/local/sbin/db-backup-to-s3 rclone ls backups:db-backups
pipefail makes the script stop if mysqldump fails, rather than uploading an empty file. On Debian and Ubuntu, root can sign in to MariaDB and MySQL without a password; elsewhere, put the credentials in /root/.my.cnf with permissions 600.
3. Run It Every Night
# /etc/systemd/system/db-backup.service [Unit] Description=Database backup to S3 Wants=network-online.target After=network-online.target [Service] Type=oneshot ExecStart=/usr/local/sbin/db-backup-to-s3 # /etc/systemd/system/db-backup.timer [Unit] Description=Nightly database backup [Timer] OnCalendar=*-*-* 02:30 RandomizedDelaySec=15m Persistent=true [Install] WantedBy=timers.target
systemctl daemon-reload systemctl enable --now db-backup.timer systemctl list-timers db-backup.timer journalctl -u db-backup.service # the last run
4. Prove You Can Restore
Restore into a scratch database, never over the live one:
# MySQL / MariaDB: restore everything into a test server, or pull one database out: rclone copy backups:db-backups/$(hostname)/mysql-2026-10-01-0230.sql.gz /tmp/ gunzip < /tmp/mysql-2026-10-01-0230.sql.gz | mysql # on a test server only # PostgreSQL: restore one database under a new name rclone copy backups:db-backups/$(hostname)/pg-mydb-2026-10-01-0230.dump /tmp/ runuser -u postgres -- createdb mydb_restore_test runuser -u postgres -- pg_restore -d mydb_restore_test < /tmp/pg-mydb-2026-10-01-0230.dump
An --all-databases dump recreates every database and its users when loaded, which is why it belongs on a test server. Check a few tables, then drop the test database.
The Rest of the Server
The database is usually the most important part, but not the only one. Back up uploads, config and code too; How to back up a Linux server with Borg includes /var/backups, where this script leaves the latest dumps. See the 3-2-1 backup rule for how many copies to keep, and where.