#!/bin/sh # Restores a Siftlode Postgres dump created by backup.sh into the running db container. # Usage: ./scripts/restore.sh backups/siftlode-YYYYMMDD-HHMMSS.dump # # Restoring the database does NOT restore the secrets that decrypt it. If this dump is from an # instance with a different TOKEN_ENCRYPTION_KEY than the current .env, the encrypted sysconfig # values and refresh tokens will not be readable. See docs/self-hosting.md § Backups. set -e # See backup.sh: the documented self-host layout ships only docker-compose.selfhost.yml, so plain # `docker compose` has no configuration file to find there. if [ -z "${COMPOSE_FILE:-}" ] && [ ! -f docker-compose.yml ] && [ -f docker-compose.selfhost.yml ]; then COMPOSE_FILE=docker-compose.selfhost.yml export COMPOSE_FILE fi FILE="$1" if [ -z "$FILE" ]; then echo "Usage: restore.sh "; exit 1; fi if [ ! -f "$FILE" ]; then echo "Dump file not found: $FILE"; exit 1; fi USER_NAME="${POSTGRES_USER:-siftlode}" DB_NAME="${POSTGRES_DB:-siftlode}" # Stop the app before dropping its tables. `--clean` DROPs each object in its own transaction, and a # running api (with its scheduler) holds locks that make those DROPs queue behind it — abort the wait # and the database is left half-dropped. It also keeps the app from writing into a schema that is # only partly back. # # It is NOT restarted automatically, on purpose. Every start runs `alembic upgrade head`, so on the # documented rollback path (restore a pre-upgrade dump, THEN pin the older IMAGE_TAG) an automatic # restart would re-apply the very migrations the restore just undid. On a FAILED restore it would # put the app back on a half-restored schema, which is the second thing the stop exists to prevent. # The operator starts it when they are ready — with `up -d`, not `start`: `start` relaunches the # EXISTING container, so it ignores a newly pinned IMAGE_TAG and comes back on the image being # rolled back FROM. if ! docker compose stop api worker; then echo "Could not stop api/worker — refusing to restore while the app may still be writing." >&2 exit 1 fi # pg_restore exits non-zero for ignorable warnings too (--clean --if-exists on a fresh database # reports every missing object it tried to drop), so a non-zero code is reported as "check this", # not swallowed and not dressed up as success. if docker compose exec -T db pg_restore -U "$USER_NAME" -d "$DB_NAME" --clean --if-exists < "$FILE"; then echo "Restored $FILE" echo "api and worker are stopped. Start them again with:" if [ -n "${COMPOSE_FILE:-}" ]; then echo " docker compose -f $COMPOSE_FILE up -d" else # Deliberately NOT a runnable line. Both compose files can be present (a source checkout), and # the two declare DIFFERENT volume names — so a pasted bare 'docker compose up -d' would recreate # the database on an empty volume and strand what was just restored. echo " docker compose -f up -d" fi echo "It must be the SAME file you started with: 'up -d' recreates from whatever file it reads," echo "and a different one points at different volumes. Rolling a release back? Pin the older" echo "IMAGE_TAG in .env FIRST: every start re-runs the migrations, and 'start' would relaunch the" echo "existing container on the image you are rolling back FROM." else echo "pg_restore reported errors while restoring $FILE — read the output above before treating this instance as restored." >&2 echo "api and worker are left STOPPED: the database may be half-restored. Fix the dump and re-run before starting them." >&2 exit 1 fi