PulseNode can back up itself (the panel: database, encryption key, session secret, .env.local)
and the managed databases it provisions, on a schedule, to a local folder and/or any
S3-compatible bucket (AWS S3, Backblaze B2, Cloudflare R2, Wasabi, MinIO, …).
Open Backups in the panel, or use the API (/api/backups/...).
| Destination | Where files go: local (a folder inside the panel's data directory) or s3 (endpoint, bucket, prefix, access key, secret key). |
| Schedule | What and when: target panel or one managed database, frequency (hourly / daily at HH:00 / weekly on a weekday), how many copies to keep, which destinations, whether to notify on success. |
| History | One row per run, with size and the result per destination. |
Runs happen one at a time. A schedule that was due while the panel was down runs once after the panel starts (never a burst). A run that is still going is never started twice. Times use the server's local time zone.
Each schedule keeps its newest N (default 7) successful copies per destination and deletes older ones automatically. Failed rows older than 180 days (or beyond the newest 200) are trimmed; rows that still point at a stored file are governed only by retention.
A failed run (dump produced nothing, upload failed on any destination, passphrase missing, …) opens a
critical backup.failed alert — visible in Alerts and sent to your notification channels (Slack,
email, webhook, …) — once per schedule until a later run succeeds, which resolves it. "Notify on
success" additionally sends an informational message after each good run. A panel restart during a run
is not a failure: that run is recorded as interrupted and the schedule stays due.
- Local — default
<data dir>/backups/scheduled(inside thepn-go-datavolume). A custom folder must be inside the data directory, or inside a directory you mount into thego-apicontainer and list inPULSENODE_BACKUPS_EXTRA_DIRS(colon separated). A local copy on the same disk does not protect against losing the server — add an off-site destination. - S3-compatible —
endpointishost[:port](a pastedhttps://hostis accepted),regionis optional,pathStyleis needed by MinIO and some others. Credentials are stored encrypted and are never returned by the API (only the last 4 characters of the access key). Changing the endpoint or bucket requires entering both keys again, so stored credentials can never be redirected to a new server. - Private addresses are blocked (loopback, LAN, link-local, cloud metadata) to stop the panel being
used as a proxy into your network. Running MinIO on your LAN or on the Docker network? Set
PULSENODE_BACKUPS_ALLOW_PRIVATE=truein.env.localand restart. - Use Test on a destination: it writes, reads back and deletes a tiny object.
An archive of: a consistent snapshot of the SQLite database (users, projects, encrypted secrets,
settings, destinations), aes-key, jwt-secret, .env.local (when the panel can read it — the
install directory is mounted at /workspace), and the scan/SBOM history. It also carries a manifest
with a SHA-256 of every file.
It is always encrypted with a passphrase you choose (Backups → Passphrase, ≥ 12 characters): tar + gzip, then AES-256-GCM in 64 KiB authenticated chunks, key derived with scrypt (N = 2^15). The passphrase is stored (encrypted) in the panel only so scheduled runs can use it.
Write the passphrase down somewhere that is not this server. It cannot be recovered from the backup, and if the server is lost the panel's own copy is lost with it. Changing the passphrase does not re-encrypt old backups — those still need the old one.
Because the archive contains the encryption key, anyone with the file and the passphrase can read every secret in your panel. Treat both like a root password.
A dump of one managed database (pg_dump, mysqldump, mongodump --archive, or the Redis RDB),
gzipped, and — if you leave Encrypt on (default) — encrypted with a key derived from the panel's
AES key. After dumping, the file is read back end to end (gzip checksum, encryption) and must look like
a dump of that engine; an empty or damaged dump fails the run instead of being uploaded.
An encrypted database backup can be restored by a panel that has the same AES key — which is exactly what the panel backup carries. External (connected) databases are not covered.
Backups → History → Restore (or POST /api/backups/history/{id}/restore {"confirm":"restore"}).
This overwrites the database's current data and is refused while another backup or restore runs.
You need: the .pnbak file (from a destination, or Download in the panel), your passphrase, and a
server with Docker.
-
Install PulseNode on the new server (
install.sh, ordocker compose uponce so the volumes exist), then stop it:docker compose down(this keeps the volumes). -
Copy the backup and put the passphrase in a file (kept out of shell history):
mkdir -p /root/restore && cd /root/restore # copy panel-XXXX.pnbak here read -rs -p 'Passphrase: ' P && printf '%s' "$P" > pass && unset P
-
Verify it (decrypts and checks every checksum, writes nothing):
cd /path/to/PulseNode # the install directory docker compose run --rm --no-deps --entrypoint pulsenode \ -v /root/restore:/restore go-api \ verify-backup --file /restore/panel-XXXX.pnbak --passphrase-file /restore/pass
-
Extract it into a new folder (it refuses to overwrite existing files unless you add
--force):docker compose run --rm --no-deps --entrypoint pulsenode \ -v /root/restore:/restore go-api \ restore-panel --file /restore/panel-XXXX.pnbak --passphrase-file /restore/pass --out /restore/out
You now have
pulsenode.db,aes-key,jwt-secret,.env.local(and the scan history) in/root/restore/out. These work without a running panel, on any machine with thepulsenodebinary or image. -
Put the files in place.
-
.env.local→ the install directory (it holdsAES_KEY/JWT_SECRET, which the panel prefers over the files below). If it was not in the archive, setAES_KEY=<contents of aes-key>andJWT_SECRET=<contents of jwt-secret>in your.env.local. -
The database and key files go into the volumes (find their exact names with
docker volume ls | grep pn-):docker run --rm -v <project>_pn-sqlite-data:/data -v /root/restore/out:/in alpine sh -c \ 'rm -f /data/pulsenode.db-wal /data/pulsenode.db-shm; cp /in/pulsenode.db /data/pulsenode.db' docker run --rm -v <project>_pn-go-data:/var/lib/pulsenode -v /root/restore/out:/in alpine sh -c \ 'cp /in/aes-key /in/jwt-secret /var/lib/pulsenode/ 2>/dev/null; cp /in/scans.json /in/sboms.json /var/lib/pulsenode/ 2>/dev/null; true'
-
-
docker compose up -d, then sign in with your old admin account. Projects, domains, destinations, schedules and notification channels are back; deployed apps are not (re-deploy them from the Projects page — their images and data volumes are separate from the panel). -
Check Backups → Status, run a schedule once, and consider rotating secrets that lived on the old server.
Why the AES key matters: every stored secret (project env vars, GitHub tokens, destination credentials, DB passwords) is encrypted with it. A database restored without its key cannot decrypt them — the panel then refuses deploys with "cannot decrypt env: AES key mismatch" instead of passing scrambled values to your apps. The panel backup always contains the matching key.
pulsenode verify-backup --file BACKUP.pnbak --passphrase-file FILE
pulsenode restore-panel --file BACKUP.pnbak --passphrase-file FILE --out DIR [--force]
--passphrase-file - reads the passphrase from standard input. Both exit non-zero on a wrong
passphrase, a damaged or truncated file, a hash mismatch, or (restore) an existing target file.
All routes are under /api/backups, need a signed-in session, and return JSON (lists are plain
arrays, never null).
GET/POST destinations PATCH/DELETE destinations/{id}
POST destinations/{id}/test POST destinations/test (draft; pass "id" to reuse stored keys)
GET/POST schedules PATCH/DELETE schedules/{id} POST schedules/{id}/run
GET history?limit=&scheduleId= GET history/{id}/download DELETE history/{id}
POST history/{id}/restore (database backups only; body {"confirm":"restore"})
GET status GET/POST passphrase
Every run, restore, download and destination change is written to the audit log.
- Panel backups can only be restored offline (above), by design — restoring over a running panel would replace the database it is using.
- Database dumps run inside the database container; very large databases need enough free disk space in the panel's data volume for the temporary file during the run.
- Object stores are written with the standard S3 API; uploads larger than 5 GiB use multipart automatically but have not been tested against every provider.