How we back up tela (and how to back up any product)
Backups are easy to set up and easy to get wrong. The setup is the cheap part; what actually saves you is a restore you've run and an alarm that fires the moment a backup goes quiet. Here's how tela's own data is backed up — a template you can copy for any self-hosted product.
:::stats
Copies of everything
5
Independent restic repositories
Locations
4
Separate sites, cloud included
Cadence
Nightly
03:30, jittered, self-healing
History
~6 months
Point-in-time recovery
Restores
Verified
Full rebuild, byte-exact :::
The engine: restic
The workhorse is restic — one static Go binary with three properties that matter for a product backup:
- Encrypted on-source. Data is encrypted before it leaves the machine, so the places that store it never see plaintext. (The flip side: lose the repository password and it's gone — so it lives in exactly one place.)
- Deduplicated. A nightly full dump costs about as much as an incremental — only the bytes that changed get written.
- Snapshotted. Every run is an immutable point-in-time snapshot, so you restore any night in the window, not just last night.
[!TIP] Backing up tela is refreshingly boring: every page is canonical markdown and every attachment lives inside Postgres — so a single database dump is the entire product. No separate uploads volume to forget.
It never stores files — it stores content-addressed chunks. A rolling hash cuts each file into variable-sized chunks at content-defined boundaries; each chunk's SHA-256 is its address; chunks are bundled into encrypted pack files. Before writing a chunk, restic checks if that hash already exists — if so, it's skipped. So a dump that changed a few rows re-uses yesterday's chunks and writes only the diff. A snapshot is just the root of a Merkle tree pointing at those shared chunks, so ten snapshots of near-identical data are ten cheap roots over one chunk pool — not ten copies. Pruning = drop the roots, garbage-collect unreferenced chunks.
Where the copies live
Because restic encrypts on-source, the machines that store backups only ever hold ciphertext — dumb, zero-trust disks. Every night the production node encrypts locally and pushes a copy to all five targets over a Tailscale mesh:
graph LR
SRC[["tela<br/>production node"]]
subgraph P["Primary site"]
H1[Machine A]
H2[Machine B]
end
subgraph S["Second site"]
O[Machine]
end
subgraph OFF["Off-site"]
FR[Machine]
end
subgraph Cloud
G[Google Drive]
end
SRC -->|encrypted| H1 & H2 & O & FR & G
To physically lose the data, four independent locations would have to fail on the same night — the last of them Google's cloud. That's 3-2-1 (three copies, two media, one offsite), pushed further: with deduplication, another copy is nearly free.
How often, and how far back
Nightly via a systemd timer (jittered so machines don't stampede; persistent, so a box that was off catches up). restic then prunes each repo on a ladder — fine-grained recently, coarser going back:
| Tier | Snapshots kept | Reaches back |
|---|---|---|
| Daily | 7 | 1 week |
| Weekly | 4 | 1 month |
| Monthly | 6 | ~6 months |
Old snapshots prune automatically every run, so storage stays flat instead of growing forever.
The two things that actually save you
Everything above is the easy 80%. These two are the point:
[!IMPORTANT] A backup you've never restored is a hope, not a backup. We periodically rebuild from scratch on a clean machine: newest offsite snapshot → empty throwaway Postgres → every page, revision, and attachment back byte-for-byte, no errors. "The file exists" and "the product boots from it" are different claims — only a restore proves the second.
[!WARNING] A backup that fails silently is worse than none — it's false confidence. Two alerts hit our phones: a repo that failed a run, and — the important one — any source with no successful backup in 36h. The trick for catching the unknown failure: don't alert on the failures you can imagine, alert on the absence of success.
And the sneakiest gap of all — data you forgot to back up: a weekly audit enumerates every live data store (Docker volumes, databases, even files held open by running processes) and pings us about anything that's neither in the backup manifest nor an explicit ignore list. Every new dataset must land in one bucket or the other, on purpose. "I forgot it exists" is designed out.
Steal the playbook
None of this is exotic or expensive — it's mostly discipline. If you run a product on your own infrastructure, this is the whole checklist:
- Many cheap copies, geographically spread — dedup makes the extra copies almost free.
- Encrypt on the source — so the storage targets can be untrusted.
- A retention ladder — depth of history, not just last night.
- Alert on the absence of success — not only on the errors you can imagine.
- A drift check — so new data can't slip through unbacked-up.
- Restore drills — the only proof that actually counts.
About tela
tela is the self-hostable, markdown-native team wiki this is about — Go + PostgreSQL, a React/Milkdown editor with live collaboration, and a built-in MCP server so coding agents are first-class. Open-source (AGPL-3.0); self-host it or start on the free cloud tier at telawiki.com. Either way the data is yours — and now you know exactly how we keep ours safe.
Related: Self-hosting a team wiki with Docker walks through the surrounding stack — app, database, proxy, and the secrets you must keep stable.