Your homelab runs your GitLab, your passwords, your photos, your home automation. What happens when the disk fails?

If you can’t answer that question confidently, you don’t have backups. You have hope.

The 3-2-1 rule has been around for decades because it works. Three copies, two different media, one offsite. Here’s how to actually implement it.

The 3-2-1 Rule Explained

flowchart TD
    subgraph rule["3-2-1 Backup Rule"]
        Data["Original Data"]

        subgraph three["3 Copies"]
            C1["Copy 1<br/>(Original)"]
            C2["Copy 2<br/>(Local Backup)"]
            C3["Copy 3<br/>(Offsite)"]
        end

        subgraph two["2 Media Types"]
            M1["NVMe/SSD"]
            M2["HDD/NAS"]
        end

        subgraph one["1 Offsite"]
            Off["Cloud/Remote"]
        end
    end

    Data --> C1
    Data --> C2
    Data --> C3
    C1 --> M1
    C2 --> M2
    C3 --> Off

Why Three Copies?

  • Copy 1: Your live data (original)
  • Copy 2: Local backup (fast restore)
  • Copy 3: Offsite backup (disaster recovery)

One copy is not a backup. Two copies can both fail in the same disaster (fire, flood, ransomware). Three copies with separation gives you real resilience.

Why Two Media Types?

Different failure modes:

  • SSDs can fail silently (bit rot)
  • HDDs have mechanical failures
  • RAID is not a backup (protects against drive failure, not data corruption)

Different media means different failure scenarios don’t take out all copies.

Why One Offsite?

Your house can burn down. Your neighborhood can flood. Your entire city can lose power. Offsite means survival even when everything local is gone.

What to Back Up

Critical (Daily Backup)

DataWhyTool
DatabasesCan’t recreatepg_dump, Velero
Secrets/credentialsSecurity criticalVault export, External Secrets
ConfigurationSystem stateGit (already offsite)
Personal filesIrreplaceableRestic

Important (Weekly Backup)

DataWhyTool
Container imagesRebuild takes timeRegistry backup
Persistent volumesStateful workloadsLonghorn/Velero
Logs (compressed)ForensicsLoki snapshots

Rebuildable (Don’t Backup)

  • Base OS (reinstall from ISO)
  • Downloaded packages (re-download)
  • Cached data (regenerates)
  • Temporary files

Don’t waste backup space on data you can recreate.

Backup Tools

Restic: File-Level Backups

Restic is my go-to for file backups. It’s fast, encrypted, deduplicated, and supports multiple backends.

# Initialize repository
restic init --repo /mnt/backup/restic

# Or with S3 backend
restic init --repo s3:s3.amazonaws.com/my-bucket

# Backup a directory
restic backup /home/user/documents

# Backup with exclusions
restic backup /data \
  --exclude="*.tmp" \
  --exclude=".cache" \
  --exclude="node_modules"

# List snapshots
restic snapshots

# Restore
restic restore latest --target /restore/location

Automated Restic Backups

#!/bin/bash
# /usr/local/bin/backup.sh

export RESTIC_REPOSITORY="s3:s3.eu-west-1.amazonaws.com/homelab-backups"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
export AWS_ACCESS_KEY_ID="your-key"
export AWS_SECRET_ACCESS_KEY="your-secret"

# Backup
restic backup /data/important \
  --exclude-caches \
  --tag homelab \
  --tag daily

# Prune old snapshots (keep 7 daily, 4 weekly, 12 monthly)
restic forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 12 \
  --prune

# Check repository integrity
restic check

Cron job:

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

Velero: Kubernetes Backups

Velero backs up Kubernetes resources and persistent volumes.

# Install Velero with S3 backend
velero install \
  --provider aws \
  --plugins velero/velero-plugin-for-aws:v1.8.0 \
  --bucket velero-backups \
  --backup-location-config region=eu-west-1 \
  --secret-file ./credentials-velero \
  --use-volume-snapshots=true \
  --snapshot-location-config region=eu-west-1

