fix(ops): make backup/restore work, and give upgrades a pin and a rollback

DX-13: both shell scripts ran bare 'docker compose', which has no config
file to find in the layout docs/self-hosting.md tells people to use (only
docker-compose.selfhost.yml is downloaded) — they now fall back to it, while
a source checkout keeps resolving docker-compose.yml as before. The two
PowerShell scripts moved the dump through the PS pipeline, which corrupts
binary on 5.1 and cannot express -Encoding Byte at all on 7 ('Byte' is not a
supported encoding name), so they simply failed there; they now write inside
the container and 'docker compose cp' it across. backup.sh also wrote its
output straight into backups/, leaving a truncated file that looks like a
real backup when pg_dump fails — it writes a .part and promotes on success.
Both restores report a non-zero pg_restore rather than printing 'Restored'
over it. Docs now state that a backup is the dump PLUS .env: without
TOKEN_ENCRYPTION_KEY every encrypted secret in the restored database is
unreadable, which the old one-line 'back up the volume' never said.

DX-14: 'latest' moves with every release and migrations run on every boot,
so self-hosters had no way to choose when they move and no way back. Docs
gain IMAGE_TAG pinning, a rollback recipe (image-only vs schema-bearing) and
the migration-failed-at-boot case; CHANGELOG.md is generated from
releaseNotes.ts so the file cannot drift from what the app shows, and
'siftlode release' refuses on a stale one rather than regenerating it into
the clean tree it just demanded.

Verified: dump + restore round-trip on both platforms against localdev into
a scratch database (8 users, 262822 videos, schema 0082_invite_lang), and
IMAGE_TAG=0.80.0 resolves on both services in 'compose config'.
This commit is contained in:
2026-09-05 00:50:07 +02:00
parent e23128b4a1
commit ca4e559cc7
8 changed files with 2061 additions and 17 deletions
+29 -1
View File
@@ -1,11 +1,39 @@
#!/bin/sh
# Dumps the Siftlode Postgres database to backups/siftlode-<timestamp>.dump (custom format).
# Run from the project root: ./scripts/backup.sh
#
# A dump is only HALF a backup. The other half is your `.env` — it holds TOKEN_ENCRYPTION_KEY,
# without which every encrypted value in the restored database (Google credentials, SMTP password,
# Plex token, stored refresh tokens) is unreadable. See docs/self-hosting.md § Backups.
set -e
# `docker compose` finds docker-compose.yml on its own and honours $COMPOSE_FILE. The documented
# self-host layout (docs/self-hosting.md § Get the files) has NEITHER — just the selfhost file — so
# without this the command below failed there with "no configuration file provided".
if [ -z "${COMPOSE_FILE:-}" ] && [ ! -f docker-compose.yml ] && [ -f docker-compose.selfhost.yml ]; then
COMPOSE_FILE=docker-compose.selfhost.yml
export COMPOSE_FILE
fi
USER_NAME="${POSTGRES_USER:-siftlode}"
DB_NAME="${POSTGRES_DB:-siftlode}"
mkdir -p backups
TS=$(date +%Y%m%d-%H%M%S)
OUT="backups/siftlode-$TS.dump"
docker compose exec -T db pg_dump -U "$USER_NAME" -Fc "$DB_NAME" > "$OUT"
# Write to a temp name and only promote it on success: a failed pg_dump would otherwise leave a
# truncated (often 0-byte) file sitting in backups/ that looks exactly like a real backup.
TMP="$OUT.part"
if ! docker compose exec -T db pg_dump -U "$USER_NAME" -Fc "$DB_NAME" > "$TMP"; then
rm -f "$TMP"
echo "BACKUP FAILED — pg_dump returned an error; no file was written." >&2
exit 1
fi
if [ ! -s "$TMP" ]; then
rm -f "$TMP"
echo "BACKUP FAILED — pg_dump produced an empty file; no backup was written." >&2
exit 1
fi
mv "$TMP" "$OUT"
echo "Wrote $OUT"
echo "Remember: back up your .env too (TOKEN_ENCRYPTION_KEY) — this dump alone cannot be fully restored."