Skip to content

Backups & Restore ​

Your node holds the only copy of your community's history. Nobody else has it.

Two commands:

bash
bash scripts/backup.sh                                            # make a backup
bash scripts/restore.sh --verify backups/dissent-<stamp>.tar.gz   # prove it works

Run the second one at least once. An untested backup is a guess.

Why it is not just the database ​

The archive contains three things, and the database alone is useless:

In the archiveWhat it is
database.dumpmessages, servers, members — everything
envMESSAGE_ENC_KEY, PLUGIN_CONFIG_KEY
node_key.pemyour node's federation identity

Message content is encrypted at rest with MESSAGE_ENC_KEY; plugin configs and any stored S3 credentials with PLUGIN_CONFIG_KEY.

A database-only backup restores to data nobody can read

Every row present, every message unrecoverable. The ciphertext survives a pg_dump perfectly well; the key that opens it does not live in the database.

Lose node_key.pem and your node cannot prove it is the same node to any peer that trusted it. Federation does not auto-heal that, by design.

This is not hypothetical. The Dissent project's own node had exactly this gap — nightly database backups shipped off-box, encryption keys that never left the machine — until it was found and fixed on 2026-08-13. restore.sh --verifyfails an archive with no env for that reason, rather than merely noting it.

Set it up ​

1. Encrypt the archive ​

The archive contains your .env, which is every secret your node holds. Without encryption it sits on disk in plaintext.

bash
age-keygen -o backup.key                      # prints a public key line
export BACKUP_AGE_RECIPIENT="age1..."         # that public line

Keep backup.key off this server

A password manager, another machine — anywhere else. A backup encrypted to a key that exists only on the machine you are protecting against is not a backup.

For the same reason, do not store the key beside the archives it opens. One directory read should not be a total compromise, and one deletion should not be a total loss.

2. Send it off the box ​

bash
export BACKUP_SSH_TARGET="user@otherhost:/backups/dissent"

A backup on the same disk as the node is not a backup. One disk failure takes both.

3. Schedule it ​

bash
17 4 * * *  cd /path/to/dissent-node && bash scripts/backup.sh >> backups/backup.log 2>&1

Once a month, run restore.sh --verify on the newest archive. Backups fail silently: a truncated dump, a lost key and a full disk all look exactly like success until you need one.

All settings ​

VariableDefaultDescription
BACKUP_DIR./backupswhere archives are written
BACKUP_KEEP_DAYS14local retention
BACKUP_AGE_RECIPIENT—age public key; enables encryption
BACKUP_SSH_TARGET—user@host:/path for an off-box copy
COMPOSE_FILEdocker-compose.prod.yml

Restoring ​

bash
# Always check first — this changes nothing:
bash scripts/restore.sh --verify backups/dissent-<stamp>.tar.gz

# Then, when you mean it (asks you to type RESTORE):
bash scripts/restore.sh backups/dissent-<stamp>.tar.gz

For an encrypted archive, point at the private key:

bash
BACKUP_AGE_IDENTITY=/path/to/backup.key bash scripts/restore.sh --verify backups/dissent-<stamp>.tar.gz

Restoring overwrites the database, .env and the node identity. Your current .env is copied to .env.before-restore-<stamp> first.

What is not in the archive ​

Uploaded files — attachments, avatars, clips. They live in the dissent-storage volume, or in your own bucket if you attached one (see Storage). Back that up separately: with S3 your provider handles durability, and with local storage you want the volume included in whatever backs up the rest of the machine.

The database records what those files are; it does not contain them.