Backups and upgrades
What to back up
| What | Where (example compose.yml) | How |
|---|---|---|
| The database | the pgdata volume | pg_dump, nightly |
| Uploaded files | the uploads volume, or your S3 bucket | copy the volume; for S3, bucket versioning or replication |
| Configuration | .env and compose.yml | keep a copy in your password manager or secrets store |
Keep APP_ENCRYPTION_KEY separately from the database backups: a backup and the key together expose the encrypted
secrets in it.
Back up the database
A consistent dump while the app keeps running:
cd /srv/sol2flow
docker compose exec -T db pg_dump -U sol2flow -Fc sol2flow > "backup/sol2flow-$(date +%F).dump"
Run it nightly (cron or a systemd timer), copy the dumps off the server, and delete old ones after a while.
Back up the files
With the local driver, archive the uploads volume:
docker run --rm -v sol2flow_uploads:/data:ro -v "$PWD/backup":/backup alpine \
tar czf "/backup/uploads-$(date +%F).tar.gz" -C /data .
(The volume name is <project>_uploads; docker volume ls shows it.) With S3, turn on versioning for the bucket or
replicate it to another one.
Restore
-
Stop the app, keep the database running:
docker compose stop app. -
Restore the database into an empty one:
docker compose exec -T db dropdb -U sol2flow sol2flowdocker compose exec -T db createdb -U sol2flow sol2flowdocker compose exec -T db pg_restore -U sol2flow -d sol2flow --no-owner < backup/sol2flow-2026-09-27.dump -
Restore the files into the
uploadsvolume (or the bucket) from the same point in time, and give them to the app's user (uid 1001):docker run --rm -v sol2flow_uploads:/data -v "$PWD/backup":/backup alpine \sh -c 'tar xzf /backup/uploads-2026-09-27.tar.gz -C /data && chown -R 1001:1001 /data' -
Start with the same
APP_ENCRYPTION_KEYandAUTH_SECRETas before:docker compose up -d.
Test a restore on a spare machine now and then — a backup you haven't restored is a hope, not a backup.
Upgrade
-
Read the changelog for the versions between yours and the new one; it names any manual steps.
-
Back up the database.
-
Set the new version in
.env(SOL2FLOW_VERSION=1.3.0), then:docker compose pulldocker compose up -ddocker compose logs -f app
Database migrations run automatically on start. Downgrades aren't supported: to go back, restore the backup you made before upgrading, together with the old image version.
Pre-release images ran as a user with a random id. The app now runs as uid and gid 1001, so change the owner of the files it writes once, before you start the new version:
docker compose run --rm --no-deps -u root --entrypoint chown app -R 1001:1001 /data/uploads
# a host directory instead of a volume:
sudo chown -R 1001:1001 /srv/sol2flow/uploads
Upgrade PostgreSQL
sol2flow supports PostgreSQL 16, 17 and 18; 18 is recommended and what the example compose.yml runs. An
installation on 16 or 17 keeps working, so move when it suits you. A new major version can't open an older data
directory: keep your db service as it is (image and the /var/lib/postgresql/data mount) until you move the data with
a dump and restore.
-
Stop the app and dump the database:
cd /srv/sol2flowdocker compose stop appdocker compose exec -T db pg_dump -U sol2flow -Fc sol2flow > backup/sol2flow-before-pg18.dumpdocker compose down -
In
compose.yml, switch thedbservice to 18 with a new volume, mounted at/var/lib/postgresql(from 18 the image keeps its data in/var/lib/postgresql/18/docker, no longer in…/data):db:image: postgres:18-alpinevolumes: [pgdata18:/var/lib/postgresql]volumes: { pgdata: {}, pgdata18: {}, uploads: {} } -
Start the database alone, restore the dump, then start everything:
docker compose up -d --wait dbdocker compose exec -T db pg_restore -U sol2flow -d sol2flow --no-owner < backup/sol2flow-before-pg18.dumpdocker compose up -d
The old pgdata volume stays untouched: to go back, restore the old db lines. Remove it once 18 runs well
(docker volume rm sol2flow_pgdata).
pg_upgradeis quicker for large databases but needs the binaries of both versions. PostgreSQL 18 turns data checksums on for new clusters; when the old cluster has none (the default up to 17), create the new one withinitdb --no-data-checksums.- On a PostgreSQL 18 server of your own, MD5 passwords are deprecated: use SCRAM
(
password_encryption = scram-sha-256, the default since 14) and set the password again if it was stored as MD5.
Rotating the encryption key
APP_ENCRYPTION_KEY protects secrets stored in the database. Change it only if it was exposed, and know what happens:
- Single sign-on client secrets become unreadable: enter each provider's client secret again.
- Two-factor authentication secrets and recovery codes become invalid: affected people need a two-factor reset by staff (Users and sign-up).
- API keys stop working: people create new ones.
- Webhook signing secrets become unreadable: rotate them.
To rotate:
- Create a new key:
openssl rand -base64 32. - Set
APP_ENCRYPTION_KEYto it in.envand rundocker compose up -d. - In Admin → Authentication, edit each OIDC provider and enter its client secret again.
- Tell your users that API keys must be recreated and webhook secrets rotated, and reset two-factor authentication for those who ask.
Logs
The app logs JSON lines to standard output: docker compose logs app. Set the detail with LOG_LEVEL. Secrets such as
passwords and tokens are removed from log entries.
To keep the log in files as well, set LOG_FILE=/data/logs/app.log with the logs volume of the
example: the app writes one file per day (app-2026-09-30.log, UTC days)
and keeps the last ten (settings). Follow it with
docker compose exec app tail -F /data/logs/app.log, or back up the volume like the uploads. To be told about errors
instead of reading logs, turn on error tracking.