DOCS · ADMIN · BACKUPS

Sleep soundly.

A nightly systemd timer gzips a pg_dump and snapshots the signing keys under /etc/qnt. Wire the optional R2 mirror to get the dumps off-host, and keep the restore drill handy so a bad migration is a 5-minute fix, not an outage.

1 What gets backed up

Two things, both captured by qnt-backup.sh: the Postgres database, and the secret keys under /etc/qnt. The database is the obvious one — the keys are the one people forget, and the one that hurts most to lose.

The Postgres database

A gzip'd pg_dump. The script defaults to PG_USER=qnt, PG_DB=qnt, PG_HOST=127.0.0.1, and reads a DATABASE_URL from /etc/qnt/qnt-server.env to pick up the DB password if one is set.

Dumps land in /var/backups/qnt (BACKUP_DIR=/var/backups/qnt in the script).

The signing keys — don't skip these

The script also snapshots /etc/qnt/*.key and server.crt:

  • the capability-token signing key
  • the JWT signing key
  • the dashboard admin key
  • the OAuth secret

Without these, every customer's capability token, every active session, and OAuth login break. The database alone is not a recoverable backup.

Retention

  • SQL dumps — kept 14 days.
  • Key snapshots — all kept. They're tiny and rarely change, so there's no reason to age them out.

2 Install the timer

Three files ship in the repo: deploy/qnt-backup.sh, deploy/qnt-backup.service, and deploy/qnt-backup.timer. Copy them into place and enable the timer.

Copy the files and enable

cp deploy/qnt-backup.sh /usr/local/bin/qnt-backup.sh
chmod +x /usr/local/bin/qnt-backup.sh
cp deploy/qnt-backup.service /etc/systemd/system/
cp deploy/qnt-backup.timer   /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now qnt-backup.timer

The schedule

  • OnCalendar=*-*-* 03:17:00 — 03:17 UTC daily, offset off the top of the hour to dodge the 03:00 cron storm.
  • Persistent=true — if the VM was off at 03:17, the run fires on next boot rather than being skipped.
  • RandomizedDelaySec=5min — jitter so the run doesn't land at exactly the same instant every night.

Run it by hand

To take a backup on demand — before a migration, say — run the script directly:

sudo /usr/local/bin/qnt-backup.sh

3 The same-VM warning

Read this before you decide you're done. A local nightly dump is a real safety net, but it has one sharp limit.

This protects against logical loss only

This only protects against logical loss (DROP TABLE, a bad migration). It does not protect against the disk failing — backups live on the same VM. Off-host mirroring is a separate hardening pass.

In other words: if the VM's disk dies, the dumps die with it. The next section wires the optional off-host mirror to close that gap.

4 Off-host mirror to Cloudflare R2

Optional and opt-in. deploy/pg-backup-r2-push.sh mirrors the dumps to a Cloudflare R2 bucket so a dead disk doesn't take the backups with it.

Safe to wire before R2 exists

The push script soft-skips when rclone or its config is absent, so you can wire it into your flow before R2 is actually set up — it just does nothing until the pieces are in place.

  • Install rclone.
  • Configure an rclone remote pointing at your R2 bucket.
  • The script then pushes the latest dump to that remote.

Once rclone and the remote are configured, the off-host copy rides along with the nightly run — no extra schedule to manage.

5 Restore drill

Practice this before you need it. The sequence below restores a gzip'd SQL dump from /var/backups/qnt — and restores the keys first if you lost the disk.

Restore the keys first (if you lost the disk)

If this is a disk-loss recovery, restore /etc/qnt/*.key from the key snapshots before starting qnt-server. Skip this and every session and capability token breaks even after the DB is back.

On a logical-loss restore (the keys never went anywhere) you can jump straight to the DB steps below.

Stop the server and recreate the DB

Stop qnt-server, pick the latest dump from /var/backups/qnt, then drop and recreate the database:

systemctl stop qnt-server
sudo -u postgres dropdb qnt
sudo -u postgres createdb qnt

Restore the dump

Pipe the gzip'd dump straight into psql (swap <dump> for the file you picked):

gunzip -c /var/backups/qnt/<dump>.sql.gz | sudo -u postgres psql -d qnt

Smoke-test, then start

sudo -u postgres psql -d qnt -c "SELECT count(*) FROM tunnels"
systemctl start qnt-server
systemctl is-active qnt-server

A row count from tunnels confirms the data is back; is-active returning active confirms qnt-server came up clean against the restored database.

Next

Backups armed and a drill in hand. Round out the operational guides.