# Backup & Restore (https://portunus.bybee.dev/en/docs/operations/backup-restore)



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

## Backup [#backup]

```sh
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:

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

## Cron schedule [#cron-schedule]

```sh
# /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 [#restore]

```sh
# 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 [#reset]

```sh
# 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 [#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 [#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.
