Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Offsite Backups to S3 or R2 with rsync and restic

By Kokil Thapa | Last reviewed: September 2026

Local snapshots and nightly database dumps are not a disaster-recovery plan. A disk failure, ransomware event, or bad deploy can wipe the only copy you have. Offsite Backups to S3 or R2 with rsync and restic give you two proven paths: rsync for fast file sync to a staging area or remote host, and restic for encrypted, deduplicated snapshots straight to S3-compatible storage. I use this pairing on Linux production servers I maintain for clients in Nepal and abroad. The goal is simple — copy data away from the server, prove you can restore it, and automate the whole loop.

Why do you need offsite backups to S3 or R2 instead of local copies only?

The 3-2-1 rule still holds in 2026. Keep three copies of important data, on two media types, with one copy offsite. A Laravel app on Ubuntu with MySQL 9.7 can survive a bad migration if you can roll back files and a database dump from yesterday.

Local backups fail in predictable ways. The same power surge kills the server and the USB drive beside it. A compromised root account deletes /var/backups along with the app. I've seen cron jobs point at stale release paths after a Deployer symlink swap — backups ran for weeks and captured nothing useful.

S3 and Cloudflare R2 solve the offsite piece. Both speak the S3 API. R2 often costs less for egress. S3 integrates tightly with AWS IAM and lifecycle rules. Neither replaces your local strategy; they complement it. For a fuller picture, read our Ubuntu server backup strategies guide and the cloud disaster-recovery strategy article.

3-2-1 Backup TopologyProductionUbuntu + MySQLLocal Copyrsync stagingOffsiteS3 or R23 copies · 2 media · 1 offsiterestic encrypts before uploadrsync keeps staging fresh
3-2-1 backup rule: production server, local rsync staging, and encrypted restic snapshots on S3 or R2

On legal-tech portals and eCommerce sites I maintain, offsite copies are non-negotiable. Client documents, order data, and uploaded files must survive hardware loss. A client portal with document sharing cannot rely on a single EC2 volume.

How does rsync compare to restic for S3 and R2 offsite backups?

These tools solve different problems. rsync excels at incremental file sync over SSH or to a local directory. restic excels at encrypted, deduplicated backup repositories on S3-compatible endpoints. Neither tool alone covers every case on a typical LAMP or Laravel stack.

rsync does not speak S3 natively. You rsync to a staging folder or another server, then upload with restic, aws s3 sync, or rclone. restic talks directly to S3 and R2, handles encryption, and tracks snapshot history. For a deep tool comparison, see rsync vs rclone for server backups.

Criteriarsyncrestic
Primary useFile sync to local path or remote hostEncrypted snapshot repo on S3/R2
S3/R2 direct uploadNo — needs staging or wrapperYes — native S3 backend
Encryption at restOnly if destination is encryptedBuilt-in AES-256
DeduplicationBlock-level delta sync onlyContent-defined chunking
Retention / pruningManual cleanup scriptsforget --prune policies
Restore UXCopy files backrestore by snapshot ID
Best forHot staging, same-region copyLong-term offsite archives

My usual pattern: rsync application files and dump output into /var/backups/staging. restic backs that directory — plus anything else needed — to R2 or S3. rsync gives you a fast local mirror for same-day file recovery. restic gives you versioned, encrypted offsite history. Compare storage costs in our Cloudflare R2 vs AWS S3 cost breakdown.

How do you configure rsync for staging before S3 or R2 upload?

Start by defining what must leave the server. On a Laravel 12 or 13 app, that typically includes storage/app, custom upload paths, .env (stored securely), and MySQL dumps. WordPress sites need wp-content/uploads plus a database export.

Create a staging directory with strict permissions

sudo mkdir -p /var/backups/staging/{files,mysql}
sudo chown -R root:backup /var/backups/staging
sudo chmod -R 750 /var/backups/staging

Dump MySQL before file sync

For MySQL 9.7 or MariaDB 12.3, use a consistent dump with single-transaction for InnoDB tables. See also database backup strategies for small servers and MySQL binary logs for replication and backup.

