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>
22 KiB
Розгортання на 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— від @BotFatherWEBAPP_URL=https://<ваш-домен>/webapp/BACKEND_PUBLIC_URL=https://<ваш-домен>MONOBANK_WEBHOOK_URL=https://<ваш-домен>/api/payments/monobank/webhookMONOBANK_TOKEN— токен еквайрингу від замовника (розділ 22.8 ТЗ)DATABASE_URL— для Postgres:postgresql+psycopg2://user:pass@localhost/bober_bbqSECRET_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-налаштування
- У @BotFather:
/setmenubutton→ вкажітьWEBAPP_URLяк кнопку меню (додатково до reply-кнопки «🍽 Меню» в самому боті). - Додайте бота адміністратором у групи/канали для замовлень «Доставка» і
«Самовивіз», дізнайтесь їхні
chat_id(переслати повідомлення з чату боту @userinfobot) і впишіть в Адмінка → Налаштування. - У 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 pull → pip install -r requirements.txt →
npm 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. Чекліст ребрендингу для нового клієнта
Після першого запуску (перед тим, як показувати клієнту):
- Адмінка → Налаштування:
cafe_name, телефон, адреса, графік роботи,logo_url, кольори теми (адмінки й вебапки) — усе інше вже підхопить ці значення (заголовки листування з Telegram-ботом, назва в API, титул сторінки адмінки тощо). webapp/.env.local(створити, НЕ редагувати закоміченийwebapp/.env):VITE_CAFE_NAME=<Назва Кафе>— впливає на<title>вебапки та PWA manifest (name/short_name); підхопиться при наступномуnpm run build.- Замінити 4 файли іконок під
webapp/public/icons/icon-{32,180,192,512}.pngна артворк клієнта (поточні — це бренд-арт Bober BBQ, бобер з шампуром; автоматично не генеруються). - Адмінка → Меню: видалити демо-меню (Шашлик/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()до самої БД, а не з файлової системи).
Що конкретно треба зробити, коли дійде до реального переходу:
- У
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()) запрацювала без змін. - Додати перевірку цілісності дампу перед завантаженням —
Postgres-аналог наявного
PRAGMA integrity_checkдля SQLite. Найпростіший варіант:pg_restore --list <файл>на щойно створеному дампі — команда завершується з помилкою, якщо файл пошкоджений/обрізаний, не торкаючись жодної реальної бази. - У
bober_bbq/admin/system.py— та сама замінаshutil.copy2()наpg_dump/pg_restoreу_create_backup()/backups_create()/backups_restore(), і прибрати обидва флеш-повідомлення-заглушки. - Там же — розмір бази для
system_status()/backups_list()рахувати через SQL-запитSELECT pg_database_size(current_database()), коли БД не SQLite, замістьstat().st_sizeна неіснуючому файлі. - Інфраструктурна деталь, яку легко забути:
pg_dump/pg_restoreмають бути фізично доступні звідти, звідки їх викликають. Якщо Postgres піднято в Docker (варіант за замовчуванням вище в цьому розділі) — бекенд-процес на хості не має цих утиліт напряму; потрібен абоdocker exec <контейнер> pg_dump ..., або окремо встановлений пакетpostgresql-clientна хості (сумісної з сервером мажорної версії). Це також треба буде дописати в цей розділ документації, коли дійде до реалізації. - Протестувати неможливо без живого Postgres-сервера — так само, як
і сам скрипт міграції (
scripts/migrate_sqlite_to_postgres.py), це можна перевірити лише проти реального інстансу Postgres, не структурним оглядом коду. Перед тим, як покладатися на це для реального клієнта — обов'язково зробити один реальний прогін бекап → знищити тестову БД → відновити → звірити дані.
До того часу: якщо клієнт колись реально перейде на Postgres, у нього
не буде жодного робочого автоматичного бекапу, доки хтось вручну не
налаштує власний pg_dump за розкладом (cron) поза цим застосунком —
і не забути окремо бекапити bober_bbq/static/uploads/ (фото товарів),
оскільки БД зберігає лише посилання на файли, а не самі файли.