Secure and Automated Backups with Borg and Rclone

Having a robust backups strategy in place is not a luxury, it’s a must. because this I wrote this post to explain how to implement a professional, automated, backup for a WordPress site using Borg for fast, deduplicated, local backups and Rclone for encrypted offsite recovery. Moreover, the same holds true for any web app: keep both your application files and your database encrypted, with backups both on-site and off-site.

This system is built on three core principles:

  1. Why a Local Backup on a Separate Volume? Speed and resilience. A local backup on a separate physical or logical volume ensures that you can restore service incredibly quickly. It also protects your backups from filesystem corruption, accidental deletion, or disk failure on your primary server volume.
  2. Why BorgBackup? Efficiency and security. Borg provides client-side encryption, ensuring your data is secure before it’s even written to the backup volume. Its powerful deduplication means that only new, changed data “chunks” are stored, saving immense amounts of disk space and making subsequent backups extremely fast.
  3. Why Rclone for Offsite Backup? Disaster recovery and universal compatibility. Rclone acts as a universal translator for cloud storage. It allows us to take our secure Borg archives and transfer them, with an additional layer of zero-knowledge encryption, to a low-cost object store like AWS S3. If the entire server or data center is compromised, this offsite copy is the ultimate failsafe.

Part 1: Fast & Efficient Local Backups with Borg

Installation and Setup

First, ensure your server’s package list is up to date and install Borg.

sudo apt update
sudo apt install borgbackup

Next, confirm your backup volume is mounted and persistent. Use df -hT to check your filesystems. You should see your primary disk and your separate backup volume. I used hetzner, so after adding the volume you can check the ID of the volume in hetzner dashboard with the ID of the volum in the fstab.

df -hT

To ensure the volume remounts after a reboot, verify it has a proper entry in /etc/fstab.

# Example fstab entry
/dev/disk/by-id/scsi-0HC_Volume_103775914 /mnt/HC_Volume_103775914 ext4 discard,nofail,defaults 0 0

Initializing the Repository

Create a directory for the backup repository and initialize it with Borg. We will use repokey encryption, which is highly secure.

mkdir /borgbackup
borg init --encryption=repokey /borgbackup

Borg will prompt you to create a strong passphrase. This is the password you will use to access the backups.

CRITICAL: Export and Secure Your Encryption Key The repository is protected by your passphrase and a key file stored inside the repository. For true disaster recovery, you must export this key and store it in a completely separate, safe location. Without both the key and your passphrase, the data is unrecoverable.

borg key export /borgbackup

Backing Up WordPress Files

Now, create your first backup. This command backs up the /var/www/html directory, shows verbose progress, and uses zstd compression for a great balance of speed and size reduction.

borg create --stats --progress -v -C zstd,6 /borgbackup::'wp-{now:%Y-%m-%d-%H-%M}' /var/www/html

-C zstd,6: This tells Borg to compress the data. zstd,6 is a modern compression method that offers a great balance between speed and compression ratio.

The output will show a detailed report, including the compression ratio.

------------------------------------------------------------------------------
                       Original size      Compressed size    Deduplicated size
This archive:               74.92 MB             27.77 MB             27.67 MB
All archives:               74.92 MB             27.76 MB             27.85 MB
------------------------------------------------------------------------------

Restoring Files

To restore, first list the available archives.

borg list /borgbackup

To perform a full restore, create a temporary directory, extract the archive, and then copy the files back into place.

# Create a temporary location for the restore
mkdir /tmp/restore
cd /tmp/restore

# Extract the desired archive
borg extract /borgbackup::wp-2025-10-20-13-08

# Copy the files back, preserving all permissions and attributes
cp -a /tmp/restore/var/www/html/. /var/www/html/

# Clean up the temporary directory
rm -rf /tmp/restore

Part 2: Securing the WordPress Database

A complete backup requires both files and the database.

Dumping the Database

First, get your database credentials from your wp-config.php file.

# Run these from your /var/www/html directory
grep DB_NAME wp-config.php
grep DB_USER wp-config.php
grep DB_PASSWORD wp-config.php

Ensure your database user has the PROCESS privilege, which is required for a consistent dump with mysqldump.

-- Connect to MySQL as root
GRANT PROCESS ON *.* TO 'your_wp_user'@'localhost';

Now, create a backup directory and dump the database to a timestamped .sql file.

mkdir /dbbackup
mysqldump -u [USERNAME] -p[PASSWORD] [DATABASE_NAME] > /dbbackup/$(date +\%Y-\%m-\%d_%H-%M-%S)_wpdb.sql

Restoring the Database

Restoring is done by importing the .sql file back into the database.

mysql -u [USERNAME] -p[PASSWORD] [DATABASE_NAME] < /path/to/your_dump_file.sql

Part 3: Offsite Disaster Recovery with Rclone and AWS S3

Step 1: Configure AWS S3

  1. Create an S3 Bucket: In the AWS Console, create a new S3 bucket in a geographic region far from your server. in a different continent is much better.
  2. Create an IAM Policy: Create a policy that grants the minimum required permissions to access only that specific bucket.
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "s3:ListBucket",
                    "s3:GetBucketLocation"
                ],
                "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME-HERE"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "s3:PutObject",
                    "s3:GetObject",
                    "s3:DeleteObject"
                ],
                "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME-HERE/*"
            }
        ]
    }
    
  3. Create a Dedicated IAM User: Create a new IAM user (e.g., rclone-uploader), attach the policy you just created, and generate an access key. Securely store the Access Key ID and Secret Access Key.

Step 2: Install and Configure Rclone

Install Rclone using the official script.

curl https://rclone.org/install.sh | sudo bash

Run the configuration utility. We will create two “remotes”: one that points to S3, and a second “crypt” remote that wraps the first one to provide encryption.

rclone config
  1. Create s3-raw remote:
    • n for a new remote.
    • Name: s3-raw
    • Storage: s3
    • Provider: Amazon Web Services S3
    • env_auth: false
    • Enter the Access Key ID and Secret Access Key you saved from AWS.
    • Select the correct region.
    • Accept defaults for the remaining options.
  2. Create s3-encrypted remote:
    • n for a new remote.
    • Name: s3-encrypted
    • Storage: crypt
    • Remote to encrypt: s3-raw:YOUR-BUCKET-NAME-HERE/encrypted-data
    • Filename encryption: 2 (Obfuscate)
    • Create and confirm a very strong password for the remote.
    • Create and confirm a different strong password for the salt.
    • Accept defaults and quit.

Why is it designed this way?

This “layered” approach is incredibly powerful and flexible:

  • Modularity: What if you decide to stop using Amazon S3 and switch to Google Drive? You don’t have to re-encrypt anything. You just create a new “Post Office” remote for Google Drive (gdrive-raw) and then edit your s3-encrypted remote to point to gdrive-raw instead of s3-raw. Your encryption layer remains the same.
  • Simplicity: The Rclone developers only had to write the encryption logic once (in the crypt remote). They didn’t need to add separate encryption code for all 70+ cloud providers.

Step 3: The First Encrypted Sync

Use rclone sync to transfer your local backup directories to your new encrypted S3 remote. The -P flag shows progress.

# Sync the Borg repository
rclone sync -P /borgbackup s3-encrypted:borg

# Sync the database backups
rclone sync -P /dbbackup s3-encrypted:db

You will see a folder named encrypted-data. Inside S3, you will find files and folders with completely random, meaningless names. This proves your data is encrypted before it ever touches Amazon’s servers.

rclone ls s3-encrypted:

Part 4: Full Automation with a Cron Job

Step 1: Create Secure Credential Files (Crucial!)

Never store passwords directly in scripts. Create dedicated, permission-restricted files.

For MySQL:

# Create and open the file
nano /root/.my.cnf

# Add the following content:
[mysqldump]
user = your_wp_user
password = "YOUR_DATABASE_PASSWORD_HERE"

# Set strict permissions so only root can read it
chmod 600 /root/.my.cnf

For Borg:

# Put your passphrase in the file
echo "YOUR_BORG_PASSPHRASE_HERE" > /root/.borg_passphrase

# Set strict permissions
chmod 600 /root/.borg_passphrase

Step 2: The Backup Script

Save the following script as /usr/local/bin/daily_backup.sh. This script automates every step we performed manually.

#!/bin/bash

Features of the script

  • Comprehensive Backup: Backs up both the entire WordPress file directory and the MySQL database.
  • Secure & Encrypted:
    • Database credentials are not stored in the script; they are read from a secure .my.cnf file.
    • Borg passphrase is not stored in the script; it is read from a restricted file in /root/.borg_passphrase.
    • Backups are encrypted locally using BorgBackup.
  • Efficient Storage: Uses BorgBackup for deduplicated backups, saving significant storage space over time.
  • Automated Offsite Sync: Securely syncs the encrypted backup repository to a cloud storage provider using Rclone.
  • Intelligent Retention Policy:
    • Borg Archives: keeps one daily backup for 7 days, one weekly backup for 4 weeks, and one monthly backup for 6 months. This provides a rich history without using excessive space.
    • Raw SQL Dumps: The local SQL dump files are automatically deleted after 30 days to save local disk space.
  • Robust Logging & Error Handling: The script logs all its actions and will stop immediately if any command fails.
  • Unique, Timestamped Backups: Every backup run creates a unique, timestamped archive, preventing overwrites and allowing for multiple backups per day.

The full code in github.

Remember to make the script executable:

chmod +x /usr/local/bin/daily_backup.sh

Step 3: Scheduling with Cron

Finally, automate the script to run daily using cron.

# Open the root user's crontab
crontab -e

Add the following line to run the script at 2:00 AM every morning and log the output.

0 2 * * * /usr/local/bin/daily_backup.sh > /var/log/daily_backup.log 2>&1

Conclusion

You now have a fully automated enterprise-grade backup solution protecting your precious data, including encryption and offsite storage, and we’ve leveraged Borg’s smart retention policies, compression and deduplication to make backups efficient, while using Rclone to securely push copies offsite to AWS S3 storage provider. However, by Rclone you have access to over 70 cloude-native storage soltions.

Ali Alrahbe
Ali Alrahbe

Hi, 👋 I'm Ali Alrahbe, a cybersecurity professional passionate about building cloud infrastructures that are both secure and resilient.

I got my start in tech on the front lines of IT support. That experience didn't just teach me how to solve complex problems—it showed me that proactive security is the bedrock of any successful digital system. That realization drove me to specialize in cloud security.
I'm AWS Certified Solutions Architect Associate, I hold a Bachelor's degree in computer systems engineering and currently pursuing a Master's in Cybersecurity in Berlin, focusing on Cloud Security, DevSecOps, and Infrastructure as Code (IaC).

On my website, Corefortify.com, I document my journey, share hands-on projects, and break down complex security concepts in the evolving world of cloud technology.

Feel free to connect with me on LinkedIn!

Articles: 14