Skip to content

Backup & Disaster Recovery

Vesana has a built-in backup system under Admin → System → Backup & DR (super admins only). It's the one standard — no separate sidecar, no manual pg_dump needed.

Four kinds, one catalog

All backups land in ONE catalog, in ONE location, under ONE retention logic:

Kind When Scope
Scheduled Per your configured schedules selectable: full or config-only
Manual "Full backup" or "Configuration only" button (also: uploaded backups carry this label) per button
Pre-restore Automatically right before every restore always full
Pre-update Automatically before every GUI update always config-only

Backups with a reduced scope carry a "config only" badge in the list.

A backup is a compressed pg_dump (.sql.gz). A full backup contains the complete dump including the TimescaleDB hypertables (check history, logs). A config-only backup leaves out the large operational tables and hypertable chunks — the schema is preserved, and those tables are created empty on restore. Order of magnitude: a full backup can easily run to a few hundred MB, a config-only backup usually stays in the low single-digit MB range.

Backups live in a Docker volume, outside the normal /opt/vesana directory tree.

FIELD_ENCRYPTION_KEY is the one thing that truly can't be replaced

Encrypted DB fields (SNMP communities, SNMPv3 and SSH passwords) can never be decrypted again without this key. The key must live outside the backup — in a password manager plus a printed copy in a safe. Keep the backup dump and FIELD_ENCRYPTION_KEY separate.

Storing safely

A backup dump alone isn't a recovery plan as long as FIELD_ENCRYPTION_KEY is missing. Practical recommendation:

  1. Store the key in a password manager (Bitwarden, 1Password, KeePass)
  2. Additionally print it offline or keep it on a separate medium
  3. Don't store it in the same volume or the same off-site copy as the database backups — otherwise losing one means losing the other too

Schedules & retention

Under Admin → System → Backup & DR → Schedules & retention you can create several schedules in parallel — e.g. a daily config backup plus a weekly full backup. Per schedule you choose:

Field Meaning
Active/Paused toggle
Frequency daily / weekly / monthly
Scope full backup / config only
Weekday only for "weekly"
Day of month only for "monthly"
Hour, minute (UTC) exact time
Custom retention optional — otherwise the global default retention applies

There's deliberately no free-form cron field — only dropdowns for frequency and time. As long as you haven't created your own schedule, the default runs: daily at 02:00 UTC, config-only, global retention. If you remove all schedules, automatic backups are off.

If a scheduled backup fails, the diagnostics log says so; the other schedules still run, and a new attempt follows after one hour at the earliest. Backup and restore never run at the same time: no backup starts while a restore is running (a scheduled one catches up on the next tick), and vice versa.

Default retention

Applies to manual, pre-restore, and pre-update backups, and to schedules without their own values:

Setting Default Meaning
Max count 5 more backups → oldest get dropped
Max age (days) 90 older ones get deleted
Max disk (GB) 10 total cap across all backups combined; at least one is always kept

The strictest of the three limits wins (count, age, disk cap). If a schedule sets its own limits, those apply to its backups; the rest continues to follow the global default retention. A cleanup run happens every 6 hours, plus right after every scheduled backup, and on manual trigger.

If a backup entry's file disappears through cleanup (e.g. because a limit just kicked in), the list shows "File missing" instead of a dead download link.

Download, restore, delete, changing the retention and the manual cleanup run require a fresh identity confirmation (passkey, authenticator code or password) — an old or hijacked session cannot quietly destroy backups.

Off-site copy (important)

Backups live locally on this server. In a total machine failure, they're gone too. For real disaster recovery you need an off-site copy:

  • In the UI, click "Download" on a backup → saves the .sql.gz locally.
  • Or copy it straight out of the Docker volume with your own script:
docker run --rm -v vesana_backup-data:/src -v $(pwd):/dst alpine \
  sh -c 'cp /src/*.sql.gz /dst/'
# then rotate to S3 / NAS / USB

Restore (on a running server)

In the UI: click "Restore" on a backup → type the hostname to confirm → confirm your identity freshly (passkey, authenticator code or password) → restore. Backups from another server are added to the catalog via Upload first — large full backups included (up to 20 GB; a reverse proxy of your own in front needs a matching upload limit).

Before replacing anything, the restore checks the backup file; a damaged file aborts without touching the database. The restore then creates the database fresh instead of deleting into the running one: it ends open database sessions, recreates the database and wraps the dump in the matching TimescaleDB routines so all time-series chunks come back correctly. Before that Vesana automatically takes a pre-restore snapshot; it and the restored backup stay visible in the catalog afterwards — the way back is clickable. For a configuration-only backup the dialog warns that history will be empty afterwards. The backup list shows the real (compressed) file size.

Restore = maintenance window

The restore replaces the database and disconnects running sessions. All changes since the selected backup are lost. Do not trigger it lightly during operation. Backups also live only locally on this server — real resilience needs an off-site copy (see above).

Disaster recovery: fresh server

Goal: server crashes → fresh server → yesterday's backup goes in → yesterday's state is back, nothing lost except the day in progress.

  1. Set up a new server with the setup script (as for a first install). On the first start, migrations run → an empty schema + the TimescaleDB extension are in place.
  2. Carry over FIELD_ENCRYPTION_KEY from the old .env (otherwise encrypted fields are unreadable after restore) — before the restore, because containers only read environment variables at start:

    cd /opt/vesana
    nano .env          # set FIELD_ENCRYPTION_KEY= to the OLD value
    docker compose -f docker-compose.prod.yml up -d --force-recreate
    

    Only the .env takes effect; secrets/field_encryption_key is a copy nothing reads (keep it in sync anyway). Same for the login signing key, if existing sessions should keep working. 3. Log in as super admin → Admin → System → Backup & DR. 4. "Upload" → pick the .sql.gz from your off-site copy. It appears as a manual backup in the catalog. 5. On the uploaded backup, click "Restore" → type the hostname → 2FA confirmation → restore runs. 6. Verify: log in with the original admin, open a host with an encrypted snmp_community (the value must be intact), check a service's check history (hypertable chunks must be there).

If an encrypted field shows up empty/broken in step 6, FIELD_ENCRYPTION_KEY wasn't the right one.

Vesana tells you itself

After every restore the instance samples whether the encrypted values are readable with the current FIELD_ENCRYPTION_KEY — across every place that holds secrets (device credentials, credential sets, collector communities, instance settings). If the key does not match, a warning naming the affected areas replaces the success message, and the same text lands in the diagnostics log (restore_key_mismatch). This does not replace step 6, but you find out immediately instead of days later.

Disaster drill

Restore without testing is hope, not backup. Recommendation: run through the fresh-server runbook once a quarter on a separate VM — including step 6.

Next

  • Updates — the updater creates a pre-update backup in the same catalog before every update; without auto-rollback, that backup is the recovery strategy
  • 2FA — step-up confirmation on restore