A Backup Strategy for a Self-Hosted Proxmox Stack (That You Actually Test)

Every homelab has a moment of religion about backups. Mine hasn’t come from an actual disaster yet — and if I’m honest, that’s exactly the problem with the setup I’m about to describe: it protects me from the mistakes I’ve already imagined, and I won’t know what it missed until it’s tested by something real. This is the backup strategy I actually run today for a Proxmox host with about 30 LXC containers, most of them personal projects — what’s covered, what deliberately isn’t, and the gap I’m still working through.

What I Actually Back Up, and What I Don’t

Out of roughly 30 containers, three have a scheduled backup job: my WireGuard gateway, my Forgejo git server, and my Vaultwarden password vault. That’s not an oversight — it’s a decision. Those three hold state that either can’t be regenerated (git history, encrypted vault contents) or would be genuinely painful to lose access through (VPN gateway). Everything else on the box is either stateless, rebuildable from a script, or — like my mail stack — deliberately excluded because a stale backup of a mail server can cause more damage than no backup at all if it’s ever restored by mistake.

The lesson this took me a while to learn: “back up everything” isn’t a policy, it’s the absence of one. Deciding what actually needs a restore path, and writing that decision down, is the actual work.

The Job Itself: vzdump, Snapshot Mode, zstd

Proxmox’s built-in scheduler (Datacenter → Backup) runs each of the three jobs nightly, staggered ten to thirty minutes apart so they don’t compete for I/O:

# what my job config actually looks like (/etc/pve/jobs.cfg)
vzdump: backup-vaultwarden
    vmid 123
    schedule 03:30
    mode snapshot
    compress zstd
    storage local
    prune-backups keep-last=1

Snapshot mode backs up a running container with no perceptible pause — the right default unless a guest can’t tolerate a crash-consistent copy (more on that below). zstd compression is the easy call: better ratio than gzip, faster than either gzip or bzip2, no real downside on modern hardware.

The Part I Should Fix: keep-last=1

Here’s the detail I’d change first if I were advising someone else: my retention is keep-last=1. One backup, replaced every night. It answers “what if this container breaks right now” — it does not answer “what if I don’t notice the problem for three days,” which is the failure mode that actually bites people. A `keep-daily=7, keep-weekly=4` policy costs almost nothing in a zstd-compressed, deduplicating-adjacent world and buys a real window to notice something went wrong before the good copy is gone. It’s on my list; I’m writing it here partly so it stays on the list.

The Bigger Gap: Everything Lives on One Disk

All three jobs write to storage: local — the same physical NVMe the containers themselves run on. That protects against a container getting corrupted or a config change going wrong. It does not protect against the drive dying, which is the failure mode backups exist for in the first place. Right now, if that NVMe fails, I lose the containers and their backups in the same event.

The fix is well understood and I just haven’t done it yet: either a second Proxmox box running Proxmox Backup Server (incremental, deduplicated, and it can live anywhere on the LAN or off-site), or the cheaper version — an rclone job pushing the nightly dump to encrypted object storage:

#!/bin/bash
# what I'd run, once there's somewhere for it to go
set -euo pipefail
rclone sync /var/lib/vz/dump crypt-remote:proxmox-dumps 
  --max-age 48h --transfers 2 --bwlimit 10M

Encrypt before it leaves the building — an unencrypted database dump sitting in someone else’s object storage is how homelabs end up in breach-notification stories, and Vaultwarden’s backup is precisely the file you don’t want exposed that way.

One Habit That Already Pays Off

Scheduled backups protect me from disasters. A manual, named backup right before I touch something protects me from myself:

vzdump 123 --mode snapshot --compress zstd --storage local 
  --notes-template 'vault-{{vmid}}-pre-upgrade'

I run this by hand before anything that could go sideways — a version upgrade, a config change I’m not 100% sure about. It’s thirty seconds and it has saved me from having to explain to myself why I didn’t.

The Restore Drill I Ran While Writing This

An untested backup is a hope, not a plan — so before publishing this, I actually did it, on the real backup from that same night:

pct restore 998 /var/lib/vz/dump/vzdump-lxc-123-2026_08_11-03_30_00.tar.zst 
  --storage local-lvm --hostname restore-drill-test --unprivileged 1

Restore of a 2.4 GiB archive: 9 seconds. Before starting it I stripped the network config down to ip=manual — the original backup carries the same static IP and MAC as the live container, and starting it unmodified on the same bridge would have handed two containers the same address on my LAN. With that one change, I started it:

pct start 998
docker ps   # → vaultwarden container up, health: starting
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8000/   # → 200

Docker came up on its own inside the restored container, Vaultwarden reattached to the restored SQLite file at /opt/vaultwarden/data/db.sqlite3 (1,064,960 bytes, present and the right size), and the web UI answered HTTP 200. Then:

pct stop 998 && pct destroy 998 --purge 1

Whole thing, start to cleanup, under two minutes. It also caught something the checklist above doesn’t mention: I hadn’t actually confirmed until just now that Vaultwarden’s Docker container restarts itself automatically on boot rather than needing a manual docker start — it does, which is one less step during a real recovery than I’d assumed. That’s the actual value of running the drill instead of trusting that it would probably work: you don’t find out what you got right by assuming it.

This was the first time I’ve run this specific drill — not yet the quarterly habit it should be. It’s on the same list as the retention policy above, except now it has a time and a result next to it instead of just an intention.

The Checklist, Honestly

  • Decide what actually needs a restore path — not everything does, and pretending otherwise means nothing gets backed up well.
  • Scheduled vzdump, snapshot mode, zstd. That part’s solid.
  • keep-last=1 is the minimum, not the goal — more history is cheap.
  • Same-disk backups are a false sense of security. Offsite (PBS or encrypted rclone) is the next thing I’m building, not a nice-to-have.
  • Manual pre-change backups with notes, always — the cheapest insurance in this whole list.
  • Run an actual restore drill, on a throwaway VMID, with the network stripped out until you’ve checked it. Ten minutes, and it’s the only way to know the backup works instead of hoping it does.

I’ll update this post once the offsite piece exists. Writing down the honest state of a setup, gaps included, has been more useful to me than any “here’s the perfect architecture” guide I’ve read — the perfect ones rarely say what they haven’t gotten to yet.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *