Stel je voor dat de disk in je homelab vannacht sterft. Niet corrupt raken, niet eerst een week lang SMART-waarschuwingen geven, gewoon weg. De GitLab met elke repo waar je om geeft, de password vault, vijftien jaar familiefoto’s, de home automation die je huis aanstuurt. Allemaal op een drive die heeft besloten dat hij klaar is.

Kun je beantwoorden wat er daarna gebeurt zonder dat je maag samentrekt? Zo niet, dan heb je geen backups. Je hebt hoop, en hoop is geen strategie waarvan je de grenzen om 2 uur ’s nachts wilt ontdekken.

Het goede nieuws: je hebt niets ingewikkelds nodig. De 3-2-1 regel bestaat al decennia, is bijna saai simpel, en het hele punt van deze post is om je van “ik denk dat ik iets heb” naar een setup te brengen die je daadwerkelijk getest hebt. We beginnen met de simpelste versie die je vandaag kunt draaien en bouwen de rest daar bovenop.

De 3-2-1-regel uitgelegd

Drie kopieën, twee verschillende media, één offsite. Dat is de hele regel. Al het andere in deze post is alleen maar die drie getallen invullen met echte tools.

flowchart TD
    subgraph rule["3-2-1 Backup Regel"]
        Data["Originele Data"]

        subgraph three["3 Kopieën"]
            C1["Kopie 1<br/>(Origineel)"]
            C2["Kopie 2<br/>(Lokale Backup)"]
            C3["Kopie 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

Waarom drie kopieën?

  • Kopie 1: Je live data (origineel)
  • Kopie 2: Lokale backup (snelle restore)
  • Kopie 3: Offsite backup (disaster recovery)

Eén kopie is gewoon je data die daar zit te wachten om te falen. Twee kopieën voelen veiliger, precies tot hetzelfde event ze allebei tegelijk uitschakelt, en genoeg events doen dat: een brand, een overstroming, een ransomware payload die elke mount aftikt die hij kan bereiken. Drie kopieën met echte scheiding ertussen is het eerste punt waarop je echte resilience hebt in plaats van een geruststellend gevoel.

Waarom twee media types?

Verschillende hardware faalt op verschillende manieren, en je wilt je kopieën verspreid hebben over die failure modes zodat één slechte dag ze niet allemaal kan meenemen.

  • SSDs kunnen stilletjes falen via bit rot, waarbij data ongemerkt slecht wordt voordat je het doorhebt
  • HDDs sterven mechanisch, vaak met genoeg waarschuwing om te reageren als je oplet
  • RAID is geen backup. Het beschermt tegen een drive die wegvalt, en doet niets tegen corruptie, verwijdering, of een slechte write die netjes gespiegeld wordt over elke disk

Zet je kopieën op een SSD én een draaiende disk én een remote bucket, en het scenario dat de één doodt is zelden het scenario dat de andere doodt.

Waarom één offsite?

Je huis kan afbranden. Je straat kan overstromen. Een stroompiek kan alles koken wat op hetzelfde circuit hangt. Lokale kopieën zijn snel en handig en volkomen waardeloos tegen alles wat het hele gebouw tegelijk raakt. Offsite is de kopie die overleeft wanneer de kamer waar je server staat dat niet doet.

Wat back-uppen

Voordat je iets back-upt, sorteer je data in drie bakken. De meeste mensen back-uppen óf veel te veel óf slaan stilletjes het ene ding over dat ze niet kunnen herbouwen, en een paar minuten sorteren lost beide op.

Kritiek (dagelijkse backup)

DataWaaromTool
DatabasesKan niet herstellenpg_dump, Velero
Secrets/credentialsSecurity kritiekVault export, External Secrets
ConfiguratieSysteem staatGit (al offsite)
Persoonlijke bestandenOnvervangbaarRestic

Belangrijk (wekelijkse backup)

DataWaaromTool
Container imagesRebuild kost tijdRegistry backup
Persistent volumesStateful workloadsLonghorn/Velero
Logs (gecomprimeerd)ForensicsLoki snapshots

Herbouwbaar (niet back-uppen)

  • Basis OS (herinstalleer van ISO)
  • Gedownloade packages (opnieuw downloaden)
  • Gecachete data (regenereert)
  • Tijdelijke bestanden

Als je het kunt herstellen uit een commando of een download, laat het weg. Backup ruimte en backup tijd kosten allebei iets, en daarvan iets uitgeven aan data die je naar believen terug kunt halen is verspild.

Backup tools

Goed, dat was de theorie. Hier wordt het praktisch. Ik begin met de simpelste tool die je een echte tweede kopie geeft, en bouw dan op naar de Kubernetes-bewuste dingen.

Restic: file-level backups

Als je na het lezen van dit alleen maar één ding doet, zet dan Restic op. Het is mijn go-to voor file backups: snel, standaard versleuteld, dedupliceert zodat herhaalde backups klein blijven, en praat zonder gedoe met een lokale disk of een S3 bucket zonder je workflow te veranderen. Dit is de minimaal werkende versie van een backup strategie, en je hebt het in vijf minuten draaien.

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

# Of met S3 backend
restic init --repo s3:s3.amazonaws.com/my-bucket

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

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

# Lijst snapshots
restic snapshots

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

Geautomatiseerde Restic backups

Een backup die je zelf moet onthouden te draaien is een backup die niet gaat gebeuren. Pak de commando’s in een script, wijs het naar je offsite bucket, en laat het de pruning regelen zodat de repo niet eindeloos groeit.

#!/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 oude snapshots (bewaar 7 dagelijks, 4 wekelijks, 12 maandelijks)
restic forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 12 \
  --prune

# Check repository integriteit
restic check

Die restic check aan het eind doet er meer toe dan het lijkt. Het verifieert dat de repository daadwerkelijk teruggelezen kan worden, en dat is het verschil tussen een backup en een map vol bytes waarvan je hoopt dat ze nog kloppen.

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

Zodra je een cluster draait, zijn file-level backups niet meer genoeg. Je wilt ook de staat van het cluster zelf: de Deployments, de ConfigMaps, de PersistentVolumeClaims, de vorm van het geheel. Velero back-upt Kubernetes resources en hun persistent volumes samen, zodat een restore de workloads én hun data in één beweging terugbrengt.

# Installeer Velero met 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

Geplande 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  # Bewaar 30 dagen
    storageLocation: default
    volumeSnapshotLocations:
      - default

Restore vanuit Velero

# Lijst backups
velero backup get

# Beschrijf een backup
velero backup describe daily-backup-20260518030000

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

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

Longhorn backups

Draai je Longhorn voor storage, dan heb je al een backup engine onder je volumes zitten, en die praat direct met S3:

# Configureer 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"

Plan terugkerende 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 opties

De “1” in 3-2-1 is het deel dat mensen overslaan, meestal omdat het de dure, ingewikkelde voelt. Dat is hij niet. Je hebt hier drie eerlijke opties, en ze ruilen kosten tegen controle op een manier die netjes aansluit op hoeveel je om sovereignty geeft.

Cloud storage

De goedkoopste manier om offsite te komen. Kies een provider, wijs Restic of Velero ernaartoe, klaar.

ProviderKostenVoordelenNadelen
Backblaze B2$0.005/GBGoedkoop, S3-compatibleUS-gebaseerd
Wasabi$0.0059/GBGeen egress kosten90-dagen minimum
AWS S3 Glacier$0.004/GBZeer goedkoopTrage retrieval
Hetzner Storage Box€3.81/1TBEU-gebaseerd, goedkoopAlleen SFTP/WebDAV

Wil je liever dat je offsite kopie binnen de EU staat, dan is de Hetzner Storage Box op prijs lastig te verslaan, en Backblaze is de makkelijke default als je het gewoon werkend wilt hebben.

Tweede locatie

Heb je een vriend of familielid dat ook een homelab draait? Back-up elkaar. Zij hosten een versleutelde kopie van jouw data, jij host een versleutelde kopie van die van hen, en niemand kan lezen wat ze opslaan.

flowchart LR
    subgraph your["Jouw Huis"]
        YourData["Jouw Data"]
        YourBackup["Hun Backup<br/>(versleuteld)"]
    end

    subgraph their["Hun Huis"]
        TheirData["Hun Data"]
        TheirBackup["Jouw Backup<br/>(versleuteld)"]
    end

    YourData -->|Versleuteld| TheirBackup
    TheirData -->|Versleuteld| YourBackup

Dit kost je niets behalve wat disk en een beetje upload bandbreedte, en het houdt je offsite kopie volledig buiten de handen van welke cloud provider dan ook.

Self-hosted cloud

De volle sovereignty-versie: draai je eigen S3-compatible storage op een tweede fysieke locatie en back-up daarnaartoe.

# MinIO op remote locatie
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

Bereik het via Tailscale zodat de storage nooit op het publieke internet hoeft te staan.

Database backups

Databases hebben hun eigen aanpak nodig, omdat de bestanden onder een draaiende database vandaan kopiëren je een snapshot geeft die mogelijk midden in een write zit en niet te herstellen is. Dump ze in plaats daarvan netjes.

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 alle databases
kubectl exec -n $NAMESPACE $POD -- \
  pg_dumpall -U postgres | \
  gzip > /backup/postgres_${DATE}.sql.gz

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

Vault backup

Vault is degene waar ik wakker van lig, want hem kwijtraken betekent toegang kwijtraken tot al het andere. Maak een raft snapshot, versleutel hem voordat hij de machine verlaat, en stuur hem pas dan weg.

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

# Versleutel en 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

Restores testen

Hier is het deel waar iedereen instemmend knikt en bijna niemand doet. Een backup die je niet getest hebt is geen backup. Het is een gok. De eerste keer dat je een restore draait zou nooit de keer moeten zijn dat je hem echt nodig hebt, wanneer je gestrest bent en het productiesysteem plat ligt en je erachter komt dat je dumps de afgelopen drie maanden leeg waren.

Maandelijkse restore test

Dus test het op een schema. Restore naar een wegwerp namespace, check of het ding daadwerkelijk opkomt, breek het weer af.

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

# Maak test namespace
kubectl create namespace restore-test

# Restore vanuit 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

# Wacht op restore
velero restore wait test-restore

# Verifieer pods draaien
kubectl get pods -n restore-test

# Test applicatie (voorbeeld: 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

Documenteer recovery procedures

Wanneer er echt iets in brand staat, wil je niet je eigen restore proces uit je geheugen zitten te reverse-engineeren. Schrijf de runbook op voor elk kritiek systeem, inclusief hoe lang het duurde en wanneer je voor het laatst bewees dat het werkt.

# GitLab Recovery Procedure

## Vereisten
- Toegang tot Velero backups
- Toegang tot PostgreSQL backups
- GitLab Helm values

## Stappen
1. Restore PostgreSQL vanuit backup
2. Restore GitLab PVCs met Velero
3. Deploy GitLab met zelfde Helm values
4. Verifieer user login werkt
5. Verifieer repositories bereikbaar zijn

## Geschatte Tijd: 45 minuten
## Laatst Getest: 2026-05-01

Die “Laatst Getest” regel is een stille druk om de test eerlijk te houden. Als de datum een jaar oud is, heb je geen backup strategie, dan heb je een backup wens.

Backups monitoren

Een backup job die twee weken geleden stil is gestopt is erger dan geen backup, want je denkt dat je gedekt bent. Hang de jobs aan je monitoring zodat een failure naar je schreeuwt in plaats van te wachten om je te verrassen.

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 niet geslaagd in 24 uur"

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

Backup dashboard

Track in Grafana:

  • Laatste succesvolle backup tijd
  • Backup duur trend
  • Storage gebruik
  • Restore test resultaten

Mijn backup setup

Zo past dit alles samen in mijn eigen homelab, wat eigenlijk gewoon de bovenstaande lagen op elkaar gestapeld is. Lokale kopieën voor snelle restores, een offsite bucket voor het rampgeval, en Git voor alles wat al tekst is.

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

    subgraph local["Lokale 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"]

Schema

WatFrequentieRetentieLocatie
Longhorn snapshotsPer uur24 uurLokale NVMe
Longhorn backupsDagelijks7 dagenNAS
Velero volledige backupDagelijks30 dagenBackblaze B2
Database dumpsDagelijks30 dagenBackblaze B2
Git reposPushAltijdGitHub mirror

Kosten

  • Backblaze B2: ~€5/maand voor 200GB
  • NAS storage: Al in bezit
  • Totaal: ~€5/maand voor gemoedsrust

Vijf euro per maand. Dat is de hele rekening voor niet wakker liggen met de vraag of de foto’s weg zijn.

Veelgemaakte fouten

De meeste hiervan heb ik zelf gemaakt, of vrienden zien maken. Ze klinken allemaal redelijk, precies tot het moment dat ze je alles kosten.

“RAID is mijn backup”

RAID beschermt tegen een drive die faalt. Het doet niets tegen:

  • Per ongeluk verwijderen
  • Ransomware
  • Software bugs die data corrumperen
  • Brand/overstroming/diefstal

Een rm -rf in de verkeerde directory plant zich direct voort over elke disk in de array. RAID is uptime, geen veiligheid.

“Ik restore wel als ik het nodig heb”

Als je nooit gerestored hebt, weet je niet of je backups werken, en het faalgeval is precies wanneer je erachter komt dat ze het niet doen. Test minimaal per kwartaal.

“Ik backup alles”

10TB aan films back-uppen die je opnieuw kunt downloaden verbrandt geld en tijd die je kunt besteden aan het beschermen van de data die je niet kunt vervangen. Sorteer eerst, back-up dan het onvervangbare spul.

“Mijn backup is in dezelfde kamer”

Eén brand haalt de server én de backup drive die ernaast staat eruit. Offsite is het deel dat de hele regel de moeite waard maakt.

Waarom dit ertoe doet

Dataverlies is geen misschien. Drives dragen een jaarlijkse failure rate van 3-5%, mensen draaien het verkeerde commando, software corrumpeert databases op manieren die elke health check doorstaan, en huizen vatten af en toe vlam. Draai een homelab lang genoeg en je komt minstens één van deze tegen.

Wat de uitkomst verandert, is of je het saaie werk van tevoren deed. Dezelfde kloof waar ik op deze blog steeds op terugkom, duikt hier ook op: het verschil tussen systemen die je daadwerkelijk begrijpt en controleert, en systemen waarvan je gewoon hoopt dat ze het houden. Een geteste backup strategie is wat “ik ben alles kwijt” verandert in “ik heb van vannacht gerestored en ik ben een middag kwijt”.

Je homelab bevat de dingen die je het meest zou haten om kwijt te raken. Geef de vijf euro en de middag uit, en bescherm ze fatsoenlijk.


De beste tijd om backups in te stellen was voordat je ze nodig had. De op een na beste tijd is nu.