#!/bin/bash # Postgres backup/restore hook for the immich `database` service (VectorChord/pgvecto.rs image). # Invoked by backupbot-two via the deploy labels: # backupbot.backup.pre-hook = "/pg_backup.sh backup" # backupbot.backup.volumes.postgres.path = "backup.sql" # backupbot.restore.post-hook = "/pg_backup.sh restore" # # IMPORTANT — why this restore does NOT drop the database: # immich's postgres image bundles the legacy pgvecto.rs (`vectors`) extension. Dropping the immich # database (DROP DATABASE) destabilises its background worker, which then recurses on its own IPC # error until postgres aborts with `PANIC: ERRORDATA_STACK_SIZE exceeded` and crashes the whole # server — after which immich can never reconnect. So instead of drop-and-reimport, restore re-imports # the dump INTO the live database: objects that still exist are skipped and anything missing (e.g. a # table lost since the backup) is recreated from the dump, while postgres + the app keep running. # The `search_path` rewrite is immich's documented restore step # (https://docs.immich.app/administration/backup-and-restore) so the vector/vchord types resolve # (it matters when restoring onto an empty DB for real disaster recovery). ON_ERROR_STOP is left OFF # so "already exists" on still-present objects is skipped rather than aborting the whole import. set -e BACKUP_FILE='/var/lib/postgresql/data/backup.sql' export PGPASSWORD=$(cat "${POSTGRES_PASSWORD_FILE:-/run/secrets/db_password}") DB_USER="${POSTGRES_USER:-postgres}" DB_NAME="${POSTGRES_DB:-immich}" function backup { pg_dump -U "$DB_USER" "$DB_NAME" | gzip > "$BACKUP_FILE" } function restore { gunzip -c "$BACKUP_FILE" \ | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \ | psql -U "$DB_USER" -d "$DB_NAME" -f - } $@