AutomationOpen SourceFreeActiveMachine-verified· beginner · ~10 min setup

Self-hosting the open-source stack? Prove your backup actually restores before you need it

Make the one self-hosting discipline that matters a machine check: back up your database, destroy the live copy, restore from the backup, and assert the restored data matches the original exactly, so you find a broken backup in CI instead of at 2am.

by Shilpa Mitra· verified today· v1.0.0

Run this workflow

CI-verified, 2/2 fixtures passing.

Build this with your agent

One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.

Intended Use

Anyone self-hosting an open-source stack who wants proof their backup is real. CI creates a fixture database with known rows, dumps it, deletes the live file to simulate disaster, restores from the dump into a fresh database, and asserts the restored data equals the original exactly. Pure stdlib, no server, no network. What stays fenced: whether your production backup cron actually ran last night, and whether an offsite copy exists, no green check can promise either, only that the restore PROCEDURE works.

Not for

  • Proving your real backups ran, this proves the restore procedure works on a fixture; you still must schedule the dump, ship it offsite, and re-run this against a real backup
  • A specific product's backup format, SQLite stands in for your single business database; MariaDB/Postgres apps like ERPNext or Nextcloud use their own dump tools, but the round-trip discipline is identical
  • Backups as a substitute for testing restores, the whole point is that an untested backup silently fails exactly when you need it

The Stack

Tested Against

sqlite3 (python3 stdlib)node@20

Side effects & data flow

Network
none, local only
Writes
./crm.db, ./crm-backup.sql
Credentials
none required

Prerequisites

  • Python 3 (sqlite3 ships in the standard library)
  • For real use: a scheduled dump of your actual database and an offsite copy

Steps

  1. 1

    Run the backup -> destroy -> restore round-trip and assert the data survived

    Create a known-good dataset, dump it to a backup file, then delete the live database to simulate a disaster. Restore from the dump into a fresh database and compare the rows against the original. If a single row differs, the check fails, which is the whole point: you learn the backup is broken here, not when the server is already gone.

    python3 - <<'PY'
    import sqlite3, os, sys
    
    SRC = "crm.db"
    BACKUP = "crm-backup.sql"
    for f in (SRC, BACKUP):
        if os.path.exists(f):
            os.remove(f)
    
    # 1. the live business data (known-good state)
    con = sqlite3.connect(SRC)
    con.executescript("CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, mrr INTEGER);")
    con.executemany("INSERT INTO customers VALUES (?,?,?)", [(1, "Acme", 500), (2, "Globex", 1200), (3, "Initech", 300)])
    con.commit()
    orig = list(con.execute("SELECT id, name, mrr FROM customers ORDER BY id"))
    
    # 2. back it up (a plain SQL dump)
    with open(BACKUP, "w") as f:
        for line in con.iterdump():
            print(line, file=f)
    con.close()
    
    # 3. disaster: the live database is gone
    os.remove(SRC)
    if os.path.exists(SRC):
        print("BAD: could not simulate data loss")
        sys.exit(1)
    
    # 4. restore from the backup into a fresh database
    con2 = sqlite3.connect(SRC)
    with open(BACKUP) as f:
        con2.executescript(f.read())
    rest = list(con2.execute("SELECT id, name, mrr FROM customers ORDER BY id"))
    con2.close()
    
    # 5. the assertion the whole recipe exists for
    if rest != orig:
        print("BAD: restore does not match the original; this backup would have silently lost data (had " + str(len(orig)) + " rows, got " + str(len(rest)) + ")")
        sys.exit(1)
    
    print("restore OK: backed up " + str(len(orig)) + " rows, destroyed the live database, restored from the dump, and the restored data matches the original exactly. A backup you have never restored is a hope, not a backup")
    PY
  2. 2

    Point it at your real stack (the part CI cannot do for you)

    Schedule the dump for your actual database (pg_dump / mysqldump / the app's built-in backup, or a snapshot tool like restic), ship a copy offsite, and periodically run this same restore-and-compare against a real backup on a throwaway host. Whether last night's cron actually fired, and whether the offsite copy is current, are the fenced parts, this recipe proves the procedure, your ops proves it ran.

Eval, 2 fixtures

Last passed: verified today
  • restore-okcontainstimeout 30s · max $0

    Expected: restore OK: backed up 3 rows, destroyed the live database, restored from the dump, and the restored data matches the original exactly. A backup you have never restored is a hope, not a backup

  • clean-exitexit_codetimeout 30s · max $0

    Expected: 0

Results

Assembling a company on open-source tools (ERPNext, Nextcloud, Vaultwarden, and the rest) buys you ownership, but the newsletter's honest catch is that you become the IT department: no support line at 2am, and a backup nobody has ever restored is a hope, not a backup. This recipe encodes the round-trip that proves it: create a known-good dataset, dump it, delete the live database entirely, restore from the dump into a fresh database, and confirm the restored rows match the original exactly. SQLite is the fixture because its dump/restore is universal and needs no server; the discipline transfers to any database your stack runs on.

Did this work for you?

Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.

Related workflows

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).