chore(deploy): leave database backups to external tools
The app no longer dumps Postgres, restores it, or checks copies in CI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
978f03b8f8
commit
dfc72b1d69
13 files changed
+16
-300
No files matched your search
@@ -10,13 +10,11 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'services/api/**'
|
- 'services/api/**'
|
||||||
- 'services/recommender/**'
|
- 'services/recommender/**'
|
||||||
- 'deploy/backup.sh'
|
|
||||||
- '.gitea/workflows/ci-db.yml'
|
- '.gitea/workflows/ci-db.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'services/api/**'
|
- 'services/api/**'
|
||||||
- 'services/recommender/**'
|
- 'services/recommender/**'
|
||||||
- 'deploy/backup.sh'
|
|
||||||
- '.gitea/workflows/ci-db.yml'
|
- '.gitea/workflows/ci-db.yml'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -84,26 +82,3 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
uv sync --frozen
|
uv sync --frozen
|
||||||
uv run pytest -q tests/test_db_integration.py
|
uv run pytest -q tests/test_db_integration.py
|
||||||
# База уже наполнена сценариями выше — проверяем настоящий цикл копия → проверка → восстановление.
|
|
||||||
- name: Бэкап, проверка и восстановление (deploy/backup.sh)
|
|
||||||
env:
|
|
||||||
PGHOST: postgres
|
|
||||||
PGUSER: pipu
|
|
||||||
PGPASSWORD: pipu
|
|
||||||
PGDATABASE: pipu
|
|
||||||
BACKUP_DIR: /tmp/backups
|
|
||||||
VERIFY_EXACT: "1"
|
|
||||||
run: |
|
|
||||||
SUDO=$(command -v sudo || true)
|
|
||||||
$SUDO install -d /usr/share/postgresql-common/pgdg
|
|
||||||
$SUDO curl -fsSLo /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc
|
|
||||||
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(. /etc/os-release && echo "$VERSION_CODENAME")-pgdg main" | $SUDO tee /etc/apt/sources.list.d/pgdg.list >/dev/null
|
|
||||||
$SUDO apt-get update -qq && $SUDO apt-get install -y -qq postgresql-client-17 >/dev/null
|
|
||||||
export PATH=/usr/lib/postgresql/17/bin:$PATH
|
|
||||||
before=$(psql -Atc "SELECT count(*) FROM tracks")
|
|
||||||
file=$(bash deploy/backup.sh once)
|
|
||||||
bash deploy/backup.sh verify "$file"
|
|
||||||
bash deploy/backup.sh restore "$file"
|
|
||||||
after=$(psql -Atc "SELECT count(*) FROM tracks")
|
|
||||||
echo "tracks: $before → $after"
|
|
||||||
test "$before" = "$after"
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Резервные копии PostgreSQL (docs/deployment.md#бэкапы). Работает в контейнере backup (compose.yaml) и в CI;
|
|
||||||
# подключение — стандартные PGHOST, PGUSER, PGPASSWORD, PGDATABASE.
|
|
||||||
# backup.sh loop — каждый день в BACKUP_HOUR (UTC): копия и ротация; раз в неделю — проверка восстановления
|
|
||||||
# backup.sh once — копия сейчас; печатает путь к файлу
|
|
||||||
# backup.sh verify FILE — восстановить FILE во временную базу и сверить таблицы и число строк
|
|
||||||
# backup.sh restore FILE — заменить рабочую базу копией (сервисы, пишущие в базу, должны быть остановлены);
|
|
||||||
# прежняя база остаётся рядом как <db>_before_restore_<время>
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
DIR="${BACKUP_DIR:-/backups}"
|
|
||||||
DB="${PGDATABASE:-pipu}"
|
|
||||||
KEEP_DAILY="${BACKUP_KEEP_DAILY:-14}"
|
|
||||||
KEEP_WEEKLY="${BACKUP_KEEP_WEEKLY:-8}"
|
|
||||||
|
|
||||||
log() { echo "$(date -u +%FT%TZ) backup: $*" >&2; }
|
|
||||||
|
|
||||||
# Число строк в каждой таблице базы $1: «таблица<TAB>строк», по алфавиту.
|
|
||||||
counts() {
|
|
||||||
psql -X -d "$1" -Atq -F $'\t' -c "
|
|
||||||
SELECT table_name,
|
|
||||||
(xpath('/row/c/text()', query_to_xml(format('SELECT count(*) AS c FROM %I.%I', table_schema, table_name),
|
|
||||||
false, true, '')))[1]::text
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
||||||
ORDER BY 1"
|
|
||||||
}
|
|
||||||
|
|
||||||
rotate() {
|
|
||||||
# shellcheck disable=SC2012 # имена файлов наши, без пробелов
|
|
||||||
ls -1t "$DIR"/daily/pipu-*.dump 2>/dev/null | tail -n +$((KEEP_DAILY + 1)) | xargs -r rm -f --
|
|
||||||
# shellcheck disable=SC2012
|
|
||||||
ls -1t "$DIR"/weekly/pipu-*.dump 2>/dev/null | tail -n +$((KEEP_WEEKLY + 1)) | xargs -r rm -f --
|
|
||||||
find "$DIR/daily" "$DIR/weekly" -name '*.counts' | while read -r c; do
|
|
||||||
[ -f "${c%.counts}.dump" ] || rm -f -- "$c"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
once() {
|
|
||||||
mkdir -p "$DIR/daily" "$DIR/weekly"
|
|
||||||
local name file
|
|
||||||
name="pipu-$(date -u +%Y%m%d-%H%M%S)"
|
|
||||||
file="$DIR/daily/$name.dump"
|
|
||||||
pg_dump -Fc -d "$DB" -f "$file.part"
|
|
||||||
pg_restore -l "$file.part" >/dev/null # оглавление читается — архив не обрезан
|
|
||||||
counts "$DB" >"$file.counts" # для verify: что было в базе в момент копии
|
|
||||||
mv "$file.part" "$file"
|
|
||||||
# Еженедельная копия — первая за 7 дней (не зависит от того, в какой день сервер был выключен).
|
|
||||||
if ! find "$DIR/weekly" -name 'pipu-*.dump' -mtime -7 | grep -q .; then
|
|
||||||
cp "$file" "$DIR/weekly/$name.dump"
|
|
||||||
cp "$file.counts" "$DIR/weekly/$name.dump.counts"
|
|
||||||
fi
|
|
||||||
rotate
|
|
||||||
log "$file ($(du -h "$file" | cut -f1))"
|
|
||||||
echo "$file"
|
|
||||||
}
|
|
||||||
|
|
||||||
verify() {
|
|
||||||
local file="$1" tmpdb="${DB}_verify" got want
|
|
||||||
[ -f "$file" ] || { log "нет файла $file"; return 1; }
|
|
||||||
dropdb --if-exists "$tmpdb"
|
|
||||||
createdb "$tmpdb"
|
|
||||||
if ! pg_restore --no-owner --exit-on-error -d "$tmpdb" "$file"; then
|
|
||||||
dropdb --if-exists "$tmpdb"
|
|
||||||
log "ПРОВЕРКА НЕ ПРОЙДЕНА: $file не восстанавливается"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
got="$(counts "$tmpdb")"
|
|
||||||
dropdb "$tmpdb"
|
|
||||||
want="$(cat "$file.counts" 2>/dev/null || counts "$DB")"
|
|
||||||
# Таблица пропала или опустела — копия негодна. Расхождение в числе строк — рабочая база менялась во время
|
|
||||||
# копии (предупреждение); VERIFY_EXACT=1 (CI, база в покое) — тоже ошибка.
|
|
||||||
local bad=0 table n m
|
|
||||||
while IFS=$'\t' read -r table n; do
|
|
||||||
m="$(printf '%s\n' "$got" | awk -F'\t' -v t="$table" '$1 == t { print $2 }')"
|
|
||||||
if [ -z "$m" ]; then
|
|
||||||
log "в копии нет таблицы $table"
|
|
||||||
bad=1
|
|
||||||
elif [ "$m" = 0 ] && [ "$n" != 0 ]; then
|
|
||||||
log "в копии пуста таблица $table (было $n)"
|
|
||||||
bad=1
|
|
||||||
elif [ "$m" != "$n" ]; then
|
|
||||||
log "$table: $m строк в копии, $n при копировании"
|
|
||||||
[ "${VERIFY_EXACT:-}" = 1 ] && bad=1
|
|
||||||
fi
|
|
||||||
done <<<"$want"
|
|
||||||
if [ "$bad" = 1 ]; then
|
|
||||||
log "ПРОВЕРКА НЕ ПРОЙДЕНА: $file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
log "проверено: $file восстанавливается ($(printf '%s\n' "$got" | wc -l) таблиц)"
|
|
||||||
date -u +%FT%TZ >"$DIR/.last-verify"
|
|
||||||
}
|
|
||||||
|
|
||||||
restore() {
|
|
||||||
local file="$1" old
|
|
||||||
verify "$file"
|
|
||||||
old="${DB}_before_restore_$(date -u +%Y%m%d%H%M%S)"
|
|
||||||
psql -X -d postgres -Atqc "SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
|
||||||
WHERE datname = '$DB' AND pid <> pg_backend_pid()" >/dev/null
|
|
||||||
psql -X -d postgres -qc "ALTER DATABASE \"$DB\" RENAME TO \"$old\""
|
|
||||||
createdb "$DB"
|
|
||||||
if ! pg_restore --no-owner --exit-on-error -d "$DB" "$file"; then
|
|
||||||
dropdb "$DB"
|
|
||||||
psql -X -d postgres -qc "ALTER DATABASE \"$old\" RENAME TO \"$DB\""
|
|
||||||
log "восстановление не удалось — рабочая база не тронута"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
log "база $DB восстановлена из $file; прежняя сохранена как $old (удалить: dropdb $old)"
|
|
||||||
}
|
|
||||||
|
|
||||||
loop() {
|
|
||||||
local hour="${BACKUP_HOUR:-3}" now next file
|
|
||||||
log "ежедневная копия в $hour:30 UTC, хранится $KEEP_DAILY ежедневных и $KEEP_WEEKLY еженедельных"
|
|
||||||
while true; do
|
|
||||||
now="$(date -u +%s)"
|
|
||||||
next="$(date -u -d "today $hour:30" +%s)"
|
|
||||||
[ "$next" -gt "$now" ] || next="$(date -u -d "tomorrow $hour:30" +%s)"
|
|
||||||
sleep $((next - now))
|
|
||||||
if ! file="$(once)"; then
|
|
||||||
log "КОПИЯ НЕ СДЕЛАНА"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [ -z "$(find "$DIR" -maxdepth 1 -name .last-verify -mtime -7)" ]; then
|
|
||||||
verify "$file" || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-}" in
|
|
||||||
loop) loop ;;
|
|
||||||
once) once ;;
|
|
||||||
verify) verify "${2:?укажите файл копии}" ;;
|
|
||||||
restore) restore "${2:?укажите файл копии}" ;;
|
|
||||||
*)
|
|
||||||
sed -n '2,9p' "$0" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -145,26 +145,6 @@ services:
|
|||||||
interval: 5s
|
interval: 5s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
# Ежедневная копия базы и еженедельная проверка восстановления (deploy/backup.sh, docs/deployment.md#бэкапы).
|
|
||||||
backup:
|
|
||||||
<<: *restart
|
|
||||||
image: pgvector/pgvector:0.8.7-pg17
|
|
||||||
entrypoint: ["bash", "/backup.sh"]
|
|
||||||
command: ["loop"]
|
|
||||||
environment:
|
|
||||||
PGHOST: postgres
|
|
||||||
PGUSER: pipu
|
|
||||||
PGPASSWORD: ${POSTGRES_PASSWORD}
|
|
||||||
PGDATABASE: pipu
|
|
||||||
BACKUP_HOUR: ${BACKUP_HOUR:-3}
|
|
||||||
BACKUP_KEEP_DAILY: ${BACKUP_KEEP_DAILY:-14}
|
|
||||||
BACKUP_KEEP_WEEKLY: ${BACKUP_KEEP_WEEKLY:-8}
|
|
||||||
volumes:
|
|
||||||
- ./backup.sh:/backup.sh:ro
|
|
||||||
- ${PIPU_DATA:-/srv/pipu}/backups:/backups
|
|
||||||
depends_on:
|
|
||||||
postgres: { condition: service_healthy }
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
<<: *restart
|
<<: *restart
|
||||||
image: redis:7.4-alpine
|
image: redis:7.4-alpine
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ OWNER="${SUDO_USER:-root}"
|
|||||||
DATA="${PIPU_DATA:-/srv/pipu}"
|
DATA="${PIPU_DATA:-/srv/pipu}"
|
||||||
|
|
||||||
echo "→ каталоги в $DATA"
|
echo "→ каталоги в $DATA"
|
||||||
mkdir -p "$DATA"/{library,library-host,cache/transcode,cache/covers,postgres,meili,redis,models,updates,uploads,backups,caddy/data,caddy/config}
|
mkdir -p "$DATA"/{library,library-host,cache/transcode,cache/covers,postgres,meili,redis,models,updates,uploads,caddy/data,caddy/config}
|
||||||
|
|
||||||
# Каталоги, куда пишут сервисы без root: api (distroless nonroot, uid 65532) и analyzer (uid 10001).
|
# Каталоги, куда пишут сервисы без root: api (distroless nonroot, uid 65532) и analyzer (uid 10001).
|
||||||
# Без рекурсии: файлы внутри создают сами сервисы, а повторный запуск не обходит весь кэш.
|
# Без рекурсии: файлы внутри создают сами сервисы, а повторный запуск не обходит весь кэш.
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Восстановление базы из резервной копии (docs/deployment.md#бэкапы).
|
|
||||||
# ./restore.sh — список копий
|
|
||||||
# ./restore.sh daily/pipu-20261020-033000.dump
|
|
||||||
# Сервисы, пишущие в базу, останавливаются на время восстановления; прежняя база сохраняется рядом.
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
[ -f .env ] || { echo "нет .env — сначала sudo ./install.sh" >&2; exit 1; }
|
|
||||||
DATA="$(sed -n 's/^PIPU_DATA=//p' .env)"
|
|
||||||
DATA="${DATA:-/srv/pipu}"
|
|
||||||
|
|
||||||
if [ -z "${1:-}" ]; then
|
|
||||||
echo "Копии в $DATA/backups (новые сверху):"
|
|
||||||
(cd "$DATA/backups" && ls -1t daily/*.dump weekly/*.dump 2>/dev/null) || echo " копий нет"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
rel="${1#"$DATA/backups/"}"
|
|
||||||
[ -f "$DATA/backups/$rel" ] || { echo "нет файла $DATA/backups/$rel" >&2; exit 1; }
|
|
||||||
|
|
||||||
echo "→ восстановление из $rel: api, рекомендации и анализ будут остановлены на время восстановления"
|
|
||||||
read -r -p "Продолжить? [y/N] " yes
|
|
||||||
[ "$yes" = y ] || [ "$yes" = Y ] || exit 1
|
|
||||||
|
|
||||||
docker compose stop api recommender analyzer storage-agent
|
|
||||||
if docker compose run --rm backup restore "/backups/$rel"; then
|
|
||||||
status=0
|
|
||||||
else
|
|
||||||
status=1
|
|
||||||
fi
|
|
||||||
docker compose up -d
|
|
||||||
[ "$status" = 0 ] || { echo "восстановление не удалось — база не изменена" >&2; exit 1; }
|
|
||||||
echo "готово. Поиск переиндексируется при старте api (или вручную: «Администрирование → Библиотека»)."
|
|
||||||
+1
-7
@@ -88,19 +88,13 @@ tgt_core="$(core "$TARGET" || true)"
|
|||||||
if [ -n "$cur_core" ] && [ -n "$tgt_core" ] && [ "$cur_core" != "$tgt_core" ] &&
|
if [ -n "$cur_core" ] && [ -n "$tgt_core" ] && [ "$cur_core" != "$tgt_core" ] &&
|
||||||
[ "$(printf '%s\n%s\n' "$cur_core" "$tgt_core" | sort -V | head -1)" = "$tgt_core" ]; then
|
[ "$(printf '%s\n%s\n' "$cur_core" "$tgt_core" | sort -V | head -1)" = "$tgt_core" ]; then
|
||||||
echo "внимание: $CURRENT → $TARGET — это откат. Миграции БД не откатываются;" >&2
|
echo "внимание: $CURRENT → $TARGET — это откат. Миграции БД не откатываются;" >&2
|
||||||
echo " сделайте резервную копию (docs/deployment.md) и проверьте, что старая версия совместима со схемой." >&2
|
echo " копию базы сделайте внешними средствами и проверьте, что старая версия совместима со схемой." >&2
|
||||||
if [ "${PIPU_ALLOW_DOWNGRADE:-}" != 1 ]; then
|
if [ "${PIPU_ALLOW_DOWNGRADE:-}" != 1 ]; then
|
||||||
echo " Чтобы продолжить: PIPU_ALLOW_DOWNGRADE=1 ./update.sh $REF" >&2
|
echo " Чтобы продолжить: PIPU_ALLOW_DOWNGRADE=1 ./update.sh $REF" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Копия базы перед обновлением: миграции назад не откатываются (deploy/restore.sh — вернуть).
|
|
||||||
if [ -n "$(docker compose ps -q --status running postgres 2>/dev/null)" ]; then
|
|
||||||
echo "→ резервная копия базы"
|
|
||||||
docker compose run --rm --no-deps backup once >/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "→ переключаемся на $REF"
|
echo "→ переключаемся на $REF"
|
||||||
if git show-ref --verify --quiet "refs/remotes/origin/$REF"; then
|
if git show-ref --verify --quiet "refs/remotes/origin/$REF"; then
|
||||||
git checkout --quiet "$REF"
|
git checkout --quiet "$REF"
|
||||||
|
|||||||
+2
-2
@@ -23,8 +23,8 @@ Pipu Music — self-hosted музыкальный сервис (веб + Android
|
|||||||
| [testing.md](testing.md) | Стратегия тестирования |
|
| [testing.md](testing.md) | Стратегия тестирования |
|
||||||
| [ci-cd.md](ci-cd.md) | Gitea Actions: проверки и релиз по тегу |
|
| [ci-cd.md](ci-cd.md) | Gitea Actions: проверки и релиз по тегу |
|
||||||
| [install-guide.md](install-guide.md) | **Пошагово:** установка сервера на домен с HTTPS, подключение веба и Android |
|
| [install-guide.md](install-guide.md) | **Пошагово:** установка сервера на домен с HTTPS, подключение веба и Android |
|
||||||
| [admin-guide.md](admin-guide.md) | **Администратору:** пользователи, хранилища, очередь, волна, обновления, бэкапы, диагностика |
|
| [admin-guide.md](admin-guide.md) | **Администратору:** пользователи, хранилища, очередь, волна, обновления, диагностика |
|
||||||
| [deployment.md](deployment.md) | Справочник по развёртыванию: сервисы, каталоги, переменные, бэкапы |
|
| [deployment.md](deployment.md) | Справочник по развёртыванию: сервисы, каталоги, переменные |
|
||||||
| [conventions.md](conventions.md) | Соглашения по коду, git, версиям |
|
| [conventions.md](conventions.md) | Соглашения по коду, git, версиям |
|
||||||
| [roadmap.md](roadmap.md) | Этапы разработки и критерии готовности |
|
| [roadmap.md](roadmap.md) | Этапы разработки и критерии готовности |
|
||||||
|
|
||||||
|
|||||||
+6
-21
@@ -1,7 +1,7 @@
|
|||||||
# Руководство администратора
|
# Руководство администратора
|
||||||
|
|
||||||
Повседневная работа с сервером после установки ([install-guide.md](install-guide.md)): пользователи, музыка,
|
Повседневная работа с сервером после установки ([install-guide.md](install-guide.md)): пользователи, музыка,
|
||||||
очередь на добавление, «Моя волна», обновления, бэкапы и диагностика. Все команды — из каталога
|
очередь на добавление, «Моя волна», обновления и диагностика. Все команды — из каталога
|
||||||
`/opt/pipu-music/deploy`. Справочник по сервисам и переменным — [deployment.md](deployment.md).
|
`/opt/pipu-music/deploy`. Справочник по сервисам и переменным — [deployment.md](deployment.md).
|
||||||
|
|
||||||
## 1. Первый вход
|
## 1. Первый вход
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
./update.sh
|
./update.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Скрипт делает копию базы, переключается на последний релиз, собирает образы, перезапускает сервисы и ждёт
|
Скрипт переключается на последний релиз, собирает образы, перезапускает сервисы и ждёт
|
||||||
готовности; миграции применяются при старте api. Конкретная версия — `./update.sh v1.0.0`; откат на старую версию
|
готовности; миграции применяются при старте api. Конкретная версия — `./update.sh v1.0.0`; откат на старую версию
|
||||||
требует `PIPU_ALLOW_DOWNGRADE=1` (миграции не откатываются — сначала проверьте совместимость). Перед обновлением
|
требует `PIPU_ALLOW_DOWNGRADE=1` (миграции не откатываются — сначала проверьте совместимость). Перед обновлением
|
||||||
прочитайте заметки к релизу на странице релизов Gitea.
|
прочитайте заметки к релизу на странице релизов Gitea.
|
||||||
@@ -99,33 +99,18 @@ Android-приложение обновляется само: при запус
|
|||||||
Раздать приложение — ссылка `https://<ваш-домен>/android` (без входа): кнопка «Скачать APK» последнего релиза,
|
Раздать приложение — ссылка `https://<ваш-домен>/android` (без входа): кнопка «Скачать APK» последнего релиза,
|
||||||
адрес сервера для копирования и короткая инструкция; та же страница — внизу экрана входа и в меню профиля.
|
адрес сервера для копирования и короткая инструкция; та же страница — внизу экрана входа и в меню профиля.
|
||||||
|
|
||||||
## 7. Бэкапы и восстановление
|
## 7. Наблюдение
|
||||||
|
|
||||||
- Каждую ночь (`BACKUP_HOUR`, по умолчанию 3:30 UTC) контейнер `backup` делает копию базы в
|
|
||||||
`/srv/pipu/backups/daily` (14 последних) и раз в неделю — в `weekly` (8 последних).
|
|
||||||
- Раз в неделю копия **проверяется восстановлением** во временную базу. Результат:
|
|
||||||
`docker compose logs backup | tail`; при ошибке в логе — «ПРОВЕРКА НЕ ПРОЙДЕНА».
|
|
||||||
- Копия сейчас: `docker compose run --rm backup once`.
|
|
||||||
- Восстановление: `./restore.sh` — список копий, `./restore.sh daily/<файл>` — восстановить. Прежняя база
|
|
||||||
остаётся рядом как `pipu_before_restore_<время>`; убедившись, что всё в порядке, удалите её:
|
|
||||||
`docker compose exec postgres dropdb -U pipu pipu_before_restore_<время>`.
|
|
||||||
- В копию входит всё, что нельзя пересоздать: пользователи, история, лайки, состояние плеера, очередь на добавление,
|
|
||||||
результаты аудиоанализа и модели волны. Поиск, обложки и транскоды восстанавливаются сами.
|
|
||||||
- **Сама музыка не бэкапится** приложением — это ваш диск или NFS. Каталог бэкапов тоже лежит на сервере:
|
|
||||||
копируйте `/srv/pipu/backups` на другую машину, например `rsync` по cron.
|
|
||||||
|
|
||||||
## 8. Наблюдение
|
|
||||||
|
|
||||||
| Что | Как |
|
| Что | Как |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Сервер жив и готов | `curl -s https://music.example.com/readyz` → все зависимости `ok` |
|
| Сервер жив и готов | `curl -s https://music.example.com/readyz` → все зависимости `ok` |
|
||||||
| Состояние сервисов | `docker compose ps` |
|
| Состояние сервисов | `docker compose ps` |
|
||||||
| Логи | `docker compose logs -f --tail=100 api` (или `recommender`, `analyzer`, `storage-agent`, `backup`) |
|
| Логи | `docker compose logs -f --tail=100 api` (или `recommender`, `analyzer`, `storage-agent`) |
|
||||||
| Место на диске | `df -h /srv/pipu`; больше всего занимают `cache/transcode` (чистится сам) и `backups` |
|
| Место на диске | `df -h /srv/pipu`; больше всего занимает `cache/transcode` (чистится сам) |
|
||||||
| Версия | `grep PIPU_VERSION .env`, `GET /api/v1/server-info`; Android — внизу вкладки «Профиль» |
|
| Версия | `grep PIPU_VERSION .env`, `GET /api/v1/server-info`; Android — внизу вкладки «Профиль» |
|
||||||
| Нагрузка | `tests/load/smoke.js` — см. [testing.md](testing.md#нагрузочный-smoke) |
|
| Нагрузка | `tests/load/smoke.js` — см. [testing.md](testing.md#нагрузочный-smoke) |
|
||||||
|
|
||||||
## 9. Частые ситуации
|
## 8. Частые ситуации
|
||||||
|
|
||||||
| Ситуация | Что делать |
|
| Ситуация | Что делать |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ jobs:
|
|||||||
- **изменения** — коммиты (Conventional Commits) с прошлого тега по разделам «Новое», «Исправления»,
|
- **изменения** — коммиты (Conventional Commits) с прошлого тега по разделам «Новое», «Исправления»,
|
||||||
«Сборка и CI» и т. д., отдельно — несовместимые (`feat!:`); для финального релиза отсчёт от прошлого
|
«Сборка и CI» и т. д., отдельно — несовместимые (`feat!:`); для финального релиза отсчёт от прошлого
|
||||||
финального, а не от последнего `-rc`; ссылка на полный diff;
|
финального, а не от последнего `-rc`; ссылка на полный diff;
|
||||||
- **обновление сервера** — бэкап, `./update.sh vX.Y.Z`, проверка версии; **установка APK** и проверка SHA-256.
|
- **обновление сервера** — `./update.sh vX.Y.Z`, проверка версии; **установка APK** и проверка SHA-256.
|
||||||
|
|
||||||
Job `android`: Android SDK, `expo prebuild`, `./gradlew assembleRelease --build-cache` (только arm64-v8a), подпись `apksigner`,
|
Job `android`: Android SDK, `expo prebuild`, `./gradlew assembleRelease --build-cache` (только arm64-v8a), подпись `apksigner`,
|
||||||
затем `release.mjs android success <apk>` прикладывает APK и `.sha256` и меняет статус. При любом сбое —
|
затем `release.mjs android success <apk>` прикладывает APK и `.sha256` и меняет статус. При любом сбое —
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
├── models/ # модели Essentia (скачиваются при первом старте analyzer)
|
├── models/ # модели Essentia (скачиваются при первом старте analyzer)
|
||||||
├── updates/ # кэш APK релизов
|
├── updates/ # кэш APK релизов
|
||||||
├── uploads/ # staging очереди на добавление (размер ≈ лимит очереди, по умолчанию 10 ГБ)
|
├── uploads/ # staging очереди на добавление (размер ≈ лимит очереди, по умолчанию 10 ГБ)
|
||||||
├── backups/
|
|
||||||
└── caddy/ # сертификаты Let's Encrypt (если используется compose.tls.yaml)
|
└── caddy/ # сертификаты Let's Encrypt (если используется compose.tls.yaml)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -95,27 +94,6 @@
|
|||||||
|
|
||||||
Пошаговая установка на домен — [install-guide.md](install-guide.md).
|
Пошаговая установка на домен — [install-guide.md](install-guide.md).
|
||||||
|
|
||||||
## Бэкапы
|
|
||||||
|
|
||||||
- Контейнер `backup` (`deploy/backup.sh loop`): каждый день в `BACKUP_HOUR`:30 UTC (по умолчанию 3:30) —
|
|
||||||
`pg_dump -Fc` → `/srv/pipu/backups/daily/`, хранится `BACKUP_KEEP_DAILY` = 14 последних; первая копия за неделю
|
|
||||||
копируется в `weekly/` (`BACKUP_KEEP_WEEKLY` = 8). Рядом с копией — `*.counts`: число строк в каждой таблице.
|
|
||||||
- Раз в неделю копия **проверяется восстановлением**: разворачивается во временную базу `pipu_verify`, сверяются
|
|
||||||
таблицы и число строк, база удаляется; время последней успешной проверки — `backups/.last-verify`, ошибки —
|
|
||||||
в `docker compose logs backup` («ПРОВЕРКА НЕ ПРОЙДЕНА»). Тот же цикл копия → проверка → восстановление
|
|
||||||
выполняется в CI (`ci / db`) на базе после сквозного сценария.
|
|
||||||
- `update.sh` перед обновлением делает внеочередную копию.
|
|
||||||
- Вручную: `docker compose run --rm backup once` — копия сейчас; `docker compose run --rm backup verify /backups/daily/<файл>`.
|
|
||||||
- Восстановление: `./restore.sh` — список копий, `./restore.sh daily/pipu-….dump` — останавливает api, рекомендации и
|
|
||||||
анализ, проверяет копию, переименовывает текущую базу в `pipu_before_restore_<время>` (страховка; удалить —
|
|
||||||
`docker compose exec postgres dropdb -U pipu <имя>`), восстанавливает и запускает сервисы; поиск переиндексируется
|
|
||||||
при старте api. Redis (сессии волны, очереди) не восстанавливается — пользователи просто начнут новую волну.
|
|
||||||
- Копии лежат на том же сервере: для защиты от потери диска синхронизируйте `/srv/pipu/backups` на другую машину
|
|
||||||
(например, `rclone sync` или `rsync` по cron).
|
|
||||||
- Meilisearch, кэши и модели **не бэкапятся** — восстанавливаются (`reindex`, повторный транскод / анализ).
|
|
||||||
- Признаки анализа хранятся в Postgres — после восстановления из бэкапа повторный многочасовой анализ не нужен.
|
|
||||||
- Сама музыка — на NFS, вне зоны ответственности приложения.
|
|
||||||
|
|
||||||
## Обновление
|
## Обновление
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+2
-17
@@ -304,7 +304,7 @@ curl -s https://music.example.com/readyz
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Обновление, бэкапы, удаление
|
## 11. Обновление и удаление
|
||||||
|
|
||||||
Обновление до последнего релиза:
|
Обновление до последнего релиза:
|
||||||
|
|
||||||
@@ -314,22 +314,7 @@ cd /opt/pipu-music/deploy && ./update.sh
|
|||||||
|
|
||||||
Конкретная версия или откат: `./update.sh v0.2.0`. Скрипт делает `git fetch`, переключается на тег,
|
Конкретная версия или откат: `./update.sh v0.2.0`. Скрипт делает `git fetch`, переключается на тег,
|
||||||
пересобирает образы и перезапускает сервисы; старые образы удаляются. Миграции БД применяются
|
пересобирает образы и перезапускает сервисы; старые образы удаляются. Миграции БД применяются
|
||||||
автоматически при старте API.
|
автоматически при старте API. Копии базы и музыки приложение не делает.
|
||||||
|
|
||||||
Бэкапы базы делаются сами: каждую ночь в `/srv/pipu/backups`, раз в неделю копия проверяется восстановлением,
|
|
||||||
перед каждым `update.sh` — внеочередная копия. Всё остальное восстанавливается само — см.
|
|
||||||
[deployment.md](deployment.md#бэкапы). Копия сейчас и восстановление:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/pipu-music/deploy && docker compose run --rm backup once
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/pipu-music/deploy && ./restore.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Без аргумента `restore.sh` показывает список копий; с именем файла (`./restore.sh daily/pipu-20261020-033000.dump`)
|
|
||||||
восстанавливает базу, сохранив прежнюю рядом.
|
|
||||||
|
|
||||||
Остановить сервис, сохранив данные:
|
Остановить сервис, сохранив данные:
|
||||||
|
|
||||||
|
|||||||
+3
-5
@@ -53,11 +53,9 @@
|
|||||||
критерий проверяется по метрикам версий в админке («Библиотека → Моя волна») и NDCG в `model_versions`.
|
критерий проверяется по метрикам версий в админке («Библиотека → Моя волна») и NDCG в `model_versions`.
|
||||||
|
|
||||||
## M7 — Стабилизация (v1.0)
|
## M7 — Стабилизация (v1.0)
|
||||||
- Нагрузочный smoke, бэкапы и восстановление (проверено), документация администратора,
|
- Нагрузочный smoke, документация администратора, доступность, полировка анимаций, багфиксы.
|
||||||
доступность, полировка анимаций, багфиксы.
|
- **Состояние:** v1.0 — [руководство администратора](admin-guide.md), аудит доступности WCAG 2.1 AA, k6-smoke
|
||||||
- **Состояние:** v1.0 — бэкапы с еженедельной проверкой восстановления (и цикл копия → проверка → восстановление
|
(`tests/load/smoke.js`, запускается вручную на сервере перед релизом). Копии базы и музыки — внешними средствами.
|
||||||
в CI), [руководство администратора](admin-guide.md), аудит доступности WCAG 2.1 AA, k6-smoke
|
|
||||||
(`tests/load/smoke.js`, запускается вручную на сервере перед релизом).
|
|
||||||
|
|
||||||
## Дальше (идеи)
|
## Дальше (идеи)
|
||||||
- Плейлисты, волна по исполнителю / треку, ежедневные подборки, тексты песен, Chromecast / DLNA,
|
- Плейлисты, волна по исполнителю / треку, ежедневные подборки, тексты песен, Chromecast / DLNA,
|
||||||
|
|||||||
@@ -109,12 +109,6 @@ export function renderNotes({ version, tag, prevTag, prerelease, repoUrl, groups
|
|||||||
'',
|
'',
|
||||||
'## Обновление сервера',
|
'## Обновление сервера',
|
||||||
'',
|
'',
|
||||||
'Перед обновлением сделайте бэкап базы:',
|
|
||||||
'',
|
|
||||||
'```bash',
|
|
||||||
'cd /opt/pipu-music/deploy && docker compose exec -T postgres pg_dump -U pipu -Fc pipu > /srv/pipu/backups/pipu-$(date +%F).dump',
|
|
||||||
'```',
|
|
||||||
'',
|
|
||||||
'Обновление:',
|
'Обновление:',
|
||||||
'',
|
'',
|
||||||
'```bash',
|
'```bash',
|
||||||
|
|||||||
Reference in new issue
Block a user