Portunus
Operations

Backup & Restore

WAL-aware online backup, validated restore, deliberate-by-design reset.

Since v0.8 every persistent server-side artefact lives in <data-dir>/state.db. Portunus ships matching backup, restore, and reset CLI subcommands.

Backup

portunus-server backup --out ~/portunus-backup-$(date +%F).db

Uses the SQLite Online Backup API (rusqlite::backup::Backup). The backup is WAL-aware and consistent without quiescing writers — safe to run while traffic is flowing.

Refuses to overwrite an existing destination — pick a unique path (date-stamped is the convention). If --out is an existing directory, the backup is written as portunus-state-<RFC3339>.db inside it.

Verify:

sqlite3 ~/portunus-backup-2026-05-08.db \
    'SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1;'

Cron schedule

# /etc/cron.daily/portunus-backup
#!/bin/sh
set -eu
DEST=/var/backups/portunus/$(date +%F).db
mkdir -p "$(dirname "$DEST")"
sudo -u portunus portunus-server \
  --data-dir /var/lib/portunus \
  backup --out "$DEST"
find /var/backups/portunus -name '*.db' -mtime +30 -delete

Restore

# 1. Stop the running server.
sudo systemctl stop portunus-server

# 2. Wipe local state.
portunus-server reset --confirm

# 3. Restore.
portunus-server restore --in /tmp/portunus-backup-2026-05-08.db

# 4. Start.
sudo systemctl start portunus-server

restore:

  • Validates the SQLite header magic before copying.
  • Refuses to clobber a non-empty data dir without --force. With --force it first removes state.db and the -wal / -shm / .lock sidecars, then copies the artefact in.
  • Runs schema migrations automatically if the binary is newer than the backup's schema.
  • On any migration or open failure, rolls back the half-written state.db (and its sidecars) so the next start does not see a corrupt file.
  • Refuses if the backup is newer than the binary, exiting 78 with event=startup.schema_version_too_new. Either keep the older backup or restore using the matching binary version.

Reset

# Dry run (no flag) prints what would be removed.
portunus-server reset

# Real wipe.
portunus-server reset --confirm

Removes state.db plus the sidecars state.db-wal, state.db-shm, and state.db.lock.

The reset CLI refuses to operate on a path whose first 16 bytes don't start with the SQLite header — protects against typo'd --data-dir. It does not wipe the generated TLS material (server.crt / server.key) or an optional server.toml in the same data-dir. On an empty data-dir reset is a no-op and exits 0.

Cross-host migration

identity.json (v0.5–v0.7) is mode-0600 atomic-write JSON, safe to rsync across hosts. The state.db file is not — use the backup / restore flow for clean migration.

Common pitfalls

  • NFS / tmpfs / ramfs for --data-dir is rejected at startup with event=startup.unsupported_filesystem. Use a local writable filesystem.
  • Two portunus-server serve against the same data-dir exit with event=startup.store_in_use (exit 75). Clustering is out of scope for the current release.
  • reset --confirm does not wipe TLS material or server.toml — only SQLite state. Clear those files separately if you need to.

On this page