Backup and restore
Two different things, for two different accidents.
| Encrypted bundle | Scheduled local backup | |
|---|---|---|
| What it holds | The configuration: monitors and their credentials, rules, channels, status pages, accounts, tokens. | The whole database, as one file, plus secret.key. |
| Where it goes | Wherever you put it — it is encrypted with a passphrase you choose. | /data/backups/, inside the same volume. |
| What it saves you from | A lost server, a move to new hardware, a fresh install. | The change you regret ten minutes later, an upgrade that went wrong. |
| What it does not hold | Metrics history, alert state and history, the audit log, sessions. | Metrics history (vm/, vm-long/) and logs (vlogs/). |
Neither replaces an archive of the whole volume if you want the graphs back too; that is at the bottom of this page.
The encrypted bundle
Section titled “The encrypted bundle”Settings → Backup → Download the bundle. Choose a passphrase of at least 16 characters — three words is enough — and confirm it. The file that comes down is plain JSON with an encrypted body:
{ "format": "dumbmonit-backup", "version": 1, "created_at": "2026-09-22 19:04:11", "source_version": "0.1.0-alpha.3", "summary": { "targets": 12, "channels": 2, "rules": 41 }, "kdf": { "algorithm": "argon2id", "salt": "…", "m_cost": 32768, "t_cost": 3, "p_cost": 1 }, "cipher": "aes-256-gcm", "payload": "…"}The header stays readable so you can tell what a file is before typing anything.
Everything else — addresses included — is inside payload, encrypted with
AES-256-GCM under a key derived from your passphrase by Argon2id. There is no
recovery: a bundle whose passphrase is lost is lost.
What is in it
Section titled “What is in it”| Section | Contents |
|---|---|
targets | Monitors: name, kind, address, interval, options, parent, relay agent, and the credential. |
rules | Alert rules, including built-in ones you have retuned or disabled. |
rule_overrides | Per-monitor thresholds and exclusions. |
channels | Notification channels, their settings, their per-channel policy and their secrets. |
notify_policy | The global notification policy (grouping, hourly cap, flapping, public URL). |
silences | Maintenance windows. |
status_pages | Public status pages and the services each one lists. |
incidents | Incidents and maintenance announcements, with their updates. |
users | Accounts: username, display name, role, SSO link, enabled or not. |
agent_tokens | Agent enrolment tokens, as hashes. Installed agents keep pushing after a restore. Tokens from an older backup are renamed to opaque tok_… identifiers. |
api_tokens | API tokens, as hashes. Existing tokens keep working. |
push_monitors | Heartbeat tokens, so the URLs your cron jobs already call stay the same. |
locations | World map locations (name, address, spot, site, main site); each monitor keeps its location. |
Monitors are matched on kind + address, which is also the unique key of the table: importing the same bundle twice never creates a second copy of anything.
Account passwords
Section titled “Account passwords”By default accounts are exported without their password hash, TOTP secret or recovery codes. Restored accounts then exist with the right name and role, but nobody can sign in with them until an administrator sets a password — or until they sign in through SSO, which needs nothing from the bundle.
Turn on Include account passwords and 2FA secrets when you are moving an instance rather than copying its configuration. An Argon2 hash in a file that travels can be attacked offline for as long as the attacker likes; the default is the safe one.
What is not in it
Section titled “What is not in it”Metrics history (that lives in VictoriaMetrics, not in the database), alert state and history, anomaly baselines, the audit log, open sessions, the registration and last-seen of agents, and the probe caches of the Proxmox and Synology panels. They are observations, not settings: they come back on their own.
And not the instance secret. That is the point: the bundle carries its
secrets re-encrypted under your passphrase precisely so that it can be restored
somewhere that has never seen secret.key.
Restoring onto a fresh instance
Section titled “Restoring onto a fresh instance”- Start the new container and create the first administrator account at
/setup, with the setup code printed in its logs. That account is yours; the restore will not touch it. - Settings → Backup → Restore a bundle. Pick the file, type the passphrase, and press Check this backup.
- Read the report. It says, section by section, what would be created, what would be updated because it differs from what is already here, and what is already identical — plus a note for anything it could not place, such as a rule that pointed at a monitor the bundle does not contain.
- Press Restore for real.
Three rules the restore never breaks:
- Nothing is ever deleted. A restore makes what the bundle describes exist; anything the instance has in addition stays.
- Nothing is ever duplicated. Every item is matched on its natural key —
kind + address for a monitor,
uidfor a rule, name for a channel, slug for a status page. Restoring the same bundle twice reports everything as already identical and writes nothing. - Existing accounts are left alone. An account whose username is already here is skipped, never overwritten: a file must not be able to change the role or the password of the administrator running the restore.
A dry run is not a simulation of the restore, it is the restore, run inside a transaction that is rolled back at the end. The report cannot be wrong about what applying would do.
A bundle written by a newer DumbMonit is refused with a sentence that says so — upgrade first, then restore. A bundle that has been modified, or whose passphrase is wrong, is refused too: AES-GCM cannot tell the two apart, and neither can the message.
From the command line
Section titled “From the command line”Both routes need an administrator session, not an API token: a token lives in a script, and a script has no business exporting every credential of the instance.
# Sign incurl -s -c jar -X POST https://localhost:8080/api/auth/login \ -H 'content-type: application/json' -H 'X-Requested-With: DumbMonit' \ -d '{"username":"admin","password":"…"}'
# Exportcurl -s -b jar -X POST https://localhost:8080/api/backup \ -H 'content-type: application/json' -H 'X-Requested-With: DumbMonit' \ -d '{"passphrase":"three words are enough"}' -o backup.json
# Dry run, then applyjq -n --slurpfile b backup.json \ '{bundle: $b[0], passphrase: "three words are enough", apply: false}' > restore.jsoncurl -s -b jar -X POST https://localhost:8080/api/backup/restore \ -H 'content-type: application/json' -H 'X-Requested-With: DumbMonit' \ -d @restore.json | jqScheduled local backups
Section titled “Scheduled local backups”DumbMonit writes a copy of its own database on a schedule, by default once a
day, keeping the last seven, into /data/backups/:
/data/backups/dumbmonit-20260922-030000.db/data/backups/dumbmonit-20260922-030000.keyThe .db file is produced with SQLite’s VACUUM INTO: an online, consistent
copy written while the server keeps running, not a file copy that could catch
the database mid-write. sqlite3 opens it as it is, with no journal to replay.
The .key beside it is the copy of secret.key — together, the pair is enough
to rebuild the instance.
They live in the same volume as the database, so they undo a mistake, not a lost disk. Copy them off the machine if you want more than that.
Settings
Section titled “Settings”| Variable | Default | Role |
|---|---|---|
DUMBMONIT_BACKUP_ENABLED | 1 | 0, false, no or off to stop writing scheduled backups. |
DUMBMONIT_BACKUP_DIR | <data dir>/backups | Where they are written. Point it at a second volume to survive the first one. |
DUMBMONIT_BACKUP_INTERVAL_HOURS | 24 | Hours between two backups. The first one happens one interval after startup, not at startup. |
DUMBMONIT_BACKUP_KEEP | 7 | How many are kept. The oldest are removed, with their .key. |
Nothing else in the directory is ever touched: only files named
dumbmonit-<timestamp>.db and their .key are rotated.
When the instance secret comes from DUMBMONIT_SECRET there is no file to copy,
and none is written — keep that value in your password manager instead. The
Backup section says which of the two cases you are in.
Knowing that it ran
Section titled “Knowing that it ran”Each run publishes three series:
| Metric | Meaning |
|---|---|
dumbmonit_instance_backup_last_success_seconds | Unix timestamp of the last successful backup. A failed attempt never refreshes it. |
dumbmonit_instance_backup_last_status | 1 if the last attempt succeeded, 0 if it failed. |
dumbmonit_instance_backup_size_bytes | Size of the last file written. |
The built-in rule DumbMonit backup did not run
(instance_backup_missing) fires when the last success is more than two days
old — one missed night can be a restart, two is a problem. The series only
exists once a first backup has been written, so an instance with the schedule
turned off is never nagged about it.
Settings → Backup shows the same thing without MetricsQL: the last run and whether it succeeded, the files kept and their size, and Back up now to write one immediately.
Restoring one
Section titled “Restoring one”Stop the stack, put the file back under the database’s name, and restore the key next to it:
docker compose stopdocker run --rm -v dumbmonit-data:/data alpine sh -c ' cp /data/backups/dumbmonit-20260922-030000.db /data/dumbmonit.db && cp /data/backups/dumbmonit-20260922-030000.key /data/secret.key && rm -f /data/dumbmonit.db-wal /data/dumbmonit.db-shm'docker compose startRemoving the -wal and -shm files matters: they belong to the database you
just replaced, and SQLite would try to apply them to the new one.
To read a backup without restoring it, copy it out and open it with sqlite3:
docker run --rm -v dumbmonit-data:/data -v "$PWD:/out" alpine \ cp /data/backups/dumbmonit-20260922-030000.db /out/sqlite3 dumbmonit-20260922-030000.db 'SELECT name, kind, address FROM targets;'Credentials in that file stay unreadable without the matching secret.key.
Archiving the whole volume
Section titled “Archiving the whole volume”The only way to keep the graphs as well. One named volume, dumbmonit-data,
holds everything: the database, the secret, the local backups and the
VictoriaMetrics data.
docker compose stopdocker run --rm -v dumbmonit-data:/data -v "$PWD:/backup" alpine \ tar czf /backup/dumbmonit-data.tgz -C /data .docker compose startTo restore, create the volume, extract the archive into it the same way, then
docker compose up -d.
If space matters, --exclude=./vm --exclude=./vm-long --exclude=./vlogs keeps the
archive small: the database and the secret are the setup; vm/ (raw metrics),
vm-long/ (5-minute aggregates) and vlogs/ (logs) are only history. Stop the
container first if you want these directories consistent.
What to do before an upgrade
Section titled “What to do before an upgrade”- Settings → Backup → Back up now — thirty seconds, and it includes the secret.
- Download a bundle as well if the upgrade is a big one, or if you are moving to another machine.
docker compose pull && docker compose up -d. Migrations run at startup.
If it goes wrong, restore the local backup and start the previous image tag.