bober-bbq-bot/docs/DEPLOYMENT.md
byrsapty 375022e739 Add backup_remote.py and wire up the rest of the rclone rework
Continuation of the previous commit (gdrive_backup.py removal landed
separately by accident) — adds the new module itself, the admin
routes/template using it, generalized Setting keys, updated docs/
provisioning notes, and tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 22:57:59 +03:00

22 KiB
Raw Permalink Blame History

Розгортання на VPS

Мінімальна робоча схема: один VPS (Ubuntu), Nginx як reverse-proxy й термінатор TLS, два systemd-сервіси (backend + bot), SQLite (або Postgres, якщо очікується більше навантаження).

Цей документ описує розгортання нового, окремого інстансу (напр. для іншого клієнта на тому самому коді). Він не стосується і не повинен застосовуватись до вже працюючого продакшн-сервера Bober BBQ.

Далі $INSTALL_DIR — шлях, куди клонується репозиторій (напр. /opt/<client-slug>-bot); підставляйте свій на кожному кроці.

Кроки 2-5 (клон, .env, nginx, TLS, systemd) автоматизовані — ./provision_client.sh --slug vegcafe --domain vegcafe.example --bot-token ··· --cafe-name "Веге Кафе" (--help для повного списку прапорців). Питає підтвердження перед кожним незворотним кроком і ніколи не чіпає директорію, яка вже існує. Розділи нижче лишаються довідкою — що саме робить скрипт і що робити, якщо якийсь крок треба виконати вручну.

1. Підготовка сервера

sudo apt update && sudo apt install -y python3.11 python3.11-venv nginx certbot python3-certbot-nginx nodejs npm fonts-dejavu-core rclone

fonts-dejavu-core — потрібен лише для PDF-відомостей постачальників (кирилична TTF-шрифтова пара для fpdf2, bober_bbq/utils/supplier_reports.py). Excel-відомості й решта застосунку без нього працюють нормально.

rclone — потрібен лише для автоматичних резервних копій у хмару (bober_bbq/utils/backup_remote.py, налаштовується в Адмінка → Резервні копії). Локальні бекапи/відновлення на цій сторінці працюють і без нього.

2. Код і залежності

git clone <репозиторій> $INSTALL_DIR
cd $INSTALL_DIR
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

cd webapp && npm install && npm run build && cd ..

3. .env

Скопіюйте .env.example.env, заповніть:

  • BOT_TOKEN — від @BotFather
  • WEBAPP_URL=https://<ваш-домен>/webapp/
  • BACKEND_PUBLIC_URL=https://<ваш-домен>
  • MONOBANK_WEBHOOK_URL=https://<ваш-домен>/api/payments/monobank/webhook
  • MONOBANK_TOKEN — токен еквайрингу від замовника (розділ 22.8 ТЗ)
  • DATABASE_URL — для Postgres: postgresql+psycopg2://user:pass@localhost/bober_bbq
  • SECRET_KEY, FLASK_ADMIN_USERNAME, FLASK_ADMIN_PASSWORD — змінити на бойові значення
  • SERVICE_WEB, SERVICE_BOT, LOG_FILE_WEB, LOG_FILE_BOT — лише якщо systemd-юніти цього інстансу названі не bober-bbq-web/bober-bbq-bot (напр. другий клієнт на тій самій VPS). Не задавайте їх, якщо юніти саме так і називаються — значення за замовчуванням підійдуть.

4. Nginx + TLS

