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.
On this page
What needs to be backed up?
./postgres/
(PostgreSQL database)
Users, posts, drafts, connected Fediverse accounts, settings, analytics data. By far the most important part.
Method: pg_dump (not a directory copy)
./uploads/
(Attachments)
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)
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)
Security-relevant events as JSON lines in monthly files. Only needed if you have to retain them.
Method: tar archive
./plugins/
(Installed plugins)
Plugins you installed yourself. They can be downloaded again if needed, a backup still makes sense.
Method: tar archive
docker-compose.yml
(Compose configuration)
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)
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
umask 077) and encrypted storage away from the server.
./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.
#!/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:
crontab -e
Add one of the following lines, depending on the frequency you want:
# 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:
# ── 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"
.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.
Stop the stack
docker compose down
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
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
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;"
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
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 ./
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.