Skip to content

Backup & Disaster Recovery

Vesana has a built-in backup system under Admin → Maintenance → Backups. 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 "Back up now" button (also: uploaded backups carry this label) selectable
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 → Maintenance → Backups → 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.

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 honestly shows "File missing" instead of a dead download link.

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.

The restore 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.customer.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 → Maintenance → Backups. 4. "Upload backup" → 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