← Blog
The Complete Backup Strategy for Self-Hosters: 3-2-1, Testing, and What to Do When It All Goes Wrong

Everyone with a server knows they should back up. Most people even have something in place. What separates real backup strategies from theatre is whether you've ever successfully restored under pressure. The moment you actually need to recover, you find out whether your setup works or whether you've been paying for false comfort.

This guide is for sysadmins and small operations teams running production workloads. It covers the 3-2-1 rule (and why the common interpretation is often wrong), how to build a working Restic-based backup pipeline, how to test restores properly, and what to actually do when something has gone wrong. The goal isn't to explain what backups are. It's to give you a strategy that holds up when you need it.

What most backup setups get wrong

Before getting into the how, it's worth naming the failure patterns that show up over and over in real recovery scenarios.

The untested backup. You have nightly snapshots going somewhere. You've never tried to restore a full system from them. You don't know how long it takes. You don't know if the destination is still writable. You don't know if the encryption key is still where you think it is. This is the single most common failure mode.

The onsite-only backup. Your backups live on the same server, or on a NAS in the same rack, as the systems they protect. A fire, a flood, a ransomware event that pivots through your network, or a full-site power incident takes both out at once. Your backups need to be somewhere else, physically.

The single-copy backup. One backup is one bit-flip away from a corrupt archive. Modern backup tools verify integrity, but verification catches corruption. It doesn't fix it. You need multiple copies.

The "hot" backup. Rsyncing a running database is a great way to end up with a backup that won't start. Backups of stateful services need coordination with the service (database dumps, LVM snapshots, or filesystem-level consistency).

The forever-retained backup. Some organizations keep every backup ever, forever. This is expensive and it means when something is compromised, the malicious version is preserved alongside the good one. You want retention policies that discard old data and keep the recent history you actually need.

The credentials that live on the machine being backed up. If your only copy of the S3 credentials or the encryption passphrase is on the server that just died, you can't restore. Credentials live in a password manager, on paper in a safe, or split across multiple physically separated locations. They don't live only on the machine.

Every one of these failure modes has taken out real production systems. The strategy in this guide addresses them directly.

The 3-2-1 rule, correctly interpreted

The 3-2-1 rule is the shortest useful summary of a real backup strategy:

  • 3 copies of your data
  • 2 different storage media
  • 1 copy offsite

Where most implementations go wrong is treating "copies" and "media" as synonymous with "different files". Three files on the same disk is not three copies. Two folders on the same NAS is not two media.

A correct implementation:

  • Copy 1: your production data on the running server
  • Copy 2: local backup on separate physical storage (a different disk, NAS, or dedicated backup server)
  • Copy 3: offsite backup on completely different infrastructure (a different provider, a different geographic location, or both)

The offsite copy is what saves you from ransomware, physical disasters, or a provider-wide incident. The local copy is what makes routine restores fast. The production data is what you use every day. All three serve different failure modes.

Modern practice adds a fourth expectation: 1 tested restore. If you haven't restored from a backup in the last quarter, you don't have a backup. You have a bet.

The tools: Restic and Borg

For self-hosters and small operations teams, two backup tools dominate the space. Both are mature, both are actively maintained, both support the workflow this guide describes.

Restic is the primary tool this guide uses. It's written in Go, ships as a single binary, and works out of the box with S3-compatible object storage, SFTP, local paths, and dozens of other backends. It handles deduplication, encryption, integrity checking, and retention policies natively. Restic's ecosystem includes wrappers like resticprofile and autorestic that simplify complex configurations.

Borg is the mature alternative. Older, slightly faster on some workloads, with excellent deduplication. Its major limitation is that it doesn't natively support object storage as a destination. You need rclone or a filesystem mount in between. For self-hosters who prefer to back up to SFTP or a directly-attached filesystem, Borg is excellent. For anyone using S3-compatible storage as their offsite target, Restic is simpler.

Both encrypt data client-side before it leaves the machine. Both deduplicate at the block level. Both have solid integrity checking. Pick either and stick with it. Mixing them is possible but adds complexity for no real gain.

The rest of this guide uses Restic in examples. The concepts translate directly to Borg with minor syntax differences.

Setting up Restic

Install Restic on the machine you want to back up. On Debian and Ubuntu:

apt install restic -y

Verify the version:

restic version

You want 0.17 or newer. If your distro ships an older version, download the latest binary from the Restic GitHub releases page and drop it in /usr/local/bin/.

Choosing a repository backend

Restic calls the destination for your backups a "repository". Where you put the repository is your first architectural decision, and it drives everything else.

Local disk: fastest, cheapest, but violates 3-2-1 on its own (it's the same physical location as the source). Useful as your fast local copy for routine restores.

A dedicated backup server on your network: separate physical hardware, fast to read and write, useful for both routine restores and disaster recovery within your own infrastructure. Combine it with an offsite copy to satisfy 3-2-1.

SFTP to a remote server: works well when you have another server in a different location. Restic connects over SSH and writes the repository as files on the remote filesystem.

S3-compatible object storage: the most common offsite destination in 2026. Any provider offering an S3-compatible API works with Restic natively, without any additional tooling. This includes both hyperscaler storage services and dedicated backup providers with cheaper long-term pricing.

For most sysadmins running a small operation, the practical answer is to run two Restic repositories: one on a local NAS or backup server for fast restores, one on S3-compatible object storage for the offsite copy. Both are updated on the same schedule from the same source.

Initializing your repositories

Restic uses a passphrase to encrypt everything. Choose a strong one and store it in your password manager. If you lose it, your backups are unrecoverable. This is the point.

For a local repository:

export RESTIC_REPOSITORY=/mnt/backup/restic-repo
export RESTIC_PASSWORD_FILE=/root/.restic-password
echo "your-strong-passphrase-here" > /root/.restic-password
chmod 600 /root/.restic-password

restic init

For an S3-compatible remote repository:

export RESTIC_REPOSITORY="s3:https://your-s3-endpoint/bucket-name"
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export RESTIC_PASSWORD_FILE=/root/.restic-password

restic init

The s3: prefix works with any S3-compatible endpoint. The URL after it tells Restic where to connect.

For long-term storage, use a service-account credential with permissions scoped to only the backup bucket. If those credentials leak, the damage is limited to that one bucket.

Backing up

Once your repository is initialized, backing up is a single command:

restic backup /etc /home /var/www /var/lib/docker/volumes

The first backup is slow because it uploads everything. Subsequent backups only send changed blocks thanks to deduplication and are typically an order of magnitude faster.

Restic outputs a summary at the end:

Files:        1234 new, 0 changed, 0 unmodified
Dirs:         89 new, 0 changed, 0 unmodified
Added to the repository: 4.523 GiB (2.891 GiB stored)
processed 1234 files, 4.523 GiB in 3:12
snapshot abc123def4 saved

The snapshot ID at the end is what you'll use to restore.

Backing up stateful services correctly

Rsyncing a running database file gives you a corrupt backup. This bites people every year.

For each stateful service you run, use its own dump or snapshot mechanism, then back up the dump file.

PostgreSQL:

pg_dump -U postgres -Fc mydb > /var/backups/mydb-$(date +%Y%m%d).dump

MySQL / MariaDB:

mysqldump --single-transaction --routines --triggers mydb | gzip > /var/backups/mydb-$(date +%Y%m%d).sql.gz

MongoDB:

mongodump --db mydb --out /var/backups/mongodb-$(date +%Y%m%d)/

Redis:

redis-cli BGSAVE
# then back up /var/lib/redis/dump.rdb

Docker volumes with running containers: for stateless volumes, back up the volume path directly. For stateful containers (databases, queues), use the tool's dump mechanism from inside the container, write the dump to a bind-mounted host directory, then back up that directory.

The pattern is the same in every case: get a consistent point-in-time file, then back up that file. Never back up a live database's on-disk files directly.

Automating the backup pipeline

A backup that requires you to remember to run it is not a backup. Automate everything through systemd or cron.

Create /usr/local/bin/backup.sh:

#!/bin/bash
set -euo pipefail

# Timestamp for logs
DATE=$(date --iso-8601=seconds)
echo "[$DATE] Starting backup"

# Dump databases first
pg_dumpall -U postgres | gzip > /var/backups/postgres-all.sql.gz
mysqldump --all-databases --single-transaction --routines --triggers | gzip > /var/backups/mysql-all.sql.gz

# Local repository
export RESTIC_REPOSITORY=/mnt/backup/restic-repo
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic backup \
    --tag daily \
    /etc /home /var/www /var/backups /var/lib/docker/volumes

# Retention on local repo: keep 7 daily, 4 weekly, 6 monthly
restic forget --prune \
    --keep-daily 7 \
    --keep-weekly 4 \
    --keep-monthly 6

# Offsite repository
export RESTIC_REPOSITORY="s3:https://your-s3-endpoint/bucket-name"
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
restic backup \
    --tag daily \
    /etc /home /var/www /var/backups /var/lib/docker/volumes

# Retention on offsite repo: keep more history
restic forget --prune \
    --keep-daily 14 \
    --keep-weekly 8 \
    --keep-monthly 12 \
    --keep-yearly 3

echo "[$DATE] Backup complete"

Make it executable:

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

Create a systemd service at /etc/systemd/system/backup.service:

[Unit]
Description=Nightly backup
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
StandardOutput=journal
StandardError=journal

And a timer at /etc/systemd/system/backup.timer:

[Unit]
Description=Nightly backup timer

[Timer]
OnCalendar=03:00
Persistent=true
RandomizedDelaySec=15min

[Install]
WantedBy=timers.target

Enable it:

systemctl daemon-reload
systemctl enable --now backup.timer

Backups now run every night at 03:00 (plus up to 15 minutes of jitter to avoid load spikes across many servers).

Check status with systemctl status backup.timer and follow logs with journalctl -u backup.service -f.

Verifying backups (not just running them)

Running the backup is only half the job. Restic can verify the integrity of its repositories, and you should schedule that separately.

Add to your weekly cron or systemd timer:

restic check --read-data-subset=10%

This reads 10% of the data blocks and verifies their checksums. Full data verification (--read-data) is expensive on large repositories. A rolling 10% subset each week covers the whole repository over ten weeks, which is a reasonable trade-off between cost and detection.

If restic check reports errors, don't panic and don't overwrite the healthy backup. Investigate first. Sometimes it's a transient network issue. Sometimes it's a real corruption you need to address before it spreads.

The restore test (the one thing everyone skips)

A backup you have never restored is a hope, not a strategy. Test the restore quarterly at minimum, monthly if you can afford the time.

The full test procedure:

  1. Provision a fresh server that resembles your production environment
  2. Install Restic and configure the same credentials
  3. Restore a recent snapshot to that server:
restic restore latest --target /
  1. Verify the restored system boots
  2. Verify the services start
  3. Verify the databases have current data
  4. Verify the application actually works end-to-end
  5. Time the whole process from bare server to functioning system

The last point matters more than most people think. Knowing you can restore in 30 minutes versus 12 hours changes your incident response completely.

Document the exact procedure. When you actually need to restore, at 3 AM under stress, you want a runbook, not a memory quiz.

Handling ransomware and adversarial scenarios

The reason offsite backups exist is that adversaries who compromise your infrastructure will attempt to destroy your backups. Modern ransomware specifically hunts for and destroys backup repositories before deploying the encryption payload.

Restic has two protective features for this:

Append-only mode. Restrict the credentials used by your backup job so they can only add data to the repository, not delete or modify existing data. For S3-compatible storage, this is enforced through IAM policies scoped to s3:PutObject and s3:GetObject on the specific bucket, without s3:DeleteObject. The restic forget --prune step needs to run from a separate machine with elevated credentials, not from the machine being backed up.

Object lock (S3-compatible immutability). Many object storage providers support setting a retention period during which objects cannot be deleted, even by administrators. Enable this on your backup bucket with a retention period matching your recovery objective (7, 14, or 30 days).

Together these mean that even if your production server is fully compromised and its credentials extracted, the attacker cannot destroy the backups they need to hold you hostage.

What to do when a restore is needed

Backups exist for two categories of events: routine operational recovery (someone deleted a file, a service ate its data) and disaster recovery (the whole server is gone or compromised). Handle them differently.

Routine recovery (single file, a database dump, a config that got clobbered):

# List snapshots
restic snapshots

# Restore a single file from the latest snapshot
restic restore latest --target /tmp/restore --include /etc/nginx/nginx.conf

# Or restore a whole directory
restic restore abc123 --target /tmp/restore --include /var/www/site

Restore to a temporary directory first, verify the content, then move the files where they need to go. Never restore directly over production without checking.

Disaster recovery (full server rebuild):

  1. Provision a fresh server matching the target specification
  2. Install base packages (SSH, minimal utilities)
  3. Install Restic and configure repository credentials
  4. Restore configuration files: restic restore latest --target / --include /etc
  5. Install and start services matching the restored configs
  6. Restore application data: restic restore latest --target / --include /var/www --include /var/lib/docker/volumes
  7. Restore database dumps to their appropriate location
  8. Import the database dumps into a fresh database instance
  9. Update DNS if the new server has a different IP
  10. Verify end-to-end functionality before declaring the restore complete

Document this sequence for your specific stack. When you're actually in a recovery scenario, the runbook eliminates decision-making at exactly the wrong time.

Monitoring backups

A backup that silently fails for a month is worse than no backup at all. Monitor them.

The simplest setup uses Uptime Kuma with a push monitor. Add this to the end of your backup script:

curl -fsS -m 10 --retry 5 -o /dev/null https://your-uptime-kuma/api/push/YOUR_TOKEN?status=up

Uptime Kuma expects a ping from your backup job at least once per configured interval. If it doesn't hear from you in 25 hours (for a nightly job), it triggers an alert.

For teams, escalate on failure to a real notification channel (Slack, Discord, email, PagerDuty). A monitoring alert that only shows up in a dashboard nobody looks at is not monitoring.

Cost and infrastructure implications

Backup storage sits in a different tier from production storage. It's cold, written once, read rarely. Object storage is priced accordingly, but only if you use the right tier and lifecycle policies.

Typical costs for backup storage in 2026 are in the range of a few euros per terabyte per month for standard object storage, and a fraction of that for archive tiers if you're willing to accept slower retrieval times. For most self-hosters and small operations, backup storage costs sit in the low tens of euros per month for the range of data volumes involved.

For the local copy, a dedicated backup server or NAS with ample disk capacity is usually the right investment. On Dedimax dedicated servers, the storage-optimized configurations are a good match for both running production services and backing them up locally. All plans include unlimited bandwidth, which matters when initial backup uploads can move hundreds of gigabytes.

A working strategy in one page

If you take nothing else from this guide, this is the summary version:

  • Run backups nightly via a systemd timer or cron. Don't rely on manual triggers.
  • Back up to two repositories: one local (fast restores), one offsite on S3-compatible storage (disaster recovery).
  • Encrypt everything with Restic. Store the passphrase somewhere separate from the source system.
  • Dump stateful services (databases, queues) before archiving. Never back up live database files.
  • Apply retention policies: 7 daily, 4 weekly, 6 monthly on local; longer history offsite.
  • Verify integrity weekly with restic check --read-data-subset=10%.
  • Test full restores quarterly. Document the procedure. Time the full recovery.
  • Use append-only credentials and S3 object lock on the offsite repository to survive ransomware.
  • Monitor backup completion with push alerts to your uptime monitoring.
  • Store credentials, encryption passphrases, and runbooks somewhere physically separate from the systems they protect.

Backups are not exciting work. They rarely justify their cost until the day they save your organization. The strategy above is what real recovery looks like when it works. Set it up once, test it, then set a calendar reminder to test it again next quarter.

Continue reading

Crear una cuenta Acceder a mi cuenta

Sin compromiso, despliegue en segundos

Zona comunitaria

Una pregunta ?
¿Quieres ir más lejos?

Te esperamos en nuestro blog. Guías y tutoriales publicados regularmente (sysadmin, gaming, devops...) !

Permítame verificar
DEDIMAX DEDIMAX DEDIMAX DEDIMAX
DEDIMAX

¿Necesita una cotización?

Escribenos !

Contáctenos

Prendre contact