Skip to main content

Backups and upgrades

What to back up​

WhatWhere (example compose.yml)How
The databasethe pgdata volumepg_dump, nightly
Uploaded filesthe uploads volume, or your S3 bucketcopy the volume; for S3, bucket versioning or replication
Configuration.env and compose.ymlkeep 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​

  1. Stop the app, keep the database running: docker compose stop app.

  2. Restore the database into an empty one:

    docker compose exec -T db dropdb -U sol2flow sol2flow
    docker compose exec -T db createdb -U sol2flow sol2flow
    docker compose exec -T db pg_restore -U sol2flow -d sol2flow --no-owner < backup/sol2flow-2026-09-27.dump
  3. Restore the files into the uploads volume (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'
  4. Start with the same APP_ENCRYPTION_KEY and AUTH_SECRET as 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​

  1. Read the changelog for the versions between yours and the new one; it names any manual steps.

  2. Back up the database.

  3. Set the new version in .env (SOL2FLOW_VERSION=1.3.0), then:

    docker compose pull
    docker compose up -d
    docker 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.

Upgrading from a pre-release image

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.

  1. Stop the app and dump the database:

    cd /srv/sol2flow
    docker compose stop app
    docker compose exec -T db pg_dump -U sol2flow -Fc sol2flow > backup/sol2flow-before-pg18.dump
    docker compose down
  2. In compose.yml, switch the db service 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-alpine
    volumes: [pgdata18:/var/lib/postgresql]
    volumes: { pgdata: {}, pgdata18: {}, uploads: {} }
  3. Start the database alone, restore the dump, then start everything:

    docker compose up -d --wait db
    docker compose exec -T db pg_restore -U sol2flow -d sol2flow --no-owner < backup/sol2flow-before-pg18.dump
    docker 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_upgrade is 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 with initdb --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:

  1. Create a new key: openssl rand -base64 32.
  2. Set APP_ENCRYPTION_KEY to it in .env and run docker compose up -d.
  3. In Admin → Authentication, edit each OIDC provider and enter its client secret again.
  4. 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.