Scheduled Backups

apiVersion: velero.io/v1
kind: Schedule
metadata:
  name: daily-backup
  namespace: velero
spec:
  schedule: "0 3 * * *"
  template:
    includedNamespaces:
      - production
      - gitlab
      - monitoring
    excludedResources:
      - events
      - pods
    ttl: 720h  # Keep for 30 days
    storageLocation: default
    volumeSnapshotLocations:
      - default

Restore from Velero

# List backups
velero backup get

# Describe a backup
velero backup describe daily-backup-20260518030000

# Restore entire backup
velero restore create --from-backup daily-backup-20260518030000

# Restore specific namespace
velero restore create --from-backup daily-backup-20260518030000 \
  --include-namespaces gitlab

Longhorn Backups

Longhorn has built-in backup to S3:

# Configure backup target
apiVersion: longhorn.io/v1beta1
kind: Setting
metadata:
  name: backup-target
  namespace: longhorn-system
value: "s3://longhorn-backups@eu-west-1/"
---
apiVersion: longhorn.io/v1beta1
kind: Setting
metadata:
  name: backup-target-credential-secret
  namespace: longhorn-system
value: "longhorn-s3-secret"

Schedule recurring backups:

apiVersion: longhorn.io/v1beta1
kind: RecurringJob
metadata:
  name: daily-backup
  namespace: longhorn-system
spec:
  cron: "0 3 * * *"
  task: backup
  groups:
    - default
  retain: 7
  concurrency: 2

Offsite Options

Cloud Storage

ProviderCostProsCons
Backblaze B2$0.005/GBCheap, S3-compatibleUS-based
Wasabi$0.0059/GBNo egress fees90-day minimum
AWS S3 Glacier$0.004/GBVery cheapSlow retrieval
Hetzner Storage Box€3.81/1TBEU-based, cheapSFTP/WebDAV only

Second Location

If you have a friend/family member with a homelab:

flowchart LR
    subgraph your["Your Home"]
        YourData["Your Data"]
        YourBackup["Their Backup<br/>(encrypted)"]
    end

    subgraph their["Their Home"]
        TheirData["Their Data"]
        TheirBackup["Your Backup<br/>(encrypted)"]
    end

    YourData -->|Encrypted| TheirBackup
    TheirData -->|Encrypted| YourBackup

Mutual offsite backup. Both encrypted so neither can read the other’s data.

Self-Hosted Cloud

Run your own S3-compatible storage at a second location:

# MinIO at remote location
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minio
spec:
  template:
    spec:
      containers:
        - name: minio
          image: minio/minio
          args:
            - server
            - /data
          env:
            - name: MINIO_ROOT_USER
              valueFrom:
                secretKeyRef:
                  name: minio-credentials
                  key: user
            - name: MINIO_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: minio-credentials
                  key: password

Access via Tailscale for security.

Database Backups

PostgreSQL

#!/bin/bash
# Kubernetes PostgreSQL backup

NAMESPACE="gitlab"
POD=$(kubectl get pod -n $NAMESPACE -l app=postgresql -o jsonpath='{.items[0].metadata.name}')
DATE=$(date +%Y%m%d_%H%M%S)

# Dump all databases
kubectl exec -n $NAMESPACE $POD -- \
  pg_dumpall -U postgres | \
  gzip > /backup/postgres_${DATE}.sql.gz

# Upload to S3
restic backup /backup/postgres_${DATE}.sql.gz --tag postgres --tag daily

Vault Backup

# Export Vault data (requires root token)
vault operator raft snapshot save /backup/vault_$(date +%Y%m%d).snap

# Encrypt and upload
gpg --encrypt --recipient backup@example.com /backup/vault_$(date +%Y%m%d).snap
restic backup /backup/vault_$(date +%Y%m%d).snap.gpg --tag vault

Testing Restores

