
September 09, 2026
12 min read
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.
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.
| Criteria | rsync | restic |
|---|---|---|
| Primary use | File sync to local path or remote host | Encrypted snapshot repo on S3/R2 |
| S3/R2 direct upload | No — needs staging or wrapper | Yes — native S3 backend |
| Encryption at rest | Only if destination is encrypted | Built-in AES-256 |
| Deduplication | Block-level delta sync only | Content-defined chunking |
| Retention / pruning | Manual cleanup scripts | forget --prune policies |
| Restore UX | Copy files back | restore by snapshot ID |
| Best for | Hot staging, same-region copy | Long-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.
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.
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.
- 02:15 — MySQL dump to staging
- 02:30 — rsync files to staging
- 02:45 — restic snapshot to R2 or S3
- 03:00 — verify snapshot with
restic snapshots - Sunday 03:00 — restic forget and prune
- 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.
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 --pruneretention 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 (
currentsymlink), 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
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.

