|
Last issue I asked you to hit reply and tell me whether this was moving too fast.
Nobody replied.
A couple of you told me in person instead — which, for a newsletter about getting machines to do things for you, is a genuinely funny place to end up. The feedback channel that worked was a conversation.
And it wasn't about pace at all. Both said the same thing: put the whole issue in the issue. Stop sending me somewhere else to read the actual content.
The numbers backed them up. That blog post I sent everyone off to read? Two visitors. One of them was me, six times, proofreading.
Message received. This one is different — the whole walkthrough is right here. No "read the full post", no second tab, no link that opens in your phone's browser and loses your place halfway down. If it's worth telling you, it's worth putting in the thing you already opened.
Fair warning: that makes this a long one. Get a coffee.
You can just keep things
If you've been following along, you now have an application running on hardware you own, reachable from anywhere, with a padlock in the address bar and nothing exposed on your router.
Which means you now have something you would genuinely miss.
That's the wall everybody hits third. Not "how do I run it" or "how do I reach it", but the quiet realisation that all of it — the documents you scanned, the notes you wrote, the year of tinkering — lives on one disk, in one machine, in one room. Consumer SSDs die without warning. So do power supplies, and cats, and people who confidently type docker compose down -v at eleven at night.
Today we fix that. Twenty minutes, and you'll have a better backup story than most companies I've worked for.
First: no, you can't just copy the folder
The instinct is to drag the folder somewhere else. It doesn't hold up, for four reasons:
- You can't find the folder. Docker keeps named volumes in its own internal storage. There's no tidy directory to grab.
- A copy has no history. If a file quietly corrupted on Tuesday and you copy on Friday, congratulations — you now have two corrupted files.
- It's the whole thing, every time. Fine at 2 GB. Miserable at 200.
- It isn't encrypted. Whatever you copy it to, anyone holding that disk can read it.
What you want is snapshots: a history you can rewind, that only stores what actually changed, encrypted before it leaves the machine.
restic, and why it's the one
restic is a single binary that does exactly that. It deduplicates, so the third snapshot of a 40 GB database costs you almost nothing. It encrypts locally, so wherever you put the backup never sees your data. And it can write to a folder, another machine over SSH, or object storage, with no change to how you use it.
We're going to run it in Docker on a schedule, using an image that already knows how to do the boring parts.
The twenty-minute version
Make a folder, and put this in docker-compose.yml:
services:
backup:
image: mazzolino/restic:latest
container_name: restic-backup
restart: unless-stopped
hostname: homelab
environment:
RUN_ON_STARTUP: "false"
BACKUP_CRON: "0 3 * * *"
RESTIC_REPOSITORY: /repo
RESTIC_PASSWORD: ${RESTIC_PASSWORD}
RESTIC_BACKUP_SOURCES: /volumes
RESTIC_BACKUP_ARGS: --verbose=1 --tag docker-volumes
RESTIC_FORGET_ARGS: --keep-last 7 --keep-weekly 4 --keep-monthly 3 --prune
TZ: America/Los_Angeles
volumes:
- /srv/backups/restic:/repo
- paperless_data:/volumes/paperless-data:ro
- ghost_content:/volumes/ghost-content:ro
volumes:
paperless_data:
external: true
ghost_content:
external: true
Then a .env file beside it with one line:
RESTIC_PASSWORD=something-long-you-will-not-lose
And docker compose up -d. Four details in there repay understanding rather than copying:
:ro on every source. Read-only. The backup process is physically incapable of damaging the thing it's backing up. Never leave this off.
external: true tells Compose these volumes already exist and belong to other stacks — don't create new empty ones. Get the names from docker volume ls.
- The
FORGET line is your retention policy. Seven daily, four weekly, three monthly, then old snapshots are pruned. Without it your repository grows forever.
- That password is not recoverable. There is no reset, no support email, no clever trick. Lose it and your backups are permanently unreadable noise. Put it in your password manager now, before you read the next paragraph.
Kick off the first run by hand rather than waiting until 3am:
docker exec restic-backup backup
One copy isn't a backup
A backup on the same machine protects you from deleting a file. It does not protect you from a dead disk, a failed power supply, a burst pipe, or a burglary — all of which take the original and the copy in one go.
So: a second copy, on a different machine. If you have another box — an old desktop, a NAS, a Pi with a USB drive — add a second service to the same compose file:
backup-offsite:
image: mazzolino/restic:latest
container_name: restic-backup-offsite
restart: unless-stopped
hostname: homelab
environment:
BACKUP_CRON: "0 4 * * *"
RESTIC_REPOSITORY: sftp:otherbox:/mnt/storage/restic
RESTIC_PASSWORD: ${RESTIC_PASSWORD}
RESTIC_BACKUP_SOURCES: /volumes
RESTIC_FORGET_ARGS: --keep-last 7 --keep-weekly 8 --keep-monthly 12 --prune
volumes:
- offsite_ssh:/root/.ssh
- paperless_data:/volumes/paperless-data:ro
- ghost_content:/volumes/ghost-content:ro
An hour later than the first, so they're never fighting over the same disk.
Here's the part I like: you don't need to open a port for this either. Put Tailscale on both machines and they get private addresses that work from anywhere, over an encrypted link, with nothing exposed to the internet. Same principle as the tunnel from last time — the connection goes out, never in. If the two machines sit in different houses, you've just built genuine offsite backup for free.
No second machine? Point the repository at Backblaze B2 instead. It's around $6 per terabyte per month, and restic encrypts everything before it leaves your house, so B2 stores an archive it cannot read.
The bit that cost me an afternoon
Everything above backs up files. Databases are not files in the way you'd like them to be.
When restic copies Postgres or MySQL data while the server is running, it grabs those files mid-write. What you get is a crash-consistent copy — byte-for-byte identical to what would be on disk if you'd pulled the plug at that exact moment.
Postgres is impressively good at recovering from that. It usually works.
"Usually works" is not a backup strategy. And you will find out which kind of copy you have on the worst possible day.
The fix is to ask the database for a proper export first, then back up the export. A scheduled job, an hour before the backup runs:
docker exec my-postgres pg_dumpall -U postgres \
> /srv/backups/dumps/postgres-$(date +%F).sql
Then add /srv/backups/dumps to your backup sources. restic's deduplication means yesterday's dump and today's mostly-identical dump cost you almost nothing.
Keep backing up the raw volumes too. Belt and braces. But when something has actually gone wrong, the dump is the copy you'll reach for.
Do not skip this part
A backup you have never restored is not a backup. It is a folder you feel good about.
The failure I keep seeing in other people's setups — and once, memorably, in my own — isn't a missing backup. It's a backup that ran green every night for eight months and turned out to contain nothing useful. Nobody checked, because checking felt unnecessary, right up until it was the only thing that mattered.
So, right now, while it's fresh. List what you have:
docker run --rm \
-v /srv/backups/restic:/repo \
-e RESTIC_PASSWORD=your-password \
restic/restic -r /repo snapshots
Then pull one back out — into a scratch folder, nowhere near the original:
docker run --rm \
-v /srv/backups/restic:/repo \
-v /tmp/restore-test:/restore \
-e RESTIC_PASSWORD=your-password \
restic/restic -r /repo restore latest --target /restore
Now go and open something in /tmp/restore-test. An actual document. A real file you recognise. Not a directory listing — a file, opened, with your eyes.
That moment, where you see your own data come back out of the box, is the entire point of this issue. Everything before it was plumbing.
Once a month, restic will also verify the repository hasn't rotted:
docker exec restic-backup restic check
Where that leaves you
Three issues in, you have an application running on hardware you own, reachable from anywhere without exposing your network, and backed up twice a night to two different machines, encrypted, versioned, with a restore you have personally tested.
That is a genuinely competent setup. I have been paid real money to fix worse.
Now — the pace question, made easier
I still haven't had a straight answer on pace, and I suspect that's my fault: asking people to compose an entire email in order to report that something felt like too much effort was not my finest piece of design.
So: one tap. Three links. It'll drop you on my homepage and look like nothing happened — the click is the vote, and I'll see the count.
About right — keep going
Slow down a bit
Something else entirely
And if you do want to say something in words, replying still works and still comes to my actual inbox. I read every one. Apparently I just need to stop making it the only option.
Next time: automations. The part where the machine starts doing things without being asked, which is either the best or the worst idea I've had, depending on the week.
— Abid
P.S. If you set that password and didn't put it in your password manager, go and do it. I'll wait. There is no version of this where you get the data back without it.
|