# Restores a Siftlode Postgres dump created by backup.ps1 into the running db container. # Usage: .\scripts\restore.ps1 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. param([Parameter(Mandatory = $true)][string]$DumpFile) $ErrorActionPreference = "Stop" $WarningPreference = "Continue" # See backup.ps1: the dump travels with `docker compose cp`, never through the PowerShell pipeline, # which corrupts binary on 5.1 and cannot express `-Encoding Byte` at all on 7. $remote = "/tmp/siftlode-restore.dump" # See backup.ps1: on Windows PowerShell 5.1 a redirected native stderr is a TERMINATING error under # $ErrorActionPreference = "Stop", so without the catch a cleanup step could kill the script after # the restore already succeeded — and the obvious next move, re-running it, wipes the database again. function Remove-RemoteDump($path) { try { docker compose exec -T db rm -f $path 2>&1 | Out-Null } catch { } } if (-not $env:COMPOSE_FILE -and -not (Test-Path "docker-compose.yml") -and (Test-Path "docker-compose.selfhost.yml")) { $env:COMPOSE_FILE = "docker-compose.selfhost.yml" } $user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "siftlode" } $dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "siftlode" } if (-not (Test-Path $DumpFile)) { throw "Dump file not found: $DumpFile" } docker compose cp $DumpFile "db:$remote" $cpCode = $LASTEXITCODE if ($cpCode -ne 0) { throw "RESTORE FAILED — could not copy the dump into the container (exit $cpCode)." } # 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. docker compose stop api worker $stopCode = $LASTEXITCODE if ($stopCode -ne 0) { throw "Could not stop api/worker (exit $stopCode) — refusing to restore while the app may still be writing." } docker compose exec -T db pg_restore -U $user -d $dbname --clean --if-exists $remote $restoreCode = $LASTEXITCODE Remove-RemoteDump $remote # 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 ($restoreCode -ne 0) { # Write-Warning, not Write-Output: `>` redirects only the SUCCESS stream, so on stdout this # explanation would be filed into the log while the bare exception stayed on screen. (The # warning stream is its own stream, not the error stream — $WarningPreference is pinned at the # top so an inherited "SilentlyContinue" cannot drop it.) Write-Warning "api and worker are left STOPPED: the database may be half-restored. Fix the dump and re-run before starting them." throw "pg_restore reported errors while restoring $DumpFile (exit $restoreCode) — read the output above before treating this instance as restored." } Write-Output "Restored $DumpFile" # Deliberately not a runnable line when we could not resolve the file: 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 the restore. $cf = if ($env:COMPOSE_FILE) { $env:COMPOSE_FILE } else { "" } Write-Output "api and worker are stopped. Start them again with:" Write-Output " docker compose -f $cf up -d" Write-Output "It must be the SAME file you started with: 'up -d' recreates from whatever file it reads," Write-Output "and a different one points at different volumes. Rolling a release back? Pin the older" Write-Output "IMAGE_TAG in .env FIRST: every start re-runs the migrations, and 'start' would relaunch the" Write-Output "existing container on the image you are rolling back FROM."