A backup you haven’t tested is not a backup.

Monthly Restore Test

#!/bin/bash
# test-restore.sh

# Create test namespace
kubectl create namespace restore-test

# Restore from Velero
velero restore create test-restore \
  --from-backup $(velero backup get -o json | jq -r '.items[0].metadata.name') \
  --include-namespaces gitlab \
  --namespace-mappings gitlab:restore-test

# Wait for restore
velero restore wait test-restore

# Verify pods are running
kubectl get pods -n restore-test

# Test application (example: GitLab)
kubectl port-forward -n restore-test svc/gitlab 8080:80 &
curl -s http://localhost:8080/health | grep "ok"

# Cleanup
kubectl delete namespace restore-test

Document Recovery Procedures

For each critical system:

# GitLab Recovery Procedure

## Prerequisites
- Access to Velero backups
- Access to PostgreSQL backups
- GitLab Helm values

## Steps
1. Restore PostgreSQL from backup
2. Restore GitLab PVCs with Velero
3. Deploy GitLab with same Helm values
4. Verify user login works
5. Verify repositories are accessible

## Estimated Time: 45 minutes
## Last Tested: 2026-05-01

Monitoring Backups

Prometheus Alerts

groups:
  - name: backup-alerts
    rules:
      - alert: BackupFailed
        expr: restic_backup_last_successful_timestamp < (time() - 86400)
        for: 1h
        labels:
          severity: critical
        annotations:
          summary: "Backup hasn't succeeded in 24 hours"

      - alert: BackupStorageLow
        expr: restic_repository_size_bytes / restic_repository_max_bytes > 0.9
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "Backup storage over 90% full"

Backup Dashboard

Track in Grafana:

  • Last successful backup time
  • Backup duration trend
  • Storage usage
  • Restore test results

My Backup Setup

flowchart TD
    subgraph homelab["Homelab (K3s)"]
        PV["Persistent Volumes"]
        DB["Databases"]
        Config["Configs (Git)"]
    end

    subgraph local["Local Backup (NAS)"]
        Longhorn["Longhorn Snapshots"]
        Restic1["Restic Repository"]
    end

    subgraph offsite["Offsite (Backblaze B2)"]
        Velero["Velero Backups"]
        Restic2["Restic Offsite"]
    end

    PV --> Longhorn
    PV --> Velero
    DB --> Restic1
    Restic1 --> Restic2
    Config --> Git["GitLab (self-hosted)"]
    Git --> GitMirror["GitHub Mirror"]

Schedule

WhatFrequencyRetentionLocation
Longhorn snapshotsHourly24 hoursLocal NVMe
Longhorn backupsDaily7 daysNAS
Velero full backupDaily30 daysBackblaze B2
Database dumpsDaily30 daysBackblaze B2
Git reposPushForeverGitHub mirror

Costs

  • Backblaze B2: ~€5/month for 200GB
  • NAS storage: Already owned
  • Total: ~€5/month for peace of mind

Common Mistakes

“RAID is my backup”

RAID protects against drive failure. It doesn’t protect against:

  • Accidental deletion
  • Ransomware
  • Software bugs corrupting data
  • Fire/flood/theft

“I’ll restore when I need to”

If you’ve never restored, you don’t know if your backups work. Test quarterly at minimum.

“I backup everything”

Backing up 10TB of movies you can re-download wastes money and time. Prioritize irreplaceable data.

“My backup is in the same room”

A fire takes out your server AND your backup drive. Offsite is non-negotiable.

Why This Matters

Data loss is not a question of if, but when:

  • Drives fail (3-5% annual failure rate)
  • Humans make mistakes (rm -rf wrong directory)
  • Software has bugs (database corruption)
  • Bad things happen (fire, flood, theft)

The difference between “minor inconvenience” and “catastrophic loss” is a tested backup strategy.

Your homelab stores things that matter. Protect them accordingly.


The best time to set up backups was before you needed them. The second best time is now.