#!/bin/bash
set -euo pipefail
STAGE="/var/backups/staging/mysql"
DB_NAME="app_production"
DB_USER="backup_reader"
DB_PASS="$(cat /root/.mysql-backup-pass)"
DATE=$(date +%Y%m%d-%H%M)

mysqldump --single-transaction --routines --triggers \
  -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" \
  | gzip -9 > "$STAGE/${DB_NAME}-${DATE}.sql.gz"

find "$STAGE" -name '*.sql.gz' -mtime +7 -delete

rsync application files to staging

#!/bin/bash
set -euo pipefail
APP="/var/www/current"
STAGE="/var/backups/staging/files"

rsync -a --delete \
  --exclude 'storage/framework/cache/*' \
  --exclude 'storage/framework/sessions/*' \
  --exclude 'storage/logs/*' \
  --exclude 'node_modules/' \
  "$APP/" "$STAGE/app/"

rsync -a "$APP/.env" "$STAGE/secrets/env.backup"

The --delete flag keeps staging aligned with production. Exclude cache and session dirs — they change constantly and bloat diffs. On sister sites sharing a Deployer 7 pipeline, I always rsync from the current symlink path, not a dated release folder. Details sit in automate server backups with rsync and cron.

rsync Staging PipelineLaravel App/var/www/currentMySQL Dumpmysqldump + gziprsync -a/var/backups/stagingReady forrestic upload
rsync staging flow: application files and MySQL dumps converge in /var/backups/staging before restic pushes offsite

Schedule rsync with cron

# /etc/cron.d/app-backup-staging
15 2 * * * root /usr/local/bin/backup-mysql.sh >> /var/log/backup-mysql.log 2>&1
30 2 * * * root /usr/local/bin/backup-rsync.sh >> /var/log/backup-rsync.log 2>&1

Stagger jobs so the database dump finishes before rsync runs. Overlap causes partial dumps in staging. Log rotation prevents a forgotten log from filling the disk.

How do you set up restic for encrypted offsite backups to R2 or S3?

restic initializes a repository, then creates snapshots. Data is encrypted client-side before upload. The server never sees plaintext. Official docs live at restic.readthedocs.io.

Install restic on Ubuntu 22 or 24

sudo apt update
sudo apt install -y restic
restic version

Configure Cloudflare R2

Create an R2 bucket and an API token scoped to that bucket. R2 uses the S3 API with a custom endpoint. See Cloudflare R2 restic examples for current endpoint formats.

# /root/.restic-env  (chmod 600)
export RESTIC_REPOSITORY="s3:https://ACCOUNT_ID.r2.cloudflarestorage.com/my-backups"
export AWS_ACCESS_KEY_ID="R2_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="R2_SECRET_KEY"
export RESTIC_PASSWORD_FILE="/root/.restic-password"

Generate a strong password with a password generator or openssl rand -base64 32. Store it offline too. Lose the password and the snapshots are unrecoverable.

Configure AWS S3 instead

For S3, point the repository at a bucket and region. Use a dedicated IAM user with least privilege. AWS documents bucket policies at Amazon S3 security best practices.

export RESTIC_REPOSITORY="s3:s3.amazonaws.com/my-company-backups/prod-web-01"
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_DEFAULT_REGION="ap-south-1"

Initialize and run the first snapshot

set -a
source /root/.restic-env
set +a

restic init
restic backup /var/backups/staging \
  --tag nightly \
  --exclude-caches \
  --verbose

restic deduplicates across snapshots. The first run uploads everything. Later runs send only new chunks. That keeps R2 storage and S3 PUT costs manageable on bandwidth-limited Nepal hosting links.

restic Encrypted UploadStaging Dir/var/backupsrestic clientchunk + encryptdedupe packsS3 / R2 Bucketencrypted objectsAES-256 before uploadpassword never sent to cloud
restic chunks, encrypts, and deduplicates staging data before uploading snapshot packs to S3 or Cloudflare R2

Retention and pruning

Snapshots accumulate cost. Define a policy and prune regularly.

