For existing self-hosted instances
Upgrade PostgreSQL 15 to 18
This guide takes you step by step through switching the FediSuite database from PostgreSQL 15 to PostgreSQL 18. You need SSH access to your server and to the directory containing your .env and docker-compose.yml. You do not need to be a database expert, but you must follow every step in the given order. For a new installation you do not need this page: it already starts with PostgreSQL 18, see the Docker installation.
Important: do not just change the version number
PostgreSQL stores its data in a separate format for each major version. PostgreSQL 18 cannot read the files of PostgreSQL 15 and does not start on the old data directory. In addition, the official PostgreSQL 18 image keeps its data directory in a different place (/var/lib/postgresql instead of /var/lib/postgresql/data). The data must therefore be taken out of the old database with a backup (dump) and loaded into a new, empty database. That is exactly what this guide does.
- Do not just change
postgres:15-alpinetopostgres:18-alpineand restart. - Never point PostgreSQL 18 at the old data directory
./postgres. The old directory stays untouched as a fallback. - Never run
docker compose down -v, and do not delete anything unless this guide explicitly says so.
On this page
- Overview: what happens and do I have to do it now?
- Preparation and inventory
- Add the new settings to .env
- Update the self-hosting files
- Stop the application and create a backup
- Start PostgreSQL 18 with an empty database
- Load the data into PostgreSQL 18
- Start and check FediSuite
- After the switch
- Back to PostgreSQL 15 (rollback)
- Errors and solutions
1. Overview: what happens and do I have to do it now?
New FediSuite installations run on PostgreSQL 18. Existing installations were not switched automatically, because changing the major version always requires a data migration. PostgreSQL 15 receives security and bug fixes until 11 November 2027, PostgreSQL 18 until 14 November 2030. You do not have to switch immediately, but you should plan the switch before support ends.
This is how the switch works:
- You back up the whole instance (database,
.env, uploads, plugins). - You start PostgreSQL 18 with a new, empty data directory (
./postgres18). - You load the backed-up data into it.
- You start the app and the workers again.
The old PostgreSQL 15 directory ./postgres is only read, never modified. As long as the app and the workers have not yet run on PostgreSQL 18, you can return to PostgreSQL 15 at any time without data loss (see rollback).
Downtime: During the switch FediSuite and all workers are stopped. Nobody can sign in, and no posts are published and no statistics are refreshed. For a small instance this usually takes only a few minutes, for many gigabytes of data correspondingly longer. Plan a maintenance window outside important dates. Posts that become due in this time are picked up by the scheduler after the start; do not rely on that for time-critical posts.
Order of updates: Change only the database in this maintenance window. Do not update FediSuite to a new version at the same time. If you still run a version before 2.0.0, first do the upgrade to 2.0.0 and switch the database only afterwards.
2. Preparation and inventory
Connect to your server via SSH and change to the directory of your installation, that is, the place where docker-compose.yml and .env live. The path below is only an example. You run all further commands in this directory.
cd /path/to/your/fedisuite-installation
pwd
ls -la .env docker-compose.yml scripts
Both files must be listed. The scripts directory may not exist yet; it arrives in step 4. Do not work in a freshly cloned copy: it contains neither your data nor your settings.
Note on sudo: If your user may use Docker only with sudo, put sudo in front of every docker command and every ./scripts/… script in this guide.
Is PostgreSQL 15 really running?
docker compose ps
docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SHOW server_version;"'
The first output lists all services (db, app, worker1, worker2) with the status healthy. The second output is the database version, for example 15.19. If it already shows 18.…, you are done and need to do nothing. If it shows neither 15 nor 18, do not continue and ask in the support forum.
Which FediSuite version is running?
docker compose exec -T app printenv VITE_APP_VERSION
The output is the version number, for example 2.1.0. Write it down: you enter it in step 3. If the command prints no number, you find the version in the changelog or in the image tag in your .env, provided it does not say latest.
Is enough disk space free?
During the switch the old database, the backup and the new database exist at the same time. Plan about three times the current database size.
df -h .
sudo du -sh postgres uploads
df -h . shows the free space (column Avail), du -sh the size of the database and the uploads. The database directory belongs to the database user, so du needs sudo here. If you set up a different data directory than ./postgres, adjust the name.
Custom storage locations: The bundled backup script and this guide assume the standard installation: database in ./postgres, plus the folders uploads, plugins and logs next to the Compose file. If you use named Docker volumes or other paths, back up those locations yourself as well (see Backup & Restore) and ask in the support forum first if you are unsure.
3. Add the new settings to .env
The current FediSuite Compose file reads three new settings from .env and also requires a fixed FediSuite tag. The new settings define which PostgreSQL image runs, where its data lives and how the data directory is mounted in the container. Without them Compose deliberately stops with an error so that nothing is switched by accident. Add them now first with the values for PostgreSQL 15. This changes nothing about your database yet.
First make a safety copy of your .env. The copy contains your passwords, so it gets permissions for you only:
cp -p .env .env.before-postgres-18
chmod 600 .env.before-postgres-18
Then open .env in an editor, for example with nano .env, and add these three lines at the end:
POSTGRES_IMAGE=postgres:15-alpine
POSTGRES_DATA_PATH=./postgres
POSTGRES_DATA_TARGET=/var/lib/postgresql/data
Also find the line FEDISUITE_IMAGE=. It must point to the exact version you run right now (from step 2), not to latest. If the line is missing, add it. Example for version 2.1.0:
FEDISUITE_IMAGE=christinloehner/fedisuite:2.1.0
If you run the image with a special tag prefix, such as arm64- on ARM servers, keep that prefix and replace only the version number.
In nano you save with Ctrl+O and Enter and leave the editor with Ctrl+X. Change nothing else in the file, especially not DATABASE_URL, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, JWT_SECRET and ACCOUNT_ENCRYPTION_KEY. Check the result with this command, which prints no passwords:
grep -E '^(FEDISUITE_IMAGE|POSTGRES_IMAGE|POSTGRES_DATA_PATH|POSTGRES_DATA_TARGET)=' .env
Exactly four lines must appear, each only once. Duplicate lines confuse Compose and the check scripts: remove the extra one.
4. Update the self-hosting files
You need the current docker-compose.yml and the scripts folder (check, backup and restore scripts) from the self-hosting repository. If your directory is a Git checkout (there is a .git folder), update it like this:
git status --short
git pull --ff-only
git status --short lists changes to versioned files. Your .env, most data folders and a docker-compose.override.yml are ignored by Git. Lines starting with ?? (for example ?? plugins/) are untracked folders or files and do not interfere with git pull. If only such lines appear or the list is empty, git pull --ff-only runs without problems. Lines starting with M are changed versioned files, see below.
If you changed docker-compose.yml yourself (for example Traefik labels or networks), git status --short lists that file and git pull aborts with an error. Do not force anything. Proceed like this:
- Save your customised file:
cp docker-compose.yml ~/docker-compose.yml.before-postgres-18 - Reset the file to the template and fetch the update:
git checkout -- docker-compose.yml, thengit pull --ff-only - Then transfer your customisations again. The easiest way is a file
docker-compose.override.ymlin the same directory: Compose reads it automatically in addition, and Git ignores it. For Traefik there is the templatedocker-compose.traefik.example.ymlin the repository.
If you have no Git checkout at all, download docker-compose.yml and the scripts folder from the repository and put them into your installation directory. Do not overwrite your .env and do not copy .env.example over it. Make scripts/*.sh executable: chmod +x scripts/*.sh.
Now check the configuration. The command starts or changes nothing:
./scripts/check-config.sh
docker compose ps
The expected message is Configuration is valid., and docker compose ps still shows all four services as healthy. Your instance has run unchanged up to this point.
If you get an error message, one of the settings from step 3 is almost always missing or misspelt. The message names the setting; see also errors and solutions.
5. Stop the application and create a backup
Downtime starts now. First stop the app and the workers. The database stays on for the backup. Then the script backup.sh creates an archive with the database, the .env, the Compose file and the folders uploads and plugins:
docker compose stop app worker1 worker2
mkdir -p "$HOME/fedisuite-backups"
./scripts/backup.sh "$HOME/fedisuite-backups"
If you have additional worker services (for example worker3), stop them as well. docker compose ps --services lists all services.
At the end the script prints Backup created: … with the file name, which contains date and time, such as fedisuite-20261011T173000Z.tar.gz. It already verifies the database dump while creating it. Still check that the archive exists and has a sensible size:
ls -lh "$HOME/fedisuite-backups"
tar -tzf "$HOME/fedisuite-backups/FILENAME.tar.gz"
Replace FILENAME with the name from the ls output. The list must contain at least database.dump, manifest.txt, .env, docker-compose.yml, uploads/ and plugins/.
The archive contains secrets. It holds your .env with passwords and the ACCOUNT_ENCRYPTION_KEY. Without this key, connected accounts from a database backup cannot be used. Keep the archive protected, do not share it, and also copy it to another computer or storage outside this server, for example with scp.
If the backup does not complete, stop here and read the error message. Then start the application again with docker compose start app worker1 worker2 so your instance stays reachable. Do not switch the database without a verified backup.
6. Start PostgreSQL 18 with an empty database
First stop the old database. Its files in ./postgres stay untouched:
docker compose stop db
Open the .env again (nano .env) and change exactly these three lines that you added in step 3:
POSTGRES_IMAGE=postgres:18-alpine
POSTGRES_DATA_PATH=./postgres18
POSTGRES_DATA_TARGET=/var/lib/postgresql
Everything else in the file stays unchanged, especially DATABASE_URL, POSTGRES_DB, POSTGRES_USER and POSTGRES_PASSWORD. The new database is created with the same credentials. Note the difference in POSTGRES_DATA_PATH (new folder ./postgres18) and POSTGRES_DATA_TARGET (no /data at the end).
Check the configuration, download the images and start only the database:
./scripts/check-config.sh
docker compose pull --policy always db app
docker compose up -d db
docker compose ps
The script must again report Configuration is valid.. The download may take a moment. If your Docker does not know the --policy switch (a very old Compose version), use docker pull postgres:18-alpine and docker pull with the image name from your FEDISUITE_IMAGE line instead. The Compose file uses pull_policy: never: Docker never downloads images on its own, which is why this step is needed. It also fetches the FediSuite image with the fixed tag so that it is ready later.
The first start creates a new database cluster. Wait until db is shown as healthy in the output of docker compose ps. This usually takes a few seconds. Then check the version and that the database is still empty:
docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SHOW server_version;" -c "\dt"'
The output contains a line with the version beginning with 18. (for example 18.6) and the message Did not find any tables. That is correct: the new database is still empty, the data comes in the next step.
7. Load the data into PostgreSQL 18
The script restore-database.sh loads only the database from the archive. It does not touch .env, uploads or plugins. It deletes the (empty) database in PostgreSQL 18 and recreates it before loading the data. For safety it requires the flag --confirm-replace-database and runs only if the app and the workers are stopped and the database is running. Both are the case now.
Use the file name of your archive from step 5:
./scripts/restore-database.sh "$HOME/fedisuite-backups/FILENAME.tar.gz" --confirm-replace-database
On success the output ends with Database restored.. For a larger database this takes several minutes. Do not interrupt it. Afterwards check that the data is there:
docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SELECT count(*) AS users FROM users;" -c "SELECT count(*) AS accounts FROM accounts;"'
The numbers must match your instance: at least one user (the admin) and the number of connected accounts. If loading aborts with an error, everything was rolled back in one transaction and the database is empty. Your backup and the old data directory are unchanged. See errors and solutions.
8. Start and check FediSuite
First start only the app and wait until it is healthy. On startup the app checks the database schema and extends it if necessary. Only then do you start the workers:
docker compose up -d app
docker compose ps
docker compose logs --tail=50 app
Repeat docker compose ps until app shows as healthy. This can take up to a minute. The logs should contain no database errors. Now start the workers:
docker compose up -d worker1 worker2
docker compose ps
docker compose logs --tail=50 worker1 worker2 db
If your installation has more workers, start them as well. After about a minute db, app, worker1 and worker2 must show as healthy, and none may restart repeatedly.
Checklist:
- Open your instance in the browser and sign in with an existing account.
- Check that your connected Fediverse accounts, the statistics and the post history are present.
- Check scheduled posts and drafts.
- Check that uploaded images and installed plugins still work.
- Check that the database really is PostgreSQL 18:
docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SHOW server_version;"' - Watch the logs for errors over the next hours:
docker compose logs --since=1h app worker1 worker2.
Note: After loading the data, PostgreSQL first has to collect new statistics about the tables. Individual queries can therefore be somewhat slower in the first minutes; this is normal and settles by itself.
9. After the switch
- Keep the old data directory: Keep
./postgresand the backup archive at least until the new database has run without errors for several days or weeks and you have a new backup made with PostgreSQL 18. Both still contain all data and credentials and must stay protected. - Create a new backup: As soon as the instance runs stably, create a fresh backup (
./scripts/backup.sh "$HOME/fedisuite-backups") and store it externally. A backup of the old version does not replace it. How to automate and test backups is described under Backup & Restore. - Keep the
.envup to date: Back up the changed.env(includingACCOUNT_ENCRYPTION_KEY) again. You can delete the copy.env.before-postgres-18once everything runs. - Delete the old directory (optional, later): When everything has been stable for some time you can free the space. First check with
grep POSTGRES_DATA_PATH .envthat it says./postgres18and that a current backup made with PostgreSQL 18 is stored outside the server. Only then delete the old directory, and only that one:sudo rm -r ./postgres. After that, a rollback to PostgreSQL 15 is no longer possible.
10. Back to PostgreSQL 15 (rollback)
PostgreSQL 18 never modified the old data directory ./postgres during the switch. You can therefore return as long as you have not deleted it.
Case A: the app and the workers have not yet run on PostgreSQL 18
This applies if you stop, for example, while starting the database or while loading the data (steps 6 and 7) because something is wrong. As soon as the app or the workers run against PostgreSQL 18, they write new data: then case B applies. Stop everything and restore the three old values in .env:
docker compose stop
nano .env
POSTGRES_IMAGE=postgres:15-alpine
POSTGRES_DATA_PATH=./postgres
POSTGRES_DATA_TARGET=/var/lib/postgresql/data
./scripts/check-config.sh
docker compose up -d
docker compose ps
The instance then runs with exactly the data state from before the switch. The folder ./postgres18 stays; you can try the switch again later.
Case B: the app and the workers have already run on PostgreSQL 18
Everything changed in FediSuite since the switch (new posts, accounts, settings, statistics) exists only in the PostgreSQL 18 database. If you now switch back as in case A, those changes are lost. Think it over, and in any case first back up the current PostgreSQL 18 database once more with ./scripts/backup.sh. The data state in ./postgres matches the time of the backup from step 5. If you have already deleted ./postgres, set the values in .env back to PostgreSQL 15 as in case A, start only the database (docker compose up -d db) and load the archive from step 5 with restore-database.sh (as in step 7). Then start the app and the workers.
Never start PostgreSQL 18 on the directory ./postgres and never PostgreSQL 15 on ./postgres18.
Errors and solutions
"required variable POSTGRES_IMAGE is missing a value"
Compose stops because the new settings are missing in .env. The same applies to POSTGRES_DATA_PATH and POSTGRES_DATA_TARGET. Add the lines as described in step 3. Your running containers are not affected by the error.
check-config.sh reports "FEDISUITE_IMAGE must use an explicit release tag, not latest"
Enter the concrete version in .env, for example FEDISUITE_IMAGE=christinloehner/fedisuite:2.1.0, not latest.
check-config.sh reports that PostgreSQL 15 or 18 requires a specific POSTGRES_DATA_TARGET
Image and mount path do not match. For postgres:15-alpine the correct value is POSTGRES_DATA_TARGET=/var/lib/postgresql/data, for postgres:18-alpine it is /var/lib/postgresql (without /data). Correct the line.
The database does not start; the log says the files are not compatible with this version
PostgreSQL 18 was pointed at the old directory. Stop everything and restore the correct values in .env: for PostgreSQL 18 POSTGRES_DATA_PATH=./postgres18 and POSTGRES_DATA_TARGET=/var/lib/postgresql. The folder ./postgres may only be used with postgres:15-alpine. You see the log with docker compose logs --tail=50 db.
On start it says the image is not available
The Compose file never downloads images on its own (pull_policy: never). Fetch them with docker compose pull --policy always db app and start again.
restore-database.sh refuses to run
The script needs a running database and stopped services. Stop app, worker1 and worker2 with docker compose stop app worker1 worker2 and check with docker compose ps that db is running. It also aborts if --confirm-replace-database is missing at the end of the command.
Loading the data aborts with an error
The script loads everything in one transaction: on an error the new database stays empty and your backup stays unchanged. Read the last lines of the output and the database log (docker compose logs --tail=100 db). A common cause is too little disk space (df -h .). Free up space and repeat step 7 with the same archive. If you get stuck, go back (rollback, case A) and ask for help in the support forum. Never publish your .env or the backup archive there.
The app does not start or does not become "healthy"
Look at the logs: docker compose logs --tail=100 app. Check that the database is running and that the tables exist (step 7). If an error about ACCOUNT_ENCRYPTION_KEY appears, the .env is not the right one or the key was changed. Use the original key from your backup.
Any questions?
General notes on updates are on the Updates & upgrades page, all .env settings in the environment variables overview. For a specific error the logs and the support forum help.