Backups 101
The 3-2-1 basics applied to a homelab - backing up compose volumes and configs, not just "hoping the disk doesn't die."
This is the least optional page in the Beginner tier. Everything else here is about adding capability; this one is about not losing what you've built.
The 3-2-1 rule, applied to a homelab
See 3-2-1 backup rule in the glossary for the general definition. In homelab terms:
- 3 copies - the live data, plus at least two backups.
- 2 different media - not two copies on the same disk. An external USB drive is a genuinely fine second copy for this tier.
- 1 off-site - a cloud storage target (Backblaze B2, a relative's house with a drive, anything genuinely not in the same building) so a fire or theft doesn't take every copy at once.
You don't need all of this from day one, but know what you're missing. A single external drive plugged into the server is a backup, not the backup - it's better than nothing and it's not 3-2-1 yet.
What actually needs backing up
For a Docker-based homelab, it's narrower than people expect:
- Bind-mounted volumes - the
./config,./datastyle folders from your compose files (see Docker Compose Basics). This is where service state, databases, and media library metadata live. - The compose files themselves - small, text, trivial to back up,
but easy to forget. Keep your
~/homelab/folder (or wherever your compose files live) in the backup scope, ideally in a git repo too. - Anything not reproducible from the above - your actual media library, for instance, if re-acquiring it would be painful. Whether that's worth backing up (vs. just re-ripping/re-downloading) is a judgment call about how replaceable the content is.
You generally do not need to back up Docker images themselves - they're re-downloadable from their registry on demand.
Databases need a dump, not a file copy
The one trap in "just back up the bind-mounted folders": a database's
data folder (the ./db-data from
Docker Compose Basics, say) is
being written to constantly while the container runs. Copy it
mid-write and the backup finishes without complaint, but restoring it
can give you a database that won't start. Two simple fixes:
- Dump it first. Have the database write a consistent export into
a folder that's in your backup scope, just before the backup runs.
For the Postgres example from Docker Compose Basics:
MariaDB/MySQL usesmkdir -p ~/homelab/app/db-dumps docker exec app-db pg_dump -U postgres postgres > ~/homelab/app/db-dumps/app.sqlmariadb-dumpinstead - see Nextcloud for an example. Some apps do this for you: Immich writes its own scheduled database dumps into its library folder. - Or stop the stack while it backs up.
docker compose stop, run the backup,docker compose start. A few minutes of downtime at 3am is a perfectly reasonable trade for a homelab.
A simple approach: restic to an external drive and B2
restic is a free, encrypted, deduplicating backup tool that works well for exactly this use case.
sudo apt install restic
Run the restic commands below as root (from a sudo -i shell, or
from a root-owned systemd timer for the nightly run). Containers often
create files your own user can't read - Postgres's data folder, for
one - and a backup run as your normal user skips them with
"permission denied." That's also why the paths below are written out
in full: inside sudo -i, ~ means /root, not your home folder.
restic repositories are always encrypted and need a password. Set one before doing anything else, so it's available for the automated runs below rather than restic prompting interactively every time:
export RESTIC_PASSWORD="a-real-generated-password"
Save that same value somewhere durable (your password manager, not
just this shell session) - a repository without its password is
unrecoverable, the same way a Vaultwarden vault is gone for good if
you forget its master password. For the
cron job or systemd timer mentioned below, put it in an
EnvironmentFile (systemd) or source it from a restricted-permission
file rather than hardcoding it in the crontab itself.
Initialize a repository on an external drive:
restic init --repo /mnt/backup-drive/homelab-restic
Back up your homelab folder:
restic backup /home/yourname/homelab --repo /mnt/backup-drive/homelab-restic
Restic only stores changed data on each run (deduplication), so repeated backups after the first are fast and small. Automate it with a cron job or systemd timer running nightly - see cron/timer setup in your distro's documentation if you haven't used either before.
For the off-site copy, restic supports Backblaze B2, S3-compatible storage, and several others directly:
export B2_ACCOUNT_ID=xxx
export B2_ACCOUNT_KEY=xxx
restic init --repo b2:your-bucket-name:homelab
restic backup /home/yourname/homelab --repo b2:your-bucket-name:homelab
Test the restore before you need it
A backup you haven't restored from is a theory, not a backup. Once it's running, actually test a restore into a scratch folder:
restic restore latest --repo /mnt/backup-drive/homelab-restic --target /tmp/restore-test
Confirm the files look right. Do this now, while it's a five-minute exercise, not during an actual outage when it's a crisis.
With real backups in place, the last thing worth doing before calling this tier done is a sanity check on what it's actually costing you to run - see Cost and Power Tracking.
Next: Cost and Power Tracking.