- deploy/backup.sh / deploy/restore.sh (compose service or local container; side-by-side test restore) - restore: chown copied backup for sqlservr, fail loudly on unreadable file list - README: update, backup, test-restore and rollback steps Validated locally: backup 194 MB, restored into a test DB with identical counts, test DB dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
987f69704b
commit
06f56012ec
1
.gitignore
vendored
1
.gitignore
vendored
@ -262,3 +262,4 @@ __pycache__/
|
|||||||
/Museum/dist/js
|
/Museum/dist/js
|
||||||
/Museum/src/dist
|
/Museum/src/dist
|
||||||
.env
|
.env
|
||||||
|
backups/
|
||||||
|
|||||||
21
README.md
21
README.md
@ -61,9 +61,30 @@ Re-running the import adds new poems and leaves existing ones untouched, so it c
|
|||||||
### Update
|
### Update
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
deploy/backup.sh # always back up first
|
||||||
git pull && docker compose up -d --build
|
git pull && docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Database migrations run automatically when the API starts.
|
||||||
|
|
||||||
|
### Backups
|
||||||
|
|
||||||
|
```sh
|
||||||
|
deploy/backup.sh # -> backups/diwan-<UTC stamp>.bak (copy it off the server)
|
||||||
|
TARGET_DB=diwan_restoretest deploy/restore.sh backups/diwan-<stamp>.bak # test a backup side by side
|
||||||
|
```
|
||||||
|
|
||||||
|
Schedule `deploy/backup.sh` with cron (e.g. daily) and copy `backups/` off the server. A backup counts only once a test restore succeeds.
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
|
||||||
|
1. `docker compose stop api site`
|
||||||
|
2. `git checkout <previous commit or tag>`
|
||||||
|
3. If the failed version applied database migrations: `deploy/restore.sh backups/<backup taken before the update>.bak`
|
||||||
|
4. `docker compose up -d --build`
|
||||||
|
|
||||||
|
Restoring replaces the `diwan` database, so anything written after that backup (comments, edits) is lost.
|
||||||
|
|
||||||
## Run locally (macOS/Linux)
|
## Run locally (macOS/Linux)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
20
deploy/backup.sh
Executable file
20
deploy/backup.sh
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Back up the Diwan SQL Server database to ./backups/diwan-<UTC timestamp>.bak
|
||||||
|
# deploy/backup.sh (docker-compose stack: service "db")
|
||||||
|
# DB_CONTAINER=diwan-mssql deploy/backup.sh (local run-local.sh container)
|
||||||
|
# Needs MSSQL_SA_PASSWORD (read from .env when present).
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
[ -f .env ] && set -a && . ./.env && set +a
|
||||||
|
: "${MSSQL_SA_PASSWORD:?set MSSQL_SA_PASSWORD}"
|
||||||
|
DB=${DB_NAME:-diwan}
|
||||||
|
C=${DB_CONTAINER:-$(docker compose ps -q db)}
|
||||||
|
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
|
||||||
|
FILE=/var/opt/mssql/backup/$DB-$STAMP.bak
|
||||||
|
docker exec "$C" mkdir -p /var/opt/mssql/backup
|
||||||
|
docker exec "$C" /opt/mssql-tools18/bin/sqlcmd -C -b -S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||||
|
-Q "BACKUP DATABASE [$DB] TO DISK = N'$FILE' WITH INIT, CHECKSUM"
|
||||||
|
mkdir -p backups
|
||||||
|
docker cp "$C:$FILE" "backups/$DB-$STAMP.bak"
|
||||||
|
docker exec "$C" rm -f "$FILE"
|
||||||
|
echo "backups/$DB-$STAMP.bak"
|
||||||
30
deploy/restore.sh
Executable file
30
deploy/restore.sh
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Restore a backup made by deploy/backup.sh.
|
||||||
|
# deploy/restore.sh backups/diwan-<stamp>.bak (replaces database "diwan"; stop the api first)
|
||||||
|
# TARGET_DB=diwan_restoretest deploy/restore.sh <file> (restore side by side, e.g. to test a backup)
|
||||||
|
# DB_CONTAINER as in backup.sh. Needs MSSQL_SA_PASSWORD.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
[ -f .env ] && set -a && . ./.env && set +a
|
||||||
|
: "${MSSQL_SA_PASSWORD:?set MSSQL_SA_PASSWORD}"
|
||||||
|
BAK=${1:?usage: deploy/restore.sh <backup file>}
|
||||||
|
TARGET=${TARGET_DB:-diwan}
|
||||||
|
C=${DB_CONTAINER:-$(docker compose ps -q db)}
|
||||||
|
SQL() { docker exec "$C" /opt/mssql-tools18/bin/sqlcmd -C -b -S localhost -U sa -P "$MSSQL_SA_PASSWORD" "$@"; }
|
||||||
|
docker exec "$C" mkdir -p /var/opt/mssql/backup
|
||||||
|
docker cp "$BAK" "$C:/var/opt/mssql/backup/restore.bak"
|
||||||
|
docker exec -u 0 "$C" chown mssql /var/opt/mssql/backup/restore.bak # docker cp keeps the host uid; sqlservr runs as mssql
|
||||||
|
# logical file names inside the backup, moved to files named after the target database
|
||||||
|
FILES=$(SQL -h -1 -W -s '|' -Q "SET NOCOUNT ON; RESTORE FILELISTONLY FROM DISK = N'/var/opt/mssql/backup/restore.bak'" | awk -F'|' 'NF>2 {print $1"|"$3}')
|
||||||
|
[ -n "$FILES" ] || { echo "could not read the backup's file list" >&2; exit 1; }
|
||||||
|
MOVE=""
|
||||||
|
while IFS='|' read -r logical type; do
|
||||||
|
[ -z "$logical" ] && continue
|
||||||
|
ext=mdf; [ "$type" = L ] && ext=ldf
|
||||||
|
MOVE="$MOVE, MOVE N'$logical' TO N'/var/opt/mssql/data/${TARGET}_${logical}.$ext'"
|
||||||
|
done <<< "$FILES"
|
||||||
|
SQL -Q "IF DB_ID(N'$TARGET') IS NOT NULL ALTER DATABASE [$TARGET] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
||||||
|
RESTORE DATABASE [$TARGET] FROM DISK = N'/var/opt/mssql/backup/restore.bak' WITH REPLACE, CHECKSUM$MOVE;
|
||||||
|
ALTER DATABASE [$TARGET] SET MULTI_USER;"
|
||||||
|
docker exec "$C" rm -f /var/opt/mssql/backup/restore.bak
|
||||||
|
echo "restored $BAK into [$TARGET]"
|
||||||
Loading…
Reference in New Issue
Block a user