Installation

Backup & Restore

Regular backups are part of running an instance. A server can fail, an update can go wrong, or a misconfiguration can corrupt data. Without a backup there is no second chance. This page shows how to back up FediSuite by hand, automatically and off-site, and how to restore it completely in an emergency.

What needs to be backed up?

./postgres/ (PostgreSQL database)
Critical

Users, posts, drafts, connected Fediverse accounts, settings, analytics data. By far the most important part.

Method: pg_dump (not a directory copy)

./uploads/ (Attachments)
Important

Media files and PDFs of posts and drafts. Since FediSuite 2.0 the files of published posts are kept so they can be reused as a draft, so the directory grows accordingly.

Method: tar archive

.env (Configuration file)
Critical

All passwords, secrets and settings. The JWT_SECRET in it encrypts stored two-factor secrets: without the original value they cannot be decrypted after a restore.

Method: encrypted copy

./logs/ (Audit logs)
Optional

Security-relevant events as JSON lines in monthly files. Only needed if you have to retain them.

Method: tar archive

./plugins/ (Installed plugins)
Optional

Plugins you installed yourself. They can be downloaded again if needed, a backup still makes sense.

Method: tar archive

docker-compose.yml (Compose configuration)
Optional

The stack definition including your own changes or a docker-compose.override.yml. The bundled file can be fetched again from the repository at any time.

Method: plain copy

./letsencrypt/ (Traefik certificates)
Optional

Only with Traefik in the same stack (scenario 2): the acme.json with the certificates. It can be issued again, a backup saves trouble with rate limits.

Method: tar archive

The three principles

3-2-1

The 3-2-1 rule

At least 3 copies on 2 different media or systems, with 1 at a different location. A backup on the same server does not protect against losing the server.

∞

Automation

A backup that has to be started by hand will be forgotten at some point. Set up a cron job that backs up daily, and check its log now and then.

Test the restore

A backup that was never tested is not reliable. Check regularly that you can restore from it before an emergency happens.

Back up the database (pg_dump)

While PostgreSQL is running you should not back up the database by copying ./postgres/. The data files can be in an inconsistent state and look damaged after a restore.

The right way is pg_dump, the PostgreSQL tool for logical backups. It creates a consistent snapshot as an SQL dump while the database is running and is independent of the file structure. The examples use the user and database fedisuite from the .env.example. Adjust them if you set other POSTGRES_* values.

Simple dump (uncompressed)

Creates a readable SQL file, good for inspecting the content.

docker compose exec -T db \
    pg_dump -U fedisuite fedisuite \
    > backup_$(date +%Y-%m-%d).sql

Compressed dump recommended

Gzip-compressed, much smaller and ready for off-site storage.

docker compose exec -T db \
    pg_dump -U fedisuite fedisuite \
    | gzip > backup_$(date +%Y-%m-%d_%H-%M-%S).sql.gz

Check the backup content

Shows the first lines of the dump without unpacking it completely.

gunzip -c backup_2026-10-01_03-00-00.sql.gz | head -30
The dump is confidential: It contains, among other things, password hashes and the access tokens of the connected accounts. Treat it like a password: restrictive file permissions (for example umask 077) and encrypted storage away from the server.
Why not just copy ./postgres/? While running, PostgreSQL does not keep the data on disk in a state that could be copied consistently from outside. A directory snapshot can contain incomplete transactions or half-written pages. pg_dump queries the database over the normal protocol and therefore gets consistent data. A directory copy is only reliable with the database stopped (docker compose stop db), and only for the same PostgreSQL major version.

Back up files

Besides the database, the uploads and the configuration have to be backed up. Uploads can take a lot of space, factor that in when choosing the backup target.

Archive the uploads

Creates a compressed archive of the uploads directory.

tar -czf uploads_$(date +%Y-%m-%d_%H-%M-%S).tar.gz ./uploads/

Back up the .env

The .env contains passwords and secrets, back it up separately and encrypted.

# Plain copy (only if the target itself is encrypted)
cp .env backups/env_$(date +%Y-%m-%d)

# Encrypted with GPG (recommended for off-site storage)
gpg --symmetric --cipher-algo AES256 \
    --output backups/env_$(date +%Y-%m-%d).gpg .env

Back up plugins, logs and the Compose file

Optional, but recommended if you made changes of your own.

tar -czf plugins_$(date +%Y-%m-%d).tar.gz ./plugins/
tar -czf logs_$(date +%Y-%m-%d).tar.gz ./logs/
cp docker-compose.yml backups/docker-compose_$(date +%Y-%m-%d).yml

Automatic backup script

The script backs up the database, uploads and configuration in one step, stamps every backup with a time and cleans up old backups. Adjust FEDISUITE_DIR and BACKUP_DIR to your paths.

/usr/local/bin/fedisuite-backup.sh
#!/bin/bash
set -euo pipefail
umask 077

# ── Configuration ────────────────────────────────────────────────
FEDISUITE_DIR="/opt/fedisuite"          # Path to the FediSuite directory
BACKUP_DIR="/var/backups/fedisuite"     # Target for backups
KEEP_DAYS=7                             # delete daily backups older than X days
DB_NAME="fedisuite"
DB_USER="fedisuite"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
DAILY="$BACKUP_DIR/daily"
LOG_PREFIX="[fedisuite-backup][$DATE]"

# ── Preparation ─────────────────────────────────────────────────
mkdir -p "$DAILY" "$BACKUP_DIR/weekly" "$BACKUP_DIR/monthly"
cd "$FEDISUITE_DIR"

echo "$LOG_PREFIX Start"

# ── 1. Database (pg_dump) ───────────────────────────────────────
echo "$LOG_PREFIX Backing up the database..."
docker compose exec -T db \
    pg_dump -U "$DB_USER" "$DB_NAME" \
    | gzip > "$DAILY/db_${DATE}.sql.gz"
gzip -t "$DAILY/db_${DATE}.sql.gz"
echo "$LOG_PREFIX  ✓ Database: db_${DATE}.sql.gz ($(du -sh "$DAILY/db_${DATE}.sql.gz" | cut -f1))"

# ── 2. Uploads ───────────────────────────────────────────────────
echo "$LOG_PREFIX Backing up the uploads..."
tar -czf "$DAILY/uploads_${DATE}.tar.gz" \
    -C "$FEDISUITE_DIR" uploads/
echo "$LOG_PREFIX  ✓ Uploads: uploads_${DATE}.tar.gz ($(du -sh "$DAILY/uploads_${DATE}.tar.gz" | cut -f1))"

# ── 3. Configuration ─────────────────────────────────────────────
echo "$LOG_PREFIX Backing up the configuration..."
cp "$FEDISUITE_DIR/.env" "$DAILY/env_${DATE}"
cp "$FEDISUITE_DIR/docker-compose.yml" "$DAILY/compose_${DATE}.yml"
echo "$LOG_PREFIX  ✓ .env and docker-compose.yml backed up"

# ── 4. Keep weekly and monthly copies ───────────────────────────
if [ "$(date +%u)" = "7" ]; then
    cp "$DAILY/db_${DATE}.sql.gz" "$BACKUP_DIR/weekly/"
fi
if [ "$(date +%d)" = "01" ]; then
    cp "$DAILY/db_${DATE}.sql.gz" "$BACKUP_DIR/monthly/"
fi

# ── 5. Clean up old backups ───────────────────────────────────
echo "$LOG_PREFIX Cleaning up backups (${KEEP_DAYS}d / 28d / 90d)..."
find "$DAILY" -maxdepth 1 -type f -mtime "+$KEEP_DAYS" -delete
find "$BACKUP_DIR/weekly" -maxdepth 1 -type f -mtime +28 -delete
find "$BACKUP_DIR/monthly" -maxdepth 1 -type f -mtime +90 -delete

echo "$LOG_PREFIX Backup completed successfully."
ls -lh "$DAILY" | tail -10

Install the script and make it executable

Runs as root or as a user with access to Docker.

# Create the script
nano /usr/local/bin/fedisuite-backup.sh

# Make it executable
chmod +x /usr/local/bin/fedisuite-backup.sh

# Test it once
/usr/local/bin/fedisuite-backup.sh

Set up a cron job

A cron job runs the script automatically and regularly. Open the crontab of the user the script should run as and add an entry:

bash
crontab -e

Add one of the following lines, depending on the frequency you want:

crontab
# Daily at 03:00 (recommended)
0 3 * * * /usr/local/bin/fedisuite-backup.sh >> /var/log/fedisuite-backup.log 2>&1

# Twice a day: 03:00 and 15:00
0 3,15 * * * /usr/local/bin/fedisuite-backup.sh >> /var/log/fedisuite-backup.log 2>&1

Check the backup log

Check regularly that the backups really run through. The script aborts on any error.

tail -50 /var/log/fedisuite-backup.log

Backup rotation

Without rotation the backup directory grows without limit. The script above keeps the daily backups for KEEP_DAYS days, copies the database dump to weekly/ and monthly/ on Sundays and on the first of the month, and deletes files there after 28 and 90 days. You adjust the retention at KEEP_DAYS, -mtime +28 and -mtime +90.

  • •/var/backups/fedisuite/daily/: database, uploads and configuration of the last days
  • •/var/backups/fedisuite/weekly/: weekly database dumps of the last 4 weeks
  • •/var/backups/fedisuite/monthly/: monthly database dumps of the last 3 months

Store backups off-site

A backup on the same server does not protect against server failure, theft or data centre problems. Transfer backups regularly to an external target. rclone is a common tool for it, it speaks many storage services and SFTP with a uniform syntax. rsync or borg to a server of your own work as well.

Install rclone

Through your distribution's package manager, for example on Debian and Ubuntu:

sudo apt install rclone

Configure the remote target

Interactive wizard: pick your provider, for example SFTP or S3-compatible storage.

rclone config

Upload the backups

sync makes the target a mirror of the local directory and also deletes files there that disappeared locally through the rotation. If the target should keep backups longer than the server, use rclone copy.

# Example: SFTP target named "backup"
rclone sync /var/backups/fedisuite backup:fedisuite-backups --progress

# Target also keeps backups deleted locally
rclone copy /var/backups/fedisuite backup:fedisuite-backups --progress

Append the upload to the end of the backup script so every backup is stored off-site automatically:

fedisuite-backup.sh: addition at the end
# ── 6. Transfer off-site ─────────────────────────────────────────
echo "$LOG_PREFIX Transferring backups off-site..."
rclone copy "$BACKUP_DIR" backup:fedisuite-backups \
    --quiet --log-level ERROR
echo "$LOG_PREFIX  ✓ Off-site transfer completed"
Security: The .env contains passwords and the JWT secret, the database dump contains password hashes and account tokens. Encrypt both before the off-site transfer, for example with GPG, or use a target that encrypts on the client side (rclone crypt).

Restore: step by step

In an emergency, for example after a failed update, a damaged file system or a server move, you restore FediSuite like this. Run the steps in this order.

1

Stop the stack

docker compose down
2

Restore the configuration (if needed)

The original JWT_SECRET has to be kept, otherwise logins and stored two-factor secrets are invalid.

# Copy the .env back from the backup
cp /var/backups/fedisuite/daily/env_DATUM .env

# Or decrypt it if it was backed up with GPG
gpg --decrypt /var/backups/fedisuite/env_DATUM.gpg > .env
3

Start only the database container

PostgreSQL has to be running before you load data. The app only starts in step 7.

docker compose up -d db
# Wait until the health check passes
docker compose ps
4

Empty the existing database

docker compose exec -T db \
    psql -U fedisuite postgres \
    -c "DROP DATABASE IF EXISTS fedisuite;"

docker compose exec -T db \
    psql -U fedisuite postgres \
    -c "CREATE DATABASE fedisuite;"
Warning: This step irreversibly deletes all current database data. Check that you picked the right backup date. On a freshly created data directory it is not needed.
5

Load the database

# Load a compressed dump
gunzip -c /var/backups/fedisuite/daily/db_DATUM.sql.gz \
    | docker compose exec -T db \
      psql -v ON_ERROR_STOP=1 -U fedisuite fedisuite

# Load an uncompressed dump
docker compose exec -T db \
    psql -v ON_ERROR_STOP=1 -U fedisuite fedisuite \
    < /var/backups/fedisuite/daily/db_DATUM.sql
6

Restore the uploads

# Move an existing uploads folder aside (if present)
[ -d "./uploads" ] && mv ./uploads ./uploads.old

# Restore from the backup
tar -xzf /var/backups/fedisuite/daily/uploads_DATUM.tar.gz -C ./
7

Start the whole stack

docker compose up -d
docker compose logs -f app

The app runs init-db.js (idempotent: it only creates missing structures, existing data stays) and also brings a dump from an older version up to the current schema. Then the workers start.