mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-09-21 13:31:42 +00:00
docs: document disposable restore rehearsal
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
# Disposable restore documentation fix report
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Updated `scripts/backup/README.md` only for the documentation change. This
|
||||||
|
report is the requested verification artifact.
|
||||||
|
|
||||||
|
## Change
|
||||||
|
|
||||||
|
The monthly recovery rehearsal now documents a unique disposable Compose
|
||||||
|
project with project-labeled volumes, snapshot selection, staging restore,
|
||||||
|
PostgreSQL import, restoration of uploads and both TDLib volumes, disposable
|
||||||
|
service startup, health and log checks, retained-file validation, known STL
|
||||||
|
checksum and metadata comparison, narrowly scoped cleanup, and an evidence
|
||||||
|
template.
|
||||||
|
|
||||||
|
The guide explicitly warns operators not to use production project names or
|
||||||
|
volumes, and states that this documentation update did not run the rehearsal.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Focused text check: passed. Confirmed the guide contains the unique project
|
||||||
|
warning, snapshot selection, staging restore, PostgreSQL import, all three
|
||||||
|
protected volume restores, health check, retained-file validation, checksum
|
||||||
|
comparison, scoped cleanup, evidence template, and the statement that the
|
||||||
|
rehearsal was not run.
|
||||||
|
- `git diff --check`: passed.
|
||||||
|
- No recovery, Docker, Restic, PostgreSQL, or health-check commands were run;
|
||||||
|
this change documents the operator procedure only.
|
||||||
+190
-11
@@ -186,21 +186,200 @@ At least monthly, perform a full repository read check:
|
|||||||
docker compose --profile backup run --rm backup check --read-data
|
docker compose --profile backup run --rm backup check --read-data
|
||||||
```
|
```
|
||||||
|
|
||||||
Also rehearse recovery using a disposable staging directory and verify the
|
### Monthly disposable recovery rehearsal
|
||||||
database dump checksum before deleting the rehearsal directory:
|
|
||||||
|
Perform the following procedure at least monthly. It restores one snapshot into
|
||||||
|
a unique, disposable Compose project, so the database and all Compose volumes
|
||||||
|
are isolated from production. **Never use the production Compose project name,
|
||||||
|
production volume names, or `restore-live` for this rehearsal.** In particular,
|
||||||
|
do not run `docker compose down -v` without the explicit rehearsal
|
||||||
|
`--project-name` shown below.
|
||||||
|
|
||||||
|
Run these commands from the production Compose checkout as an operator allowed
|
||||||
|
to read `/etc/dragons-stash/backup.env`. They use production secrets only to
|
||||||
|
start a separate application stack; do not expose its published port beyond the
|
||||||
|
host. First select a snapshot and set the expected values for a known retained
|
||||||
|
STL that was recorded when the backup was made. `EXPECTED_FILE_PATH` must be
|
||||||
|
the database value under `/data/uploads`, and `EXPECTED_FILE_SIZE` is bytes.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
REHEARSAL_DIR=/var/lib/dragons-stash/backup-staging/monthly-rehearsal-SNAPSHOT_ID
|
set -Eeuo pipefail
|
||||||
./scripts/backup/restore.sh restore-to-staging SNAPSHOT_ID "$REHEARSAL_DIR"
|
set -a
|
||||||
(
|
. /etc/dragons-stash/backup.env
|
||||||
cd "$REHEARSAL_DIR"/staging/backup-*
|
set +a
|
||||||
sha256sum --check manifest/database.dump.sha256
|
|
||||||
)
|
./scripts/backup/restore.sh list
|
||||||
|
SNAPSHOT_ID=SNAPSHOT_ID_FROM_LIST
|
||||||
|
REHEARSAL_ID="$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
||||||
|
REHEARSAL_PROJECT="dragonsstash-rehearsal-$REHEARSAL_ID"
|
||||||
|
REHEARSAL_DIR="$BACKUP_STAGING_PATH/monthly-rehearsal-$REHEARSAL_ID"
|
||||||
|
REHEARSAL_ENV="$REHEARSAL_DIR/compose.env"
|
||||||
|
REHEARSAL_APP_PORT=13000 # Choose an unused host-local port.
|
||||||
|
|
||||||
|
EXPECTED_UPLOAD_ID=RECORDED_UPLOAD_ID
|
||||||
|
EXPECTED_UPLOAD_STATUS=COMPLETED
|
||||||
|
EXPECTED_FILE_NAME=RECORDED_FILENAME.stl
|
||||||
|
EXPECTED_FILE_PATH=/data/uploads/RECORDED_RELATIVE_PATH.stl
|
||||||
|
EXPECTED_FILE_SIZE=RECORDED_SIZE_IN_BYTES
|
||||||
|
EXPECTED_SHA256=RECORDED_SHA256
|
||||||
|
|
||||||
|
./scripts/backup/restore.sh verify "$SNAPSHOT_ID"
|
||||||
|
./scripts/backup/restore.sh restore-to-staging "$SNAPSHOT_ID" "$REHEARSAL_DIR"
|
||||||
|
RESTORE_ROOT="$(printf '%s\n' "$REHEARSAL_DIR"/staging/backup-*)"
|
||||||
|
sha256sum --check "$RESTORE_ROOT/manifest/database.dump.sha256"
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
cp .env "$REHEARSAL_ENV"
|
||||||
|
printf '\nAPP_PORT=%s\nNEXT_PUBLIC_APP_URL=http://localhost:%s\n' \
|
||||||
|
"$REHEARSAL_APP_PORT" "$REHEARSAL_APP_PORT" >> "$REHEARSAL_ENV"
|
||||||
```
|
```
|
||||||
|
|
||||||
The staging restore also verifies the custom PostgreSQL dump and required data
|
Confirm that `RESTORE_ROOT` names exactly one `backup-*` directory before
|
||||||
directories. Inspect the restored data as appropriate, then remove the
|
continuing. The staging restore has already verified the custom PostgreSQL dump
|
||||||
disposable directory using your approved host cleanup procedure.
|
and restored `data/uploads`, `data/tdlib-worker`, and `data/tdlib-bot`.
|
||||||
|
|
||||||
|
Create the isolated project and volumes, start only its database, then import
|
||||||
|
the dump. The `create` command makes the project-scoped application volumes
|
||||||
|
without starting app, worker, or bot.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
compose_rehearsal() {
|
||||||
|
docker compose --project-name "$REHEARSAL_PROJECT" --env-file "$REHEARSAL_ENV" "$@"
|
||||||
|
}
|
||||||
|
rehearsal_volume() {
|
||||||
|
docker volume ls \
|
||||||
|
--filter "label=com.docker.compose.project=$REHEARSAL_PROJECT" \
|
||||||
|
--filter "label=com.docker.compose.volume=$1" \
|
||||||
|
--format '{{.Name}}'
|
||||||
|
}
|
||||||
|
|
||||||
|
compose_rehearsal --profile full create app worker bot
|
||||||
|
compose_rehearsal --profile full up -d db
|
||||||
|
compose_rehearsal exec -T db dropdb --if-exists --force \
|
||||||
|
--username "${POSTGRES_USER:-dragons}" "${POSTGRES_DB:-dragonsstash}"
|
||||||
|
compose_rehearsal exec -T db createdb --username "${POSTGRES_USER:-dragons}" \
|
||||||
|
"${POSTGRES_DB:-dragonsstash}"
|
||||||
|
compose_rehearsal exec -T db pg_restore --no-owner --exit-on-error \
|
||||||
|
--username "${POSTGRES_USER:-dragons}" --dbname "${POSTGRES_DB:-dragonsstash}" \
|
||||||
|
< "$RESTORE_ROOT/database.dump"
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy each restored file tree into its matching **rehearsal** volume. Each
|
||||||
|
target is new and empty; the function rejects an ambiguous volume lookup.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
restore_rehearsal_volume() {
|
||||||
|
local logical_name="$1"
|
||||||
|
local source_dir="$2"
|
||||||
|
local volume_name
|
||||||
|
volume_name="$(rehearsal_volume "$logical_name")"
|
||||||
|
test -n "$volume_name"
|
||||||
|
test "$(printf '%s\n' "$volume_name" | wc -l)" -eq 1
|
||||||
|
compose_rehearsal --profile backup run --rm --no-deps \
|
||||||
|
--entrypoint bash \
|
||||||
|
-v "$source_dir:/restore-source:ro" \
|
||||||
|
-v "$volume_name:/restore-target" \
|
||||||
|
backup -ceu 'cp -a /restore-source/. /restore-target/'
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_rehearsal_volume manual_uploads "$REHEARSAL_DIR/data/uploads"
|
||||||
|
restore_rehearsal_volume tdlib_state "$REHEARSAL_DIR/data/tdlib-worker"
|
||||||
|
restore_rehearsal_volume tdlib_bot_state "$REHEARSAL_DIR/data/tdlib-bot"
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the disposable app, worker, and bot services. Check the health endpoint,
|
||||||
|
then retain the `ps` and log output as rehearsal evidence. Do not use the bot
|
||||||
|
to send messages during this check.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
compose_rehearsal --profile full up -d app worker bot
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
"http://localhost:$REHEARSAL_APP_PORT/api/health"
|
||||||
|
compose_rehearsal --profile full ps
|
||||||
|
compose_rehearsal --profile full logs --tail=100 app worker bot
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate that every retained database file reference has a restored file. Rows
|
||||||
|
whose `retainedAt` is `NULL` are legacy references and are warnings, not
|
||||||
|
failures. Then compare the recorded STL checksum and metadata with the
|
||||||
|
disposable database and volume; both commands must succeed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
compose_rehearsal exec -T db psql --no-psqlrc --tuples-only --no-align --quiet \
|
||||||
|
--field-separator=$'\t' --username "${POSTGRES_USER:-dragons}" \
|
||||||
|
--dbname "${POSTGRES_DB:-dragonsstash}" \
|
||||||
|
--command "SELECT CASE WHEN \"retainedAt\" IS NULL THEN 'legacy' ELSE 'retained' END, \"filePath\" FROM \"manual_upload_files\" ORDER BY 2" |
|
||||||
|
compose_rehearsal --profile backup run --rm --no-deps -T --entrypoint bash backup -ceu '
|
||||||
|
missing=0
|
||||||
|
while IFS="$(printf "\\t")" read -r retention file_path; do
|
||||||
|
if [[ "$retention" == legacy ]]; then
|
||||||
|
printf "Warning: legacy reference is not required: %s\\n" "$file_path" >&2
|
||||||
|
elif [[ "$retention" != retained || "$file_path" != /data/uploads/* || ! -f "/data/uploads/${file_path#/data/uploads/}" ]]; then
|
||||||
|
printf "Missing or invalid retained upload: %s\\n" "$file_path" >&2
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
exit "$missing"
|
||||||
|
'
|
||||||
|
|
||||||
|
actual_sha256="$(compose_rehearsal --profile backup run --rm --no-deps \
|
||||||
|
--entrypoint sha256sum backup "$EXPECTED_FILE_PATH" | awk '{print $1}')"
|
||||||
|
test "$actual_sha256" = "$EXPECTED_SHA256"
|
||||||
|
|
||||||
|
metadata_rows="$(compose_rehearsal exec -T db psql --no-psqlrc --tuples-only \
|
||||||
|
--no-align --quiet --username "${POSTGRES_USER:-dragons}" \
|
||||||
|
--dbname "${POSTGRES_DB:-dragonsstash}" \
|
||||||
|
--set="upload_id=$EXPECTED_UPLOAD_ID" --set="upload_status=$EXPECTED_UPLOAD_STATUS" \
|
||||||
|
--set="file_name=$EXPECTED_FILE_NAME" --set="file_path=$EXPECTED_FILE_PATH" \
|
||||||
|
--set="file_size=$EXPECTED_FILE_SIZE" --command '
|
||||||
|
SELECT count(*) FROM "manual_uploads" u
|
||||||
|
JOIN "manual_upload_files" f ON f."uploadId" = u.id
|
||||||
|
WHERE u.id = :'upload_id'
|
||||||
|
AND u.status::text = :'upload_status'
|
||||||
|
AND f."fileName" = :'file_name'
|
||||||
|
AND f."filePath" = :'file_path'
|
||||||
|
AND f."fileSize" = :'file_size'::bigint
|
||||||
|
AND f."retainedAt" IS NOT NULL;
|
||||||
|
')"
|
||||||
|
test "$metadata_rows" = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
After recording the evidence, destroy only the explicitly named disposable
|
||||||
|
project and its project-labeled volumes. Do not remove `REHEARSAL_DIR` until
|
||||||
|
the evidence is recorded; then remove only that generated child directory by
|
||||||
|
your approved host cleanup procedure. Production services, volumes, and backup
|
||||||
|
snapshots must remain untouched.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
compose_rehearsal --profile full down --volumes --remove-orphans
|
||||||
|
docker volume ls --filter "label=com.docker.compose.project=$REHEARSAL_PROJECT"
|
||||||
|
test -z "$(docker volume ls --quiet --filter "label=com.docker.compose.project=$REHEARSAL_PROJECT")"
|
||||||
|
```
|
||||||
|
|
||||||
|
Record this evidence for every rehearsal; do not include secrets, database
|
||||||
|
dumps, Telegram session contents, or private NAS details.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Disposable restore rehearsal
|
||||||
|
Date/time (UTC):
|
||||||
|
Operator:
|
||||||
|
Snapshot ID:
|
||||||
|
Snapshot backup date:
|
||||||
|
Disposable Compose project:
|
||||||
|
Health endpoint result (HTTP/body):
|
||||||
|
docker compose ps result:
|
||||||
|
app/worker/bot log review result:
|
||||||
|
Retained-file reference validation result:
|
||||||
|
Known retained STL upload ID/path:
|
||||||
|
Expected SHA-256 / restored SHA-256:
|
||||||
|
Expected metadata (status, filename, size, retainedAt) / restored result:
|
||||||
|
Database dump manifest checksum result:
|
||||||
|
Cleanup result (project and rehearsal volumes absent):
|
||||||
|
Notes/caveats:
|
||||||
|
```
|
||||||
|
|
||||||
|
This document describes the procedure only; it has not been run by this
|
||||||
|
documentation update.
|
||||||
|
|
||||||
## 6. Restore modes
|
## 6. Restore modes
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user