server {
    listen 80;
    server_name your-domain.example;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
sudo certbot --nginx -d your-domain.example

Telegram Mini App і monobank webhook вимагають дійсний HTTPS-сертифікат — без нього WebApp-кнопка не відкриється, а вебхук оплати не спрацює.

5. systemd-юніти

Оберіть імена юнітів для цього клієнта (типово bober-bbq-web/ bober-bbq-bot, якщо це єдиний інстанс на сервері; інакше — щось унікальне, напр. <client-slug>-web/<client-slug>-bot, і не забудьте задати ці ж імена в .env через SERVICE_WEB/SERVICE_BOT, інакше deploy.sh перезапускатиме не ті юніти).

/etc/systemd/system/<SERVICE_WEB>.service:

[Unit]
Description=<Cafe Name> backend (API + admin)
After=network.target

[Service]
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/.venv/bin/python run_web.py
Restart=on-failure
EnvironmentFile=$INSTALL_DIR/.env

[Install]
WantedBy=multi-user.target

/etc/systemd/system/<SERVICE_BOT>.service:

[Unit]
Description=<Cafe Name> Telegram bot
After=network.target <SERVICE_WEB>.service

[Service]
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/.venv/bin/python run_bot.py
Restart=on-failure
EnvironmentFile=$INSTALL_DIR/.env

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now <SERVICE_WEB> <SERVICE_BOT>

Якщо обрали нестандартні імена, також вкажіть логи цього інстансу в StandardOutput=append:/var/log/<SERVICE_WEB>.log / StandardError=append:/var/log/<SERVICE_WEB>.log (аналогічно для бота) і задайте ті самі шляхи в .env через LOG_FILE_WEB/LOG_FILE_BOT — інакше перегляд логів в Адмінці → Система буде показувати не той файл.

6. Telegram-налаштування

  1. У @BotFather: /setmenubutton → вкажіть WEBAPP_URL як кнопку меню (додатково до reply-кнопки «🍽 Меню» в самому боті).
  2. Додайте бота адміністратором у групи/канали для замовлень «Доставка» і «Самовивіз», дізнайтесь їхні chat_id (переслати повідомлення з чату боту @userinfobot) і впишіть в Адмінка → Налаштування.
  3. У my.monobank.ua / кабінеті еквайрингу ФОП переконайтесь, що вебхук MONOBANK_WEBHOOK_URL доступний ззовні (перевірте curl -X POST https://your-domain.example/api/payments/monobank/webhook).

7. Резервне копіювання

Основний спосіб — вбудована система в самій адмінці (Адмінка → Резервні копії): періодично (не cron, а внутрішній APScheduler-job в run_web.py, щогодини перевіряє, чи настав час) знімає копію SQLite-бази і static/ uploads/ (фото товарів), заливає обране сховище через rclone і ротує старі копії — усе налаштовується прямо у формі (Google Drive або S3-сумісне сховище: AWS S3 / Backblaze B2 / Wasabi / MinIO / інше), без жодного rclone.conf на диску сервера. Дивись bober_bbq/utils/backup_remote.py. Вимагає встановленого rclone (крок 1 вище).

Резервний варіант без хмари — просте копіювання файлу за розкладом (cron):

0 3 * * * cp $INSTALL_DIR/instance/bober_bbq.db /opt/backups/bober_bbq-$(date +\%F).db

Postgres: стандартний pg_dump за розкладом (вбудована система в адмінці поки що підтримує лише SQLite — див. TODO нижче). Не забудьте також бекапити bober_bbq/static/uploads/ (фото товарів) — БД зберігає лише посилання на файли.

8. Оновлення коду

deploy.sh — закомічений у репозиторії корінь ($INSTALL_DIR/deploy.sh):

$INSTALL_DIR/deploy.sh

Робить послідовно: git pullpip install -r requirements.txtnpm install && npm run build у webapp/systemctl restart тих юнітів, що вказані в .env через SERVICE_WEB/SERVICE_BOT (або bober-bbq-web/bober-bbq-bot, якщо не вказано). Той самий скрипт, без змін, коректно працює для будь-якого клієнта на цьому коді — імена юнітів він бере з .env цього конкретного інстансу.

Якщо розгортаєте вручну на новому сервері — ті самі кроки:

cd $INSTALL_DIR
git pull
.venv/bin/pip install -r requirements.txt
cd webapp && npm install && npm run build && cd ..
sudo systemctl restart <SERVICE_WEB> <SERVICE_BOT>

9. Чекліст ребрендингу для нового клієнта

Після першого запуску (перед тим, як показувати клієнту):

  1. Адмінка → Налаштування: cafe_name, телефон, адреса, графік роботи, logo_url, кольори теми (адмінки й вебапки) — усе інше вже підхопить ці значення (заголовки листування з Telegram-ботом, назва в API, титул сторінки адмінки тощо).
  2. webapp/.env.local (створити, НЕ редагувати закомічений webapp/.env): VITE_CAFE_NAME=<Назва Кафе> — впливає на <title> вебапки та PWA manifest (name/short_name); підхопиться при наступному npm run build.
  3. Замінити 4 файли іконок під webapp/public/icons/icon-{32,180,192,512}.png на артворк клієнта (поточні — це бренд-арт Bober BBQ, бобер з шампуром; автоматично не генеруються).
  4. Адмінка → Меню: видалити демо-меню (Шашлик/BBQ категорії й товари, що приходять із seed.py при першому запуску порожньої БД) і наповнити реальним асортиментом клієнта.

10. Використання PostgreSQL замість SQLite

За замовчуванням проєкт працює на SQLite (instance/bober_bbq.db) — для одного невеликого кафе цього досить: файл, який не треба адмініструвати окремо, і бекап якого — просто cp. Це лишається дефолтом і для нових клієнтів; Postgres — свідомий опт-ін для конкретного випадку, не апгрейд "за замовчуванням".

Коли варто переходити на Postgres: не через обсяг даних (навіть кілька років замовлень одного кафе — це нічого для будь-якої з двох СУБД), а через одночасний запис із кількох джерел. SQLite дозволяє лише одному писачу за раз (інші запити на запис чекають або отримують database is locked); для одного кафе з одним-двома адмінами й ботом це непомітно, але стає проблемою, якщо в клієнта:

  • кілька адміністраторів одночасно активно редагують меню/склад/замовлення (не просто переглядають — саме одночасні записи);
  • дуже високий потік замовлень (кілька точок продажу на одному бекенді, інтеграція з зовнішньою системою, що постійно пише в БД).

Як підняти Postgres. Найпростіше — окремий контейнер поруч із systemd-юнітами застосунку (не потребує apt install postgresql* і окремого адміністрування пакета в ОС):

sudo apt install -y docker.io docker-compose-plugin

docker-compose.postgres.yml (покласти поруч із $INSTALL_DIR, не в git):

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: bober_bbq
      POSTGRES_USER: bober_bbq
      POSTGRES_PASSWORD: <згенерований пароль>
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5432:5432"  # тільки localhost — бекенд і Postgres на одній машині
volumes:
  pgdata:
docker compose -f docker-compose.postgres.yml up -d

Рівноцінна альтернатива без Docker — пакет дистрибутива:

sudo apt install -y postgresql
sudo -u postgres createuser bober_bbq --pwprompt
sudo -u postgres createdb bober_bbq --owner=bober_bbq

DATABASE_URL (у .env, замінює дефолтний SQLite-шлях — bober_bbq/config.py читає цю змінну напряму):

DATABASE_URL=postgresql+psycopg2://bober_bbq:<пароль>@localhost/bober_bbq

psycopg2-binary (Postgres-драйвер для SQLAlchemy) уже в requirements.txt — окремо встановлювати не треба, pip install -r requirements.txt (крок 2 / deploy.sh) підхопить його завжди, незалежно від того, яку БД зрештою обрали.

Перенесення даних наявного клієнта з SQLite. Тільки для вже працюючого клієнта, який переїжджає з SQLite на Postgres — новому клієнту досить одразу вказати Postgres-DATABASE_URL перед першим запуском (seed.py наповнить порожню Postgres-БД так само, як наповнив би SQLite). Разова ручна операція, scripts/migrate_sqlite_to_postgres.py:

sudo systemctl stop <SERVICE_WEB> <SERVICE_BOT>   # зупинити запис у SQLite

.venv/bin/python scripts/migrate_sqlite_to_postgres.py \
  --sqlite-path instance/bober_bbq.db \
  --postgres-url postgresql+psycopg2://bober_bbq:<пароль>@localhost/bober_bbq

# після успішного переносу — прописати DATABASE_URL в .env (вище) і:
sudo systemctl start <SERVICE_WEB> <SERVICE_BOT>

Скрипт сам створює схему в порожній Postgres-БД і копіює всі таблиці в безпечному щодо зовнішніх ключів порядку, використовуючи ORM-моделі застосунку (а не сирий SQL) — деталі, застереження щодо повторного запуску й обов'язковий крок скидання Postgres-послідовностей автоінкременту описані в докстрінгу самого файлу (--help). Не запускається автоматично й ніяк не задіяний у run_web.py — лише вручну, один раз, при фактичному переїзді конкретного клієнта.

⚠️ TODO перед реальним переходом: автоматичні бекапи не підтримують Postgres

Уся наявна інфраструктура резервного копіювання (планові бекапи через rclone + кнопки «Backup зараз» / «Відновити» в адмінці) жорстко зав'язана на SQLite і мовчки (або з явним попередженням) вимикається, якщо DATABASE_URL вказує на Postgres. Це не гіпотетична проблема — обидва місця вже сьогодні перевіряють тип БД і відмовляються працювати:

  • bober_bbq/utils/backup_remote.py, create_local_backup() — перевіряє config.SQLALCHEMY_DATABASE_URI.startswith("sqlite:///") і, якщо ні, просто повертає None — плановий бекап (run_scheduled_backup, раз на годину через APScheduler у run_web.py) тихо нічого не робить. Власник кафе не отримає жодного попередження про те, що бекапи перестали створюватися.
  • bober_bbq/admin/system.py, _create_backup() / backups_create() / backups_restore() — та сама перевірка через _sqlite_path(); якщо БД не SQLite, кнопки в адмінці явно показують флеш-повідомлення «Резервне копіювання зараз підтримується лише для SQLite» / «Відновлення підтримується лише для SQLite» замість реальної дії.
  • system_status() / backups_list() (той самий файл) — розмір бази й підпис "SQLite" рахуються через sqlite_path.stat().st_size; для Postgres це поле просто не заповниться (розмір там треба брати запитом pg_database_size() до самої БД, а не з файлової системи).

Що конкретно треба зробити, коли дійде до реального переходу:

  1. У backup_remote.py додати гілку для Postgres поруч із наявною SQLite-гілкою в create_local_backup(): викликати pg_dump у форматі custom (pg_dump -Fc, а не звичайний SQL-дамп — компактніше і відновлюється через pg_restore, без ручного psql < file.sql). Результат — файл на диску, той самий контракт, що й зараз (Path | None), щоб решта пайплайна (завантаження через rclone, rotate_backups()) запрацювала без змін.
  2. Додати перевірку цілісності дампу перед завантаженням — Postgres-аналог наявного PRAGMA integrity_check для SQLite. Найпростіший варіант: pg_restore --list <файл> на щойно створеному дампі — команда завершується з помилкою, якщо файл пошкоджений/обрізаний, не торкаючись жодної реальної бази.
  3. У bober_bbq/admin/system.py — та сама заміна shutil.copy2() на pg_dump/pg_restore у _create_backup()/backups_create()/ backups_restore(), і прибрати обидва флеш-повідомлення-заглушки.
  4. Там же — розмір бази для system_status()/backups_list() рахувати через SQL-запит SELECT pg_database_size(current_database()), коли БД не SQLite, замість stat().st_size на неіснуючому файлі.
  5. Інфраструктурна деталь, яку легко забути: pg_dump/pg_restore мають бути фізично доступні звідти, звідки їх викликають. Якщо Postgres піднято в Docker (варіант за замовчуванням вище в цьому розділі) — бекенд-процес на хості не має цих утиліт напряму; потрібен або docker exec <контейнер> pg_dump ..., або окремо встановлений пакет postgresql-client на хості (сумісної з сервером мажорної версії). Це також треба буде дописати в цей розділ документації, коли дійде до реалізації.
  6. Протестувати неможливо без живого Postgres-сервера — так само, як і сам скрипт міграції (scripts/migrate_sqlite_to_postgres.py), це можна перевірити лише проти реального інстансу Postgres, не структурним оглядом коду. Перед тим, як покладатися на це для реального клієнта — обов'язково зробити один реальний прогін бекап → знищити тестову БД → відновити → звірити дані.

До того часу: якщо клієнт колись реально перейде на Postgres, у нього не буде жодного робочого автоматичного бекапу, доки хтось вручну не налаштує власний pg_dump за розкладом (cron) поза цим застосунком — і не забути окремо бекапити bober_bbq/static/uploads/ (фото товарів), оскільки БД зберігає лише посилання на файли, а не самі файли.