restic forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  --prune

The --prune flag removes unreferenced data from the repository. Without it, forgotten snapshots still consume storage. Schedule pruning weekly — it is heavier than a normal backup.

Automate restic after rsync

# /etc/cron.d/app-backup-offsite
45 2 * * * root /usr/local/bin/backup-restic.sh >> /var/log/backup-restic.log 2>&1
0 3 * * 0 root /usr/local/bin/backup-restic-prune.sh >> /var/log/backup-restic.log 2>&1

Wrap scripts with set -euo pipefail and send failure alerts. A silent cron failure is the most common backup gap I see on production servers. For Laravel-specific dumps, Spatie Backup is worth comparing in Laravel Spatie automated database backups.

What backup schedule and monitoring work for production servers?

A nightly window between 02:00 and 04:00 server time suits most South Asia traffic patterns. Dashain and Tihar spikes on retail sites may need a lighter window shift — coordinate with the business calendar.

  1. 02:15 — MySQL dump to staging
  2. 02:30 — rsync files to staging
  3. 02:45 — restic snapshot to R2 or S3
  4. 03:00 — verify snapshot with restic snapshots
  5. Sunday 03:00 — restic forget and prune
  6. First Monday — test restore to a temp directory

Monitor with simple checks, not fancy dashboards. Parse log exit codes. Use restic check monthly to detect repository corruption early. If you host on AWS, our Laravel on EC2, RDS, and S3 guide covers how backup fits the wider stack.

Cost control matters on budget-sensitive projects. R2 free egress helps when you restore often. S3 Intelligent-Tiering or lifecycle rules move old restic packs to cheaper storage classes. Estimate monthly spend before committing — a 50 GB staging set with 30 days of retention often lands around Rs 500–1,500/month (~USD 4–11) on R2, less than a hour of emergency consulting.

Common Backup FailuresStale deploy path in cronWrong AWS regionrestic password lostDisk full in stagingOverlapping dump + rsyncNever tested restoreFix: monthly restore drill
Common offsite backup failures on production servers — most are operational, not tool bugs

On shared EC2 hosts running multiple client sites, isolate staging per site under /var/backups/staging/site-name. Separate restic repositories prevent one client's prune from touching another's snapshot index. Sites like Notary Kathmandu on a shared Deployer pipeline benefit from this separation.

How do you verify and restore offsite backups from S3 or R2?

Backups you never restore are guesses. Run a structured test at least monthly.

List and inspect snapshots

source /root/.restic-env
restic snapshots
restic stats --mode restore-size latest

Restore files to a temp directory

mkdir -p /tmp/restore-test
restic restore latest --target /tmp/restore-test
ls -la /tmp/restore-test/var/backups/staging/files/app/

Validate a database dump

zcat /tmp/restore-test/var/backups/staging/mysql/app_production-*.sql.gz | head
gunzip -c /tmp/restore-test/.../app.sql.gz | mysql -u root test_restore_db

Delete the temp directory after validation. Document restore steps in your runbook so anyone on the team can execute them under stress. For WordPress stacks, cross-check WordPress automated backups with WP CLI if you mix CMS and custom apps on one host.

When disaster hits, restore order matters. Provision a clean server with matching PHP 8.3 or 8.4 and MySQL versions. Pull the latest restic snapshot. Import the database. Point the web root at restored files. Reload PHP-FPM to clear opcache. Verify TLS and cron on the new box. If you need hands-on help, our support and maintenance service covers recovery work.

S3 versioning adds a safety net against accidental overwrites at the bucket level. restic already versioned snapshots, so versioning is optional. MFA delete on production buckets stops a stolen key from wiping history — enable it once your restore drill passes.

Key Takeaways

  • Use rsync to stage application files and MySQL dumps locally; restic pushes encrypted snapshots to S3 or R2.
  • Stagger cron jobs: dump first, rsync second, restic third — never overlap.
  • Store the restic password offline; without it, cloud snapshots are permanent bricks.
  • Apply forget --prune retention weekly so storage costs stay predictable.
  • Run a monthly restore drill to a temp directory and import the database to prove backups work.
  • Point rsync at live deploy paths (current symlink), not stale release folders.

People Also Ask

Can rsync upload directly to S3 or Cloudflare R2?

No. rsync syncs over SSH or to a local filesystem path. To reach S3 or R2, stage files locally and upload with restic, the AWS CLI, or rclone. restic is the better fit when you need encryption, deduplication, and snapshot retention in one tool.

Is restic or restic plus rsync better for a single Ubuntu VPS?

Use both. rsync gives you a fast on-server staging copy for same-day file recovery. restic adds encrypted offsite history with point-in-time snapshots. Together they cover quick restores and true disaster recovery without duplicating upload bandwidth on every file change.

How much do S3 and R2 backups cost for a small Laravel server?

A 20–50 GB staging set with daily snapshots and weekly pruning often costs Rs 300–1,500/month (~USD 2–11) on R2, depending on retention and deduplication ratios. S3 costs vary by region and storage class. R2 wins on egress when you restore frequently.

What should you exclude from offsite backups?

Exclude cache directories, session files, temporary uploads, node_modules, and log files that regenerate quickly. Always include database dumps, user uploads, configuration secrets (encrypted at rest), and any custom storage paths your application depends on.

Build a backup pipeline you can trust

Offsite backups to S3 or R2 with rsync and restic are not exotic DevOps. They are baseline hygiene for any production site that stores client data, orders, or documents. Stage with rsync, snapshot with restic, prune on schedule, and prove restores monthly. That is the difference between a backup script and a recovery plan. If you want this configured on your stack — or a full audit of an existing setup — contact us or explore hosting and domain services. Related reading: automate off-site backups to S3, AWS S3 for Laravel file storage, and the full backup archive on the blog.

Frequently Asked Questions

The 3-2-1 rule still applies in 2026: keep three copies of important data on two media types with one copy offsite. Local backups fail when the same power surge kills the server and a USB drive beside it, or when a compromised root account deletes /var/backups along with the application. I've seen cron jobs point at stale Deployer release paths and capture nothing useful for weeks. S3 and Cloudflare R2 solve the offsite piece through the S3 API. They complement local rsync staging, not replace it. For client portals and eCommerce sites, offsite copies are non-negotiable when documents and order data must survive hardware loss.

rsync excels at incremental file sync over SSH or to a local directory; restic excels at encrypted, deduplicated snapshot repositories on S3-compatible endpoints. rsync cannot speak S3 natively—you stage files locally or on another host, then upload with restic, aws s3 sync, or rclone. restic handles encryption, deduplication, and snapshot history directly against R2 or S3. My usual pattern rsyncs Laravel files and MySQL dumps into /var/backups/staging, then restic backs that directory offsite. rsync gives fast same-day recovery; restic gives versioned encrypted history for true disaster recovery.

No. rsync syncs over SSH or to a local filesystem path. Stage files first, then upload with restic, the AWS CLI, or rclone.

Use both. rsync stages files for same-day recovery; restic adds encrypted offsite snapshots with point-in-time history.

A 20–50 GB staging set with daily snapshots and weekly pruning often costs Rs 300–1,500/month (~USD 2–11) on R2. S3 varies by region and storage class.

Define what must leave the server: on Laravel 12 or 13 apps, that includes storage/app, custom upload paths, and MySQL dumps; WordPress needs wp-content/uploads plus a database export. Create /var/backups/staging with strict permissions (chmod 750, owned by root:backup). Dump MySQL 9.7 or MariaDB 12.3 with mysqldump --single-transaction before file sync. rsync from /var/www/current—the Deployer symlink—not stale release folders. Use --delete on file sync but exclude cache, sessions, logs, and node_modules. Stagger cron: dump at 02:15, rsync at 02:30, so partial dumps never reach staging.

