diff --git a/.env.example b/.env.example index eede0a4..791638d 100644 --- a/.env.example +++ b/.env.example @@ -123,6 +123,11 @@ BACKUP_VPS_DIR=/srv/fs-backups/restic # в deploy/backup/id_backup (в git НЕ идёт), pubkey — в authorized_keys у fsbackup@VPS. # PowerShell: [Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-Path "deploy\backup\id_backup"))) BACKUP_SSH_KEY_B64= +# Отчёты о бэкапах в Telegram и команды /backups, /status (deploy/backup/README.md, раздел 10). +# id своего чата: напишите боту /start → `docker compose exec backup fs-backup telegram chats`. Несколько — через +# запятую. Пусто = отчёты выключены. Токен по умолчанию — TELEGRAM_BOT_TOKEN приложения. +BACKUP_TELEGRAM_CHAT_ID= +#BACKUP_TELEGRAM_BOT_TOKEN= # Только для ПК (scripts/fs-backup.ps1 / .sh): как зайти на Pi по SSH и где там лежит # docker-compose.yml прода. BACKUP_PI_SSH=pi@192.168.1.10 diff --git a/deploy/backup/Dockerfile b/deploy/backup/Dockerfile index 27bfb70..9474703 100644 --- a/deploy/backup/Dockerfile +++ b/deploy/backup/Dockerfile @@ -4,8 +4,9 @@ FROM restic/restic:0.19.1 # sqlite — консистентная копия и проверка БД; supercronic — cron для контейнера без root; -# tini — корректные сигналы. jq, openssh-client, busybox (wget, flock, tar) уже есть в базе. -RUN apk add --no-cache sqlite supercronic tini +# tini — корректные сигналы; curl — отчёты и бот в Telegram (#83). +# jq, openssh-client, busybox (wget, flock, tar) уже есть в базе. +RUN apk add --no-cache sqlite supercronic tini curl # Тот же uid, что у appuser в образе приложения (10001): файлы после restore получают # правильного владельца, а -wal/-shm SQLite никогда не достаются root. diff --git a/deploy/backup/README.md b/deploy/backup/README.md index 8048f06..dc68c16 100644 --- a/deploy/backup/README.md +++ b/deploy/backup/README.md @@ -831,6 +831,42 @@ ID одного и того же снимка в `local` и `vps` разные **Обновить образ бэкапа** (после изменений в `deploy/backup/`). На ПК — `.\scripts\build-push.ps1`, на Pi — `docker compose up -d backup`. +### Отчёты в Telegram + +Бот приложения (тот же, что для входа через Telegram) может присылать вам отчёты о бэкапах +и отвечать на команды. Пока `BACKUP_TELEGRAM_CHAT_ID` пуст, отчёты выключены. + +**Что приходит само:** +- после каждого бэкапа — «✅ Бэкап»: сколько игроков и партий в БД, по каждому репозиторию + снимок, число снимков и размер; +- если бэкап не удался, в том числе не начавшись (нет пароля, занята другая операция, БД + не прошла проверку) — «❌ Бэкап не удался» с причиной и упавшими репозиториями; +- после еженедельной проверки — «🔍 Проверка данных: OK» или «❌». + +**Команды боту** (отвечает только чатам из `BACKUP_TELEGRAM_CHAT_ID`): +- `/backups` — хранящиеся снимки: по репозиторию число и размер, 10 последних с временем, + игроками и партиями, 📌 у именованных; +- `/status` — то же, что `fs-backup status`; +- `/help` — список команд. + +**Настройка:** +1. В Telegram найдите бота приложения и напишите ему `/start`. +2. Узнайте id своего чата: + ```bash + docker compose exec backup fs-backup telegram chats + # 123456789 @you /start + ``` + Когда бот уже работает (id вписан), он сам забирает сообщения. Тогда id нового чата + ищите в журнале: `docker compose logs backup | grep "чужого чата"`. +3. Впишите id в `.env` — `BACKUP_TELEGRAM_CHAT_ID=123456789`, несколько через запятую — + и пересоздайте контейнер: `docker compose up -d backup`. +4. Проверьте связь: `docker compose exec backup fs-backup telegram test` — в чат придёт + пробное сообщение. + +Токен бота по умолчанию берётся из `TELEGRAM_BOT_TOKEN` приложения. Чтобы слать отчёты от +другого бота, задайте `BACKUP_TELEGRAM_BOT_TOKEN`. Если Telegram недоступен, бэкапы работают +как обычно — в журнале будет только строка «Telegram: … не отправлено». + > **Никогда не выполняйте на проде `docker compose down -v`.** Флаг `-v` удаляет тома — > данные приложения **и** локальную копию бэкапов. Обычный `docker compose down` данные > не трогает. @@ -852,6 +888,8 @@ docker compose logs --tail 100 backup | Симптом | Причина | Что сделать | |---|---|---| | `BACKUP_PASSWORD не задан в .env — бэкапы ОТКЛЮЧЕНЫ` | нет пароля в `.env` | добавить `BACKUP_PASSWORD` (шаг 5), затем `docker compose up -d backup` | +| отчёты в Telegram не приходят, `telegram test` пишет «отправить не удалось» | неверный chat id или токен, боту не писали `/start` | раздел 10, «Отчёты в Telegram»: написать боту, взять id из журнала, `docker compose up -d backup` | +| бот не отвечает на `/backups`, в журнале «сообщение из чужого чата» | ваш id не в `BACKUP_TELEGRAM_CHAT_ID` | вписать id из этой строки журнала, `docker compose up -d backup` | | `неверный BACKUP_PASSWORD для репозитория …` | пароль в `.env` не тот, с которым создан репозиторий | вернуть правильный пароль из менеджера паролей; `docker compose up -d backup` | | `BACKUP_SSH_KEY_B64 не декодируется из base64` / `— не приватный SSH-ключ` | строка ключа обрезана, с пробелами или от `.pub` | заново скопировать base64 **приватного** ключа (шаг 5, пункт 5), одной строкой | | `Репозиторий vps недоступен` и выше `Permission denied (publickey)` | на VPS нет публичного ключа или ключ другой | шаг 3, пункты 4 и 9: проверить `authorized_keys` и вход `sftp` с ПК этим ключом | @@ -920,6 +958,9 @@ docker volume rm <имя тома> | `recover` | разбор прерванного восстановления | | `export [--repo …]` | снимок в tar в stdout: `docker compose exec -T backup fs-backup export latest > fs.tar` | | `restic <аргументы>` | любая команда restic с настройками контейнера, например `restic vps stats` | +| `telegram chats` | id чатов, писавших боту, — для `BACKUP_TELEGRAM_CHAT_ID` | +| `telegram test` | пробное сообщение в чаты `BACKUP_TELEGRAM_CHAT_ID` | +| `telegram bot` | бот-слушатель команд `/backups`, `/status` (контейнер запускает его сам) | | `help` | краткая справка | | `init`, `info`, `health`, `has-snapshots` | служебные: создать репозитории, данные снимка для скриптов ПК, healthcheck, проверка «есть ли снимки» при старте | @@ -955,6 +996,8 @@ docker volume rm <имя тома> | `BACKUP_VPS_PORT` | Pi | `22` | SSH-порт VPS | | `BACKUP_VPS_DIR` | Pi | `/srv/fs-backups/restic` | папка репозитория на VPS | | `BACKUP_SSH_KEY_B64` | Pi | — | приватный ключ для VPS, base64 | +| `BACKUP_TELEGRAM_CHAT_ID` | Pi | — | id чатов для отчётов и команд, через запятую; пусто = без Telegram | +| `BACKUP_TELEGRAM_BOT_TOKEN` | Pi | `TELEGRAM_BOT_TOKEN` | токен бота, если отчёты должен слать другой бот | | `BACKUP_MEM_LIMIT` | Pi | `384m` | лимит памяти контейнера | | `BACKUP_PI_SSH` | ПК | — | как зайти на Pi: `pi@` | | `BACKUP_PI_DIR` | ПК | `~/forbidden-stars` | папка прода на Pi | diff --git a/deploy/backup/entrypoint.sh b/deploy/backup/entrypoint.sh index e1028ca..6ca3ae4 100644 --- a/deploy/backup/entrypoint.sh +++ b/deploy/backup/entrypoint.sh @@ -5,6 +5,14 @@ set -eu log() { printf '[backup %s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; } +# Telegram-бот (#83): отвечает владельцу на /backups и /status. Стартует раньше проверки +# пароля — при выключенных бэкапах скажет, что они выключены. Упал — перезапуск через минуту. +if [ -n "${BACKUP_TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${BACKUP_TELEGRAM_CHAT_ID:-}" ]; then + ( while :; do fs-backup telegram bot || true; sleep 60; done ) & +else + log "Telegram не настроен (BACKUP_TELEGRAM_CHAT_ID/токен бота) — отчёты в Telegram выключены." +fi + # Без пароля бэкапы невозможны. Не падаем (иначе restart-петля и спам в логах) — ждём, # пока пароль появится в .env; healthcheck при этом показывает unhealthy. if [ -z "${BACKUP_PASSWORD:-}" ]; then diff --git a/deploy/backup/fs-backup.sh b/deploy/backup/fs-backup.sh index 0757614..194fa86 100644 --- a/deploy/backup/fs-backup.sh +++ b/deploy/backup/fs-backup.sh @@ -34,11 +34,15 @@ APP_HOST="${BACKUP_APP_HOST:-app}" export RESTIC_PASSWORD="${BACKUP_PASSWORD:-}" export RESTIC_COMPRESSION="${BACKUP_COMPRESSION:-max}" export RESTIC_CACHE_DIR="${RESTIC_CACHE_DIR:-/backup/cache}" +TG_TOKEN="${BACKUP_TELEGRAM_BOT_TOKEN:-}" +TG_CHATS="${BACKUP_TELEGRAM_CHAT_ID:-}" +TG_API="${BACKUP_TELEGRAM_API:-https://api.telegram.org}" +LAST_ERROR="" # текст последнего die — причина сбоя в отчёте Telegram # ─── Общие функции ──────────────────────────────────────────────────────────── # Весь служебный вывод — в stderr: stdout у export занят tar-потоком. log() { printf '[backup %s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2; } -die() { log "ОШИБКА: $*"; exit 1; } +die() { LAST_ERROR="$*"; log "ОШИБКА: $*"; exit 1; } require_password() { [ -n "$RESTIC_PASSWORD" ] || die "BACKUP_PASSWORD не задан в .env (см. deploy/backup/README.md, шаг 1)." @@ -118,6 +122,99 @@ human() { # байты → «12.3 MB» fmt_epoch() { date -d "@$1" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "$1"; } +# ─── Telegram (#83) ─────────────────────────────────────────────────────────── +# Отчёты о бэкапах и проверках — в чаты BACKUP_TELEGRAM_CHAT_ID (через запятую) от бота +# BACKUP_TELEGRAM_BOT_TOKEN. Не настроено — молчим. Telegram недоступен — бэкап от этого не +# страдает: неудача отправки только пишется в лог. Токен не попадает ни в лог, ни в текст. +tg_enabled() { [ -n "$TG_TOKEN" ] && [ -n "$TG_CHATS" ]; } + +tg_escape() { sed -e 's/&/\&/g' -e 's//\>/g'; } + +tg_chat_ids() { printf '%s' "$TG_CHATS" | tr ',;' ' '; } + +tg_api() { # tg_api <метод> [аргументы curl…] → JSON ответа + _method="$1" + shift + curl -fsS --max-time 70 "$@" "$TG_API/bot$TG_TOKEN/$_method" 2>/dev/null +} + +tg_post() { # tg_post — одно сообщение в один чат + tg_api sendMessage -o /dev/null \ + --data-urlencode "chat_id=$1" \ + --data-urlencode "text=$2" \ + --data-urlencode "parse_mode=HTML" \ + --data-urlencode "disable_web_page_preview=true" +} + +tg_send() { # tg_send — во все чаты владельца + tg_enabled || return 0 + for _chat in $(tg_chat_ids); do + tg_post "$_chat" "$1" || log "Telegram: сообщение в чат $_chat не отправлено." + done +} + +repo_summary() { # repo_summary → «снимков 30 · 45.2 MB» + if _st="$(r "$1" stats --mode raw-data --json 2>/dev/null)"; then + printf '%s' "$_st" | jq -r '"\(.snapshots_count)\t\(.total_size)"' | { + IFS="$(printf '\t')" read -r _cnt _size + echo "снимков $_cnt · $(human "$_size")" + } + else + echo "размер недоступен" + fi +} + +# Итог run/verify одним сообщением. Списки — «репо:поле:поле» через пробел; их собирают +# cmd_run/cmd_verify, а вызывает обработчик EXIT — так отчёт уходит и при раннем отказе. +notify_run() { # notify_run <код выхода> + tg_enabled || return 0 + _when="$(date '+%d.%m %H:%M')" + _db="БД: игроков $_players, партий $_matches" + if [ "$1" -eq 0 ]; then + _text="✅ Бэкап $_when +$_db" + else + _text="❌ Бэкап не удался $_when +$(printf '%s' "${LAST_ERROR:-прервался с кодом $1}" | tg_escape)" + [ "$_players" = "?" ] || _text="$_text +$_db" + fi + for _item in $_ok_repos; do + _text="$_text +• ${_item%%:*}: снимок ${_item#*:} · $(repo_summary "${_item%%:*}")" + done + for _repo in $_failed; do + _text="$_text +• $_repo: ошибка" + done + [ "$1" -eq 0 ] || _text="$_text +Подробности: docker compose logs backup" + tg_send "$_text" +} + +notify_verify() { # notify_verify <код выхода> + tg_enabled || return 0 + _when="$(date '+%d.%m %H:%M')" + if [ "$1" -eq 0 ]; then + _text="🔍 Проверка данных: OK $_when" + else + _text="❌ Проверка данных не прошла $_when +$(printf '%s' "${LAST_ERROR:-прервалась с кодом $1}" | tg_escape)" + fi + for _item in $_verified; do + _rest="${_item#*:}" + _text="$_text +• ${_item%%:*}: данные целы, в последнем снимке игроков ${_rest%%:*}, партий ${_rest#*:}" + done + for _repo in $_failed; do + _text="$_text +• $_repo: ошибка" + done + [ "$1" -eq 0 ] || _text="$_text +Подробности: docker compose logs backup" + tg_send "$_text" +} + # Проверка SQLite без записи рядом с файлом (immutable: ни -wal, ни -shm не создаются). db_ok() { [ -s "$1" ] || return 1 @@ -236,11 +333,18 @@ cmd_run() { esac shift done + _players="?" + _matches="?" + _ok_repos="" + _failed="" + # Отчёт в Telegram уходит при любом исходе, в том числе при раннем отказе (занят lock, + # нет пароля). Копию БД убираем только после take_lock: до него она может быть чужой. + trap '_rc=$?; notify_run "$_rc"' EXIT require_password setup_ssh take_lock warn_leftovers - trap 'rm -f "$SNAP_ROOT/$DB_NAME"' EXIT + trap '_rc=$?; rm -f "$SNAP_ROOT/$DB_NAME"; notify_run "$_rc"' EXIT log "Снимок данных: консистентная копия БД…" stage_db @@ -252,7 +356,6 @@ cmd_run() { log "БД в порядке: игроков $_players, партий $_matches." if [ "$_allow_empty" = no ]; then guard_empty "$_players" "$_matches"; fi - _failed="" for _repo in $(repos); do log "=== Репозиторий $_repo ($(repo_url "$_repo")) ===" if backup_to "$_repo" --tag "$_kind" --tag "players:$_players" --tag "matches:$_matches" ${_tag:+--tag keep --tag "$_tag"} \ @@ -260,6 +363,7 @@ cmd_run() { && r "$_repo" check >&2; then _sid="$(latest_short_id "$_repo")" mark "run-$_repo" ok "снимок $_sid" + _ok_repos="$_ok_repos $_repo:$_sid" log "OK: репозиторий $_repo, снимок $_sid." else mark "run-$_repo" err "бэкап/очистка/проверка не удались — см. docker compose logs backup" @@ -365,10 +469,12 @@ cmd_has_snapshots() { # ─── verify ─────────────────────────────────────────────────────────────────── cmd_verify() { + _verified="" + _failed="" + trap '_rc=$?; notify_verify "$_rc"' EXIT require_password setup_ssh take_lock - _failed="" for _repo in $(repos); do log "=== Проверка репозитория $_repo: структура + $VERIFY_SUBSET данных ===" _tmp="$RUNTIME_DIR/verify.db" @@ -378,6 +484,7 @@ cmd_verify() { && db_ok "$_tmp"; then _counts="$(db_counts "$_tmp")" mark "verify-$_repo" ok "данные целы; последний снимок: игроков ${_counts%%|*}, партий ${_counts##*|}" + _verified="$_verified $_repo:${_counts%%|*}:${_counts##*|}" log "OK: $_repo — данные целы, БД последнего снимка открывается (игроков ${_counts%%|*}, партий ${_counts##*|})." else mark "verify-$_repo" err "проверка не прошла — см. docker compose logs backup" @@ -779,6 +886,121 @@ cmd_init() { for _repo in $(repos); do ensure_repo "$_repo" && log "Репозиторий $_repo готов."; done } +# ─── telegram ───────────────────────────────────────────────────────────────── +tg_help_text() { + cat <<'EOF' +Бэкапы Forbidden Stars +/backups — хранящиеся снимки по репозиториям +/status — последние бэкапы и проверки, размеры +Отчёт о каждом бэкапе и проверке приходит сюда сам. +EOF +} + +tg_backups_text() { # хранящиеся снимки: по репозиторию итог и 10 последних + [ -n "$RESTIC_PASSWORD" ] || { echo "Бэкапы отключены: BACKUP_PASSWORD не задан."; return 0; } + setup_ssh + for _repo in $(repos); do + echo "$_repo · $(repo_summary "$_repo")" + if _json="$(r "$_repo" snapshots --json 2>/dev/null)"; then + printf '%s' "$_json" | jq -r ' + def tagval($p): ([.tags[]? | select(startswith($p)) | ltrimstr($p)] | first) // "?"; + def names: [.tags[]? | select(test("^(players:|matches:|scheduled$|manual$|keep$)") | not)]; + sort_by(.time) | .[-10:] | reverse | .[] | + "\(.time[5:16] | sub("T"; " ")) игроков \(tagval("players:")), партий \(tagval("matches:"))" + + (if (names | length) > 0 then " · 📌 " + (names | join(", ")) else "" end)' + else + echo "репозиторий недоступен или занят — повторите позже" + fi + echo + done +} + +tg_allowed() { # tg_allowed — чат из белого списка владельца + for _allowed in $(tg_chat_ids); do [ "$_allowed" = "$1" ] && return 0; done + return 1 +} + +tg_reply() { # tg_reply <текст команды> + # Каждая выборка — в подоболочке $(…): die внутри не роняет цикл бота. + case "$2" in + /backups*|/list*) _reply="$(tg_backups_text 2>/dev/null)" ;; + /status*) _reply="
$(cmd_status 2>&1 | head -n 60 | tg_escape)
" ;; + *) _reply="$(tg_help_text)" ;; + esac + [ -n "$_reply" ] || _reply="Не удалось получить данные — см. docker compose logs backup." + tg_post "$1" "$_reply" || log "Telegram: ответ в чат $1 не отправлен." +} + +# Бот: long polling getUpdates. Отвечает только чатам из BACKUP_TELEGRAM_CHAT_ID; про чужие +# пишет chat id в лог — так владелец узнаёт свой при настройке. Lock не берёт: чтение +# restic совместимо с идущим бэкапом. +cmd_telegram_bot() { + tg_enabled || die "Telegram не настроен: нужны BACKUP_TELEGRAM_CHAT_ID и токен бота." + _offset_file="$STATE_DIR/telegram.offset" + _updates="$RUNTIME_DIR/telegram-updates" + log "Telegram-бот: слушаю команды /backups, /status, /help." + while :; do + _offset="$(cat "$_offset_file" 2>/dev/null || echo 0)" + if ! _resp="$(tg_api getUpdates --data-urlencode "offset=$_offset" \ + --data-urlencode "timeout=50" --data-urlencode 'allowed_updates=["message"]')"; then + sleep 30 + continue + fi + # Разделитель — \037 (не пробельный): пустые поля (нет username/текста) не схлопываются. + printf '%s' "$_resp" | jq -r '.result[]? | + [.update_id, (.message.chat.id // ""), (.message.from.username // ""), + ((.message.text // "") | gsub("[\n]"; " "))] | map(tostring) | join("")' \ + > "$_updates" 2>/dev/null || { sleep 30; continue; } + while IFS="$(printf '\037')" read -r _uid _chat _from _text; do + echo $((_uid + 1)) > "$_offset_file" + [ -n "$_chat" ] || continue + if tg_allowed "$_chat"; then + tg_reply "$_chat" "$_text" + else + log "Telegram: сообщение из чужого чата $_chat (@${_from:-?}) — без ответа. Если это вы, впишите $_chat в BACKUP_TELEGRAM_CHAT_ID." + fi + done < "$_updates" + done +} + +# Кто писал боту: id чатов для BACKUP_TELEGRAM_CHAT_ID. Нужен только токен; апдейты не +# подтверждаются (без offset), так что ничего не теряется. Пока бот-слушатель запущен, он +# забирает сообщения сам — тогда id смотрите в журнале («сообщение из чужого чата …»). +cmd_telegram_chats() { + [ -n "$TG_TOKEN" ] || die "нет токена бота: задайте TELEGRAM_BOT_TOKEN или BACKUP_TELEGRAM_BOT_TOKEN." + _resp="$(tg_api getUpdates --data-urlencode "timeout=0")" \ + || die "Telegram не ответил (неверный токен или нет сети)." + _chats="$(printf '%s' "$_resp" | jq -r '[.result[]? | .message | select(. != null) + | "\(.chat.id)\t@\(.from.username // "?")\t\(.text // "")"] | unique | .[]')" + if [ -z "$_chats" ]; then + echo "Сообщений боту нет: напишите ему /start в Telegram и повторите." + return 0 + fi + echo "Чаты, писавшие боту (id — в BACKUP_TELEGRAM_CHAT_ID):" + printf '%s\n' "$_chats" +} + +cmd_telegram() { + case "${1:-}" in + bot) cmd_telegram_bot ;; + chats) cmd_telegram_chats ;; + test) + tg_enabled || die "Telegram не настроен: нужны BACKUP_TELEGRAM_CHAT_ID и токен бота (BACKUP_TELEGRAM_BOT_TOKEN или TELEGRAM_BOT_TOKEN)." + _sent=0 + for _chat in $(tg_chat_ids); do + if tg_post "$_chat" "🔔 Проверка связи: отчёты о бэкапах Forbidden Stars будут приходить сюда."; then + _sent=$((_sent + 1)) + log "Telegram: тестовое сообщение в чат $_chat отправлено." + else + log "Telegram: в чат $_chat отправить не удалось (неверный токен/chat id или боту не писали /start)." + fi + done + [ "$_sent" -gt 0 ] || die "ни одно тестовое сообщение не дошло." + ;; + *) die "telegram: укажите chats, test или bot" ;; + esac +} + usage() { cat <<'EOF' fs-backup — бэкапы Forbidden Stars (restic). Запуск на Pi из папки с docker-compose.yml: @@ -806,6 +1028,11 @@ fs-backup — бэкапы Forbidden Stars (restic). Запуск на Pi из export [--repo local|vps] > fs.tar снимок в tar без сжатия (нужен exec -T) +Telegram (отчёты о каждом бэкапе и проверке, команды /backups и /status): + telegram chats id чатов, писавших боту (для BACKUP_TELEGRAM_CHAT_ID) + telegram test пробное сообщение в чаты BACKUP_TELEGRAM_CHAT_ID + telegram bot бот-слушатель команд (контейнер запускает его сам) + Прочее: init создать репозитории (делается автоматически) restic <аргументы> произвольная команда restic с настройками контейнера @@ -833,6 +1060,7 @@ case "$_cmd" in restic) cmd_restic "$@" ;; health) cmd_health "$@" ;; has-snapshots) cmd_has_snapshots "$@" ;; + telegram) cmd_telegram "$@" ;; help|-h|--help) usage ;; *) usage >&2; exit 2 ;; esac diff --git a/docker-compose.yml b/docker-compose.yml index bda1dd3..7d5916b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,8 +75,8 @@ services: # Бэкапы (restic): снимки по расписанию в локальный репозиторий (том backup-data) и на VPS # (SFTP, если задан BACKUP_VPS_HOST). От app не зависит и app не мешает. Переменные — - # только BACKUP_* (секреты приложения сюда не передаются). Всё про настройку и - # восстановление — deploy/backup/README.md. + # только BACKUP_* (секреты приложения сюда не передаются; исключение — токен бота для + # отчётов в Telegram). Всё про настройку и восстановление — deploy/backup/README.md. backup: build: ./deploy/backup image: ${IMAGE_REGISTRY:-gitea.arseniev.info/notbigghost}/forbidden-stars-backup:${IMAGE_TAG:-latest} @@ -97,6 +97,10 @@ services: BACKUP_VPS_PORT: ${BACKUP_VPS_PORT:-22} BACKUP_VPS_DIR: ${BACKUP_VPS_DIR:-/srv/fs-backups/restic} BACKUP_SSH_KEY_B64: ${BACKUP_SSH_KEY_B64:-} + # Отчёты и бот в Telegram (#83). Единственное исключение из «секреты приложения сюда + # не передаются»: по умолчанию — токен бота приложения (тот же ForbidenStarsBot). + BACKUP_TELEGRAM_BOT_TOKEN: ${BACKUP_TELEGRAM_BOT_TOKEN:-${TELEGRAM_BOT_TOKEN:-}} + BACKUP_TELEGRAM_CHAT_ID: ${BACKUP_TELEGRAM_CHAT_ID:-} TZ: ${BACKUP_TZ:-Europe/Moscow} volumes: - db-data:/fs-db # живая БД (снимается консистентно)