diff --git a/compose.production.yaml b/compose.production.yaml index e07cdeb..b936270 100644 --- a/compose.production.yaml +++ b/compose.production.yaml @@ -124,11 +124,15 @@ services: limits: {cpus: "0.75", memory: 512M} storage-tool: - image: alpine:3.22 + image: minio/mc:RELEASE.2025-07-21T05-28-08Z profiles: [tools] + environment: + S3_ACCESS_KEY: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY} + S3_SECRET_KEY: ${S3_SECRET_KEY:?Set S3_SECRET_KEY} + S3_BUCKET: ${S3_BUCKET:-catch-screenshots} volumes: - - minio_data:/data - ${BACKUP_DIRECTORY:-./backups}:/backup + networks: [backend] security_opt: [no-new-privileges:true] networks: diff --git a/deploy/README.md b/deploy/README.md index 8ea75e8..91ae8ac 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -80,11 +80,16 @@ CONFIRM_RESTORE=rf4-spotter ./deploy/restore.sh /srv/rf4-backups/20260906T120000 curl -fsS https://rf4spotter.ru/ready ``` -Перед приглашением пользователей проведите учебное восстановление на отдельном сервере или с отдельным Compose project name. Одной только успешной архивации недостаточно. После проверки настройте ежедневный запуск `backup.sh`, шифрование/выгрузку копий во внешнее хранилище и уведомление при ошибке; храните минимум 7 ежедневных и 4 еженедельных копии. +Репозиторий содержит изолированный drill, который создаёт отдельные Compose volumes, портит тестовые данные, восстанавливает их и сравнивает PostgreSQL и MinIO: + +```bash +./deploy/test-backup-restore.sh +``` + +Drill успешно пройден 6 сентября 2026 года. На целевом сервере всё равно проведите учебное восстановление с реальной зашифрованной копией перед приглашением пользователей. Затем настройте ежедневный запуск `backup.sh`, выгрузку копий во внешнее хранилище и уведомление при ошибке; храните минимум 7 ежедневных и 4 еженедельных копии. ## 7. Что ещё блокирует приглашение альфа-пользователей -- учебное восстановление PostgreSQL и MinIO на изолированном контуре; - проверка полного bootstrap на пустых volumes; - политика хранения и удаления пользовательских данных; - базовый мониторинг `/ready`, диска и срока TLS-сертификата. diff --git a/deploy/backup.sh b/deploy/backup.sh index ec8f739..1ecabb3 100755 --- a/deploy/backup.sh +++ b/deploy/backup.sh @@ -14,13 +14,20 @@ esac backup_root=${1%/} stamp=$(date -u +%Y%m%dT%H%M%SZ) target="$backup_root/$stamp" +if [ -e "$target" ]; then + echo "Backup target already exists: $target" >&2 + exit 1 +fi mkdir -p "$target" chmod 700 "$target" export BACKUP_DIRECTORY="$backup_root" -compose="docker compose --env-file .env.production -f compose.production.yaml" +env_file=${COMPOSE_ENV_FILE:-.env.production} +compose="docker compose --env-file $env_file -f compose.production.yaml" $compose exec -T db sh -c 'pg_dump --format=custom --no-owner --no-acl -U "$POSTGRES_USER" -d "$POSTGRES_DB"' > "$target/postgres.dump" -$compose run --rm storage-tool tar -czf "/backup/$stamp/minio.tar.gz" -C /data . -sha256sum "$target/postgres.dump" "$target/minio.tar.gz" > "$target/SHA256SUMS" +$compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null && mc mb --ignore-existing "local/$S3_BUCKET" >/dev/null && mc mirror --overwrite "local/$S3_BUCKET" "/backup/'"$stamp"'/minio" && chmod -R a+rwX "/backup/'"$stamp"'/minio"' +tar -czf "$target/minio.tar.gz" -C "$target" minio +rm -rf "$target/minio" +(cd "$target" && sha256sum postgres.dump minio.tar.gz > SHA256SUMS) chmod 600 "$target/postgres.dump" "$target/minio.tar.gz" "$target/SHA256SUMS" echo "Backup created: $target" diff --git a/deploy/restore.sh b/deploy/restore.sh index 00048a5..b02d1ea 100755 --- a/deploy/restore.sh +++ b/deploy/restore.sh @@ -16,12 +16,16 @@ test -f "$source_dir/postgres.dump" test -f "$source_dir/minio.tar.gz" (cd "$source_dir" && sha256sum -c SHA256SUMS) backup_root=$(dirname "$source_dir") -stamp=$(basename "$source_dir") export BACKUP_DIRECTORY="$backup_root" +restore_tmp=$(mktemp -d "$backup_root/.rf4-restore.XXXXXX") +trap 'rm -rf "$restore_tmp"' EXIT INT TERM +tar -xzf "$source_dir/minio.tar.gz" -C "$restore_tmp" +restore_name=$(basename "$restore_tmp") -compose="docker compose --env-file .env.production -f compose.production.yaml" -$compose stop api web minio +env_file=${COMPOSE_ENV_FILE:-.env.production} +compose="docker compose --env-file $env_file -f compose.production.yaml" +$compose stop api web $compose exec -T db sh -c 'pg_restore --clean --if-exists --exit-on-error --no-owner --no-acl -U "$POSTGRES_USER" -d "$POSTGRES_DB"' < "$source_dir/postgres.dump" -$compose run --rm storage-tool sh -c 'find /data -mindepth 1 -delete && tar -xzf "/backup/'"$stamp"'/minio.tar.gz" -C /data' -$compose up -d minio api web proxy +$compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null && mc rm --recursive --force "local/$S3_BUCKET" >/dev/null 2>&1 || true; mc mb --ignore-existing "local/$S3_BUCKET" >/dev/null && mc mirror --overwrite "/backup/'"$restore_name"'/minio" "local/$S3_BUCKET"' +$compose up -d ${RESTORE_START_SERVICES:-minio api web proxy} echo "Restore completed from: $source_dir" diff --git a/deploy/test-backup-restore.sh b/deploy/test-backup-restore.sh new file mode 100755 index 0000000..9669be5 --- /dev/null +++ b/deploy/test-backup-restore.sh @@ -0,0 +1,33 @@ +#!/bin/sh +set -eu + +repo=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +cd "$repo" +drill_root=$(mktemp -d /tmp/rf4-restore-drill.XXXXXX) +project="rf4-restore-drill-$$" +export COMPOSE_PROJECT_NAME="$project" +export COMPOSE_ENV_FILE=.env.production.example +export BACKUP_DIRECTORY="$drill_root" +compose="docker compose --env-file $COMPOSE_ENV_FILE -f compose.production.yaml" + +cleanup() { + $compose down --volumes --remove-orphans >/dev/null 2>&1 || true + rm -rf "$drill_root" +} +trap cleanup EXIT INT TERM + +$compose up -d --wait db minio +$compose exec -T db sh -c 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "create table restore_drill (value text primary key); insert into restore_drill values ('"'"'database-ok'"'"');"' >/dev/null +$compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null; mc mb --ignore-existing "local/$S3_BUCKET" >/dev/null; printf object-ok | mc pipe "local/$S3_BUCKET/restore-drill.txt"' >/dev/null + +./deploy/backup.sh "$drill_root" +backup=$(find "$drill_root" -mindepth 1 -maxdepth 1 -type d ! -name '.rf4-restore.*' | head -n 1) +$compose exec -T db sh -c 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "drop table restore_drill;"' >/dev/null +$compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null; mc rm --force "local/$S3_BUCKET/restore-drill.txt"' >/dev/null + +CONFIRM_RESTORE=rf4-spotter RESTORE_START_SERVICES=minio ./deploy/restore.sh "$backup" +db_value=$($compose exec -T db sh -c 'psql -At -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "select value from restore_drill;"') +object_value=$($compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null; mc cat "local/$S3_BUCKET/restore-drill.txt"') +test "$db_value" = "database-ok" +test "$object_value" = "object-ok" +echo "Backup/restore drill passed for PostgreSQL and MinIO" diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f645ade..af38982 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -66,7 +66,7 @@ - [ ] Провести security-проверку admin-аутентификации, CORS, security headers, загрузок и управления секретами; вынести допустимые origins в конфигурацию и исключить демонстрационные секреты в production-режиме. - [x] Проверить авторизацию повторной загрузки скриншота: используется отдельный одноразовый случайный токен, в БД хранится только SHA-256, UUID заявки недостаточно. - [ ] Определить сроки хранения ников, исходных payload, staging-наблюдений, moderation events и submission attempts; добавить документированную очистку/анонимизацию. -- [ ] Добавить резервное копирование и документированное восстановление PostgreSQL и MinIO: скрипты, контрольные суммы и runbook готовы; остаётся учебное восстановление на отдельных временных volumes. +- [x] Добавить резервное копирование и документированное восстановление PostgreSQL и MinIO: консистентные `pg_dump` и MinIO API mirror, контрольные суммы, runbook и успешный изолированный drill с намеренным удалением данных (6 сентября 2026). - [ ] Проверить доступность интерфейса: клавиатура, focus states, контраст, подписи полей и семантика таблиц/карточек. - [ ] Провести Lighthouse-проверку основных страниц и устранить критические проблемы производительности. - [x] Провести UI/UX-аудит desktop/mobile и сформировать приоритетный план (`docs/UI_UX_AUDIT.md`). @@ -79,7 +79,7 @@ - [ ] Добавить smoke-проверку административной очереди внешних источников на desktop/mobile без публикации реальных записей. - [ ] Обновить README: архитектура, все переменные окружения, импорт, модерация, backup/restore, эксплуатация логов и известные ограничения. - [ ] Выбрать лицензию кода и политику использования данных. -- [ ] Завершить production-профиль для `rf4spotter.ru`: Compose, Caddy/TLS, закрытые внутренние сервисы, CORS, resource limits, fail-fast секреты и runbook добавлены; остаются backup/restore и проверка на целевом сервере. +- [ ] Завершить production-профиль для `rf4spotter.ru`: Compose, Caddy/TLS, закрытые внутренние сервисы, CORS, resource limits, fail-fast секреты, backup/restore и runbook добавлены; остаются полный bootstrap и проверка на целевом сервере. - [ ] Заменить демонстрационные секреты и определить целевое размещение перед внешней публикацией. ## Источники данных и согласование