Install restic on Ubuntu 22 or 24 via apt, then define RESTIC_REPOSITORY, AWS credentials, and RESTIC_PASSWORD_FILE in /root/.restic-env (chmod 600). For R2, create a bucket and API token scoped to that bucket with the custom endpoint URL. For S3, set AWS_DEFAULT_REGION and a dedicated IAM user with least privilege. Run restic init once, then restic backup /var/backups/staging with tags and --exclude-caches. Store the restic password offline—without it, snapshots are unrecoverable. Schedule backup after rsync completes and run restic forget with --keep-daily, --keep-weekly, --keep-monthly, and --prune weekly.

Exclude paths that change constantly and regenerate without data loss: Laravel cache and session directories under storage/framework, storage/logs, node_modules, and temporary upload folders. WordPress and WooCommerce sites should skip similar volatile paths while always including wp-content/uploads, database dumps, user-generated files, and securely stored configuration such as .env copies. Including cache and logs bloats rsync diffs and inflates restic storage costs without improving recovery ability. Build your exclude list from application behavior, not defaults copied from another stack.

A nightly window between 02:00 and 04:00 server time suits most South Asia traffic. Stagger jobs: MySQL dump at 02:15, rsync at 02:30, restic at 02:45, snapshot verification at 03:00. Run restic forget and prune on Sunday at 03:00; test a restore on the first Monday. Monitor with log exit codes, not fancy dashboards—a silent cron failure is the most common backup gap I see. Run restic check monthly to catch repository corruption. Retail sites may shift the window during Dashain and Tihar traffic spikes.

Run a structured restore test at least monthly. Source /root/.restic-env, run restic snapshots and restic stats, then restic restore latest --target /tmp/restore-test. Validate files under the staging path and test a database dump with zcat or gunzip piped to mysql on a test database. Delete the temp directory afterward and document steps in a runbook. During disaster recovery, provision a clean server with matching PHP 8.3 or 8.4 and MySQL versions, pull the latest snapshot, import the database, point the web root at restored files, reload PHP-FPM, and verify TLS and cron.

Encrypted restic snapshots become permanent bricks without the password—there is no vendor reset or key recovery. restic encrypts client-side before upload, so Cloudflare R2 and AWS S3 only store ciphertext. Generate the password with openssl rand -base64 32 or a password generator, save it in RESTIC_PASSWORD_FILE with chmod 600, and keep a separate offline copy with your disaster-recovery documentation. Treat it with the same urgency as your database root credentials.

Most failures are operational, not tool bugs. Silent cron failures leave you without recent snapshots—wrap scripts with set -euo pipefail and alert on non-zero exit codes. Overlapping jobs cause partial MySQL dumps in staging when rsync runs before mysqldump finishes. Backups pointing at stale Deployer release folders instead of /var/www/current capture nothing useful for weeks. Forgotten restic prune lets storage costs climb. On shared EC2 hosts, mixing client data in one repository risks one prune affecting another site's index—isolate staging per site under /var/backups/staging/site-name.

Deployer 7 uses zero-downtime symlinked releases; the live application lives at /var/www/current, not inside dated release directories. After a deploy, an rsync job targeting an old release path backs up stale code while production runs elsewhere. I've seen this on sister sites sharing a Deployer pipeline—cron ran for weeks and captured nothing useful. Always rsync from the current symlink so staging mirrors what actually serves traffic before restic pushes offsite.

Snapshots accumulate cost without pruning. Run restic forget with --keep-daily 7, --keep-weekly 4, --keep-monthly 6, then --prune to remove unreferenced chunks from the repository. Without --prune, forgotten snapshots still consume storage. Schedule pruning weekly on Sunday—it is heavier than a normal backup run. On budget-sensitive projects, combine retention policies with R2 free egress or S3 Intelligent-Tiering and lifecycle rules for older restic pack files. A 50 GB staging set with 30 days of retention often lands around Rs 500–1,500/month (~USD 4–11) on R2.

S3 versioning adds a safety net against accidental bucket-level overwrites, but restic already maintains versioned snapshots in the repository. Versioning is optional rather than required. For production buckets, enable MFA delete once your restore drill passes—a stolen access key could otherwise wipe backup history at the storage layer even when restic snapshots exist.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: