VPS Snaps

How to back up and restore a Mastodon server

Back up four things, in the order Mastodon's documentation ranks them: the PostgreSQL database (pg_dump -Fc), the secrets in .env.production, uploaded files in public/system unless they live in object storage, and, optionally, Redis. To restore, follow Mastodon's migration guide: create the database from template0, load it with pg_restore, put the files and .env.production back, start Mastodon, then rebuild home feeds with tootctl feeds build.

9 min readUpdated Checked against official documentation

What to back up

Paths here are for a source install made with the Production Guide, in /home/mastodon/live. With Docker Compose, .env.production and public/system sit next to docker-compose.yml.

WhatWhereIf you lose it
PostgreSQL databasemastodon_productionEverything: accounts, posts, follows. The server is gone.
Secrets.env.productionEveryone is logged out; two-factor login and Web Push stop working.
Uploaded filespublic/systemAvatars, headers and media attachments. Mastodon keeps running.
Redis/var/lib/redis/dump.rdbQueued Sidekiq jobs and their retries. Feeds can be rebuilt.
Elasticsearch, if usedIts data folderNothing: tootctl search deploy rebuilds the indexes.
nginx, systemd units, certificates/etc/nginx/sites-available/mastodon, /etc/systemd/system/mastodon-*.serviceConvenience only. They can be recreated.

Mastodon's backup page considers hardware failure only. A deleted account, a bad moderation action or a broken upgrade is a human or software error, and the fix for those is history: dumps from several past days, not just last night's.

The secrets in .env.production

The docs call the secrets the easiest part, since they never change. They also can't be recreated. As of Mastodon 4.7 they are:

  • SECRET_KEY_BASE signs browser sessions. A new value logs everyone out.
  • ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY, ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY and ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT encrypt data inside the database, including two-factor secrets since 4.3. Mastodon refuses to start without them, and its sample config warns that changing them causes data loss. A database backup without these three is incomplete.
  • VAPID_PRIVATE_KEY and VAPID_PUBLIC_KEY sign Web Push notifications. New keys break existing push subscriptions.
  • OTP_SECRET exists only on servers set up before 4.4.0, which removed it. The configuration page still lists it; keep it if your file has it.

The file also holds the database, SMTP and object storage passwords, and LOCAL_DOMAIN, which can't be changed safely once set. Store it only where the backups are encrypted.

Dump PostgreSQL in custom format

Run pg_dump as the mastodon user, as the migration guide does. It reads a consistent snapshot while Mastodon keeps running:

Terminal
sudo -u mastodon pg_dump -Fc mastodon_production -f /home/mastodon/mastodon.dump
  • -Fc writes the custom format: compressed, read by pg_restore, which can load it with parallel jobs or restore single tables.
  • The Production Guide creates the mastodon role without a password; it connects over the local socket as the matching system user.
  • Big databases take time: the migration guide warns of hours for a 15 GB dump.

With the repository's docker-compose.yml, PostgreSQL runs in the db service, and mastodon:setup defaults to user postgres and database postgres there; check DB_USER and DB_NAME in .env.production. Add -T: docker compose exec allocates a terminal by default, and a binary dump should not pass through one.

Terminal
docker compose exec -T db pg_dump -U postgres -Fc postgres > mastodon.dump

A Docker restore follows the same steps, into an empty database, with the file fed in on standard input. Parallel -j needs a file path, so it is left out:

Terminal
docker compose exec -T db pg_restore -U postgres --no-owner -d postgres < mastodon.dump

Uploaded files: originals and the remote cache

With local storage, uploads live in public/system, or in PAPERCLIP_ROOT_PATH if you set it. Mastodon also stores copies of media from other servers, and since version 3.1.4 they go in their own cache/ folder:

FolderHoldsBack up?
public/system/cacheCopies of other servers' media attachments, avatars, headers and emoji, plus all link preview imagesNo
Everything else in public/systemYour users' media, avatars and headers, your custom emoji and site images, and data exportsYes

The cache is often the largest part, and Mastodon fetches it again when needed. RAILS_ENV=production bin/tootctl media usage, run as mastodon in ~/live, shows the total and local size of each kind of file. Servers installed before 3.1.4 may still keep remote files mixed in with local ones until tootctl upgrade storage-schema moves them.

With object storage (S3_ENABLED=true), Mastodon's docs say you don't need to back up the bucket, since the provider handles hardware failure. That doesn't cover deletion, a lost account or leaked keys. To be safe, copy the bucket into another provider's account with rclone.

Redis and Elasticsearch

The docs call losing Redis almost harmless: you lose queued Sidekiq jobs and scheduled retries, and home and list feeds are rebuilt with tootctl. Copying /var/lib/redis/dump.rdb is enough if you want it; the Redis guide explains why that copy is safe while Redis runs. Elasticsearch needs no backup: tootctl search deploy rebuilds it from the database.

Automate it

This script uses restic, set up as in that guide with /etc/restic/env, so every copy is encrypted, versioned and off the server. It runs as root:

/usr/local/bin/mastodon-backup.sh
#!/usr/bin/env bash
set -euo pipefail
PATH=/usr/local/bin:/usr/bin:/bin
cd /
set -a; . /etc/restic/env; set +a

LIVE=/home/mastodon/live
VERSION=$(sudo -u mastodon git -C "$LIVE" describe --tags)

restic backup --tag mastodon-db --tag "$VERSION" --stdin-filename mastodon.dump \
  --stdin-from-command -- \
  sudo -u mastodon pg_dump -Fc -Z 0 mastodon_production

restic backup --tag mastodon-files \
  --exclude "$LIVE/public/system/cache" \
  "$LIVE/.env.production" "$LIVE/public/system" /var/lib/redis/dump.rdb \
  /etc/nginx/sites-available/mastodon /etc/systemd/system/mastodon-*.service

restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
Terminal
chmod 700 /usr/local/bin/mastodon-backup.sh
/etc/cron.d/mastodon-backup
15 4 * * * root /usr/local/bin/mastodon-backup.sh >> /var/log/mastodon-backup.log 2>&1
  • cd / starts in a folder the mastodon user can read, since its commands inherit root's working directory. PATH adds /usr/local/bin, where the restic guide installs restic.
  • git describe --tags prints the release the source checkout is on, such as v4.7.3. It becomes a tag on the database snapshot, so you know which version to restore onto.
  • -Z 0 turns off pg_dump's compression. The file stays in custom format, restic can deduplicate unchanged data between nights, and it compresses the repository itself. If pg_dump fails, restic saves no snapshot.
  • Delete any path your server doesn't have, such as dump.rdb when Redis persistence is off; restic reports missing paths as errors.
  • forget groups snapshots by host and paths, so the database and the files each keep 7 daily, 4 weekly and 6 monthly copies.

Restore onto a new server

These are the steps of Mastodon's migration guide, fed from the backup. Set up the new machine with the Production Guide but don't run mastodon:setup, and in the checkout step use the version the backup came from. Install restic, put back /etc/restic/env from the copy you keep off the server, and list the dumps with their version tags:

Terminal
set -a; . /etc/restic/env; set +a
Terminal
restic snapshots --tag mastodon-db

Load the database into a new, empty one:

Terminal
restic dump --tag mastodon-db latest mastodon.dump > /home/mastodon/mastodon.dump
Terminal
chown mastodon:mastodon /home/mastodon/mastodon.dump
Terminal
sudo -u mastodon createdb -T template0 mastodon_production
Terminal
sudo -u mastodon pg_restore -Fc -j4 -U mastodon -n public --no-owner --role=mastodon -d mastodon_production /home/mastodon/mastodon.dump
  • -T template0 copies PostgreSQL's untouched template, so nothing added to the default template can clash with the dump.
  • -j4 loads data and builds indexes with four parallel jobs; use your CPU count.
  • -n public restores only the public schema, where Mastodon's tables live.
  • --no-owner --role=mastodon makes mastodon own everything. If the new server's role has another name, change -U and --role.

Stop Redis before its file comes back, or it overwrites the restored copy, then restore the files. Owners are restored by numeric ID, which may belong to another user on the new machine, so reset them:

Terminal
systemctl stop redis-server
Terminal
restic restore latest --tag mastodon-files --target /
Terminal
chown -R mastodon:mastodon /home/mastodon/live/public/system /home/mastodon/live/.env.production
Terminal
chown redis:redis /var/lib/redis/dump.rdb

As the mastodon user (su - mastodon), in ~/live, compile the assets:

Terminal
RAILS_ENV=production bundle exec rails assets:precompile

As root, start everything:

Terminal
systemctl daemon-reload
Terminal
systemctl start redis-server
Terminal
systemctl enable --now mastodon-web mastodon-sidekiq mastodon-streaming
Terminal
systemctl restart nginx

Back as mastodon in ~/live, rebuild home and list feeds. With no username, it does every active user and prints how many it regenerated; this can take a long time on a big server. If you use Elasticsearch, run RAILS_ENV=production bin/tootctl search deploy too.

Terminal
RAILS_ENV=production bin/tootctl feeds build

Remote media is missing because cache/ was left out. To fetch the last week's again, use --force: the database still names those files, and without it tootctl skips them. bin/tootctl accounts refresh --all refetches avatars and headers for every remote account the server knows, which is a lot of requests on a big server.

Terminal
RAILS_ENV=production bin/tootctl media refresh --days 7 --force

Finally, point DNS at the new server and set up nginx and certificates as the migration guide describes. Moving to a new provider? See migrating a server.

Test the backup without a second live server

Don't start a full restored copy with network access. It carries your domain, your SMTP login and every local account's signing key, which the accounts table stores. With Sidekiq running, it can deliver posts to other servers and send email as your server.

Test the parts instead, on any machine with PostgreSQL of the same major version or newer:

Terminal
install -d -o postgres -m 700 /srv/restore-test
Terminal
restic dump --tag mastodon-db latest mastodon.dump > /srv/restore-test/mastodon.dump
Terminal
sudo -u postgres createdb -T template0 mastodon_restore_test
Terminal
sudo -u postgres pg_restore -j4 --no-owner -d mastodon_restore_test /srv/restore-test/mastodon.dump
Terminal
sudo -u postgres psql -d mastodon_restore_test -c "SELECT (SELECT count(*) FROM users) AS users, (SELECT count(*) FROM statuses WHERE local OR uri IS NULL) AS local_posts, (SELECT max(created_at) FROM statuses WHERE local OR uri IS NULL) AS newest_local_post, (SELECT count(*) FROM media_attachments WHERE remote_url = '' AND file_file_name IS NOT NULL) AS local_media;"

Run the same query on the live server (sudo -u mastodon psql mastodon_production). The counts should match as of the dump, and newest_local_post shows its age. Then restore the uploads and count the original files:

Terminal
restic restore latest --tag mastodon-files --target /srv/restore-test --include /home/mastodon/live/public/system
Terminal
find /srv/restore-test/home/mastodon/live/public/system/media_attachments/files -path '*/original/*' -type f | wc -l

That number should be close to local_media; a big gap means missing files. Clean up:

Terminal
sudo -u postgres dropdb mastodon_restore_test
Terminal
rm -rf /srv/restore-test

Common errors

ErrorFix
Mastodon now requires that these variables are set at startupThe three ACTIVE_RECORD_ENCRYPTION_* keys are missing. Restore .env.production from the same backup as the database.
Everyone is logged outSECRET_KEY_BASE differs from the old server's.
Push notifications stoppedThe VAPID keys changed. Restore the originals.
role "..." does not exist from pg_restoreRestore with --no-owner --role=mastodon, as above.
database "mastodon_production" already existsSetup or an earlier attempt made it. Drop it with dropdb if it holds nothing you need, then restore.
Empty home timelinesRun tootctl feeds build.

Frequently asked questions

What do I need to back up on a Mastodon server?
The PostgreSQL database, .env.production, and public/system if media is stored locally. Redis is optional, and Elasticsearch can be rebuilt.
Do I need to back up public/system/cache?
No. It holds copies of media from other servers. After a restore, tootctl media refresh with --days and --force fetches recent ones again.
Do I need to back up Mastodon media stored in S3?
Mastodon's docs leave hardware failure to the provider. A copy in another account still protects you from deletion, a lost account or leaked keys.
What happens if I lose Mastodon's Redis data?
Very little: queued Sidekiq jobs and retries are lost, and feeds are rebuilt with tootctl feeds build.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: