This commit is contained in:
dv 2026-03-19 17:05:20 +03:00
parent a29c45ee32
commit 6808521be0
7 changed files with 258 additions and 35 deletions

View file

@ -1,6 +1,9 @@
APP_ENV=dev APP_ENV=dev
APP_HOST=0.0.0.0 APP_HOST=0.0.0.0
APP_PORT=8000 APP_PORT=8000
APP_PUBLIC_IP=
APP_PUBLIC_DOMAIN=
APP_BASE_URL=
DB_PATH=data/app.db DB_PATH=data/app.db
SESSION_SECRET=change-me SESSION_SECRET=change-me
@ -22,4 +25,3 @@ EXTERNAL_API_CREATE_URL_WITH_KEY=
OPERATOR_CHAT_IDS= OPERATOR_CHAT_IDS=
REQUEST_CATEGORIES=Общая консультация,Техническая проблема,Оплата,Другое REQUEST_CATEGORIES=Общая консультация,Техническая проблема,Оплата,Другое

View file

@ -6,47 +6,29 @@ steps:
image: python:3.11-slim image: python:3.11-slim
commands: commands:
- pip install --no-cache-dir -r requirements.txt - pip install --no-cache-dir -r requirements.txt
- pytest -q - pytest -q tests -p no:cacheprovider
docker_build_push: deploy_no_docker:
image: woodpeckerci/plugin-docker-buildx
settings:
registry: ${CI_REGISTRY}
repo: ${CI_REGISTRY_IMAGE}
username:
from_secret: registry_user
password:
from_secret: registry_password
dockerfile: Dockerfile
platforms: linux/amd64
tags:
- ${CI_COMMIT_SHA}
- stable
when:
- branch: [main, master]
deploy_pull_on_host:
image: appleboy/drone-ssh image: appleboy/drone-ssh
settings: settings:
host: host:
from_secret: deploy_host from_secret: forgejo_host_ssh
username: username:
from_secret: deploy_user from_secret: forgejo_user_ssh
key: key:
from_secret: deploy_ssh_key from_secret: forgejo_key_ssh
port: port:
from_secret: deploy_port from_secret: forgejo_port_ssh
script: script:
- docker login ${CI_REGISTRY} -u ${REGISTRY_USER} -p ${REGISTRY_PASSWORD} - cd /home/caps/intra_max_bot
- docker pull ${CI_REGISTRY_IMAGE}:stable - git pull --ff-only
- docker rm -f max-support-bot || true - if [ ! -d .venv ]; then python3 -m venv .venv; fi
- docker run -d --name max-support-bot --restart unless-stopped -p 8000:8000 --env-file /opt/max-support/.env -v /opt/max-support/data:/app/data ${CI_REGISTRY_IMAGE}:stable - . .venv/bin/activate
- curl -fsS http://127.0.0.1:8000/health - pip install --no-cache-dir -r requirements.txt
environment: - sudo systemctl restart max-support
REGISTRY_USER: - set -a
from_secret: registry_user - . /home/caps/intra_max_bot/.env
REGISTRY_PASSWORD: - set +a
from_secret: registry_password - curl -fsS http://127.0.0.1:${APP_PORT}/health
when: when:
- branch: [main, master] - branch: [main, master]

130
DEPLOY_VARIANT_A.md Normal file
View file

@ -0,0 +1,130 @@
# Deploy Guide (Variant A: No Docker Runtime)
This guide is for deployment from source code on the server:
- `git pull` after each push
- restart `systemd` service
- no Docker runtime
## 0) What is already prepared in this repo
- CI pipeline for Variant A: [`.woodpecker.yml`](.woodpecker.yml)
- Env template with host/port/public IP/domain vars: [`.env.example`](.env.example)
- App runner that uses `APP_HOST`/`APP_PORT`: [`app/run.py`](app/run.py)
- `systemd` template: [`deploy/systemd/max-support.service.template`](deploy/systemd/max-support.service.template)
- `sudoers` template: [`deploy/sudoers/max-support-restart.template`](deploy/sudoers/max-support-restart.template)
- One-shot setup script: [`deploy/scripts/setup_variant_a.sh`](deploy/scripts/setup_variant_a.sh)
## 1) Create repository and connect CI
1. Create repository in GitHub/GitLab.
2. Push current project to `main`.
3. Connect repository to Woodpecker.
## 2) Prepare server
Example for Ubuntu/Debian:
```bash
sudo apt update
sudo apt install -y git curl python3 python3-venv python3-pip
sudo mkdir -p /opt/max-support
sudo chown -R $USER:$USER /opt/max-support
git clone <SSH_REPO_URL> /opt/max-support
cd /opt/max-support
```
## 3) Create server `.env`
Create `/opt/max-support/.env`:
```env
APP_ENV=prod
APP_HOST=0.0.0.0
APP_PORT=8000
# Public endpoint metadata (for ops/documentation)
APP_PUBLIC_IP=203.0.113.10
APP_PUBLIC_DOMAIN=bot.example.com
APP_BASE_URL=https://bot.example.com
DB_PATH=/opt/max-support/data/app.db
SESSION_SECRET=CHANGE_ME_LONG_RANDOM_STRING
WEB_ADMIN_LOGIN=admin
WEB_ADMIN_PASSWORD=CHANGE_ME_STRONG_PASSWORD
MAX_BOT_TOKEN=YOUR_MAX_BOT_TOKEN
MAX_BOT_API_URL=https://botapi.max.ru
# Recommended first start: polling mode
MAX_USE_WEBHOOK=false
MAX_WEBHOOK_URL=
MAX_WEBHOOK_SECRET=
CONTEXT_BACKEND=memory
REDIS_URL=redis://127.0.0.1:6379/0
CHANNEL_LOCAL_ENABLED=true
CHANNEL_EXTERNAL_ENABLED=true
EXTERNAL_API_CREATE_URL_WITH_KEY=https://external.example/api/create?key=YOUR_KEY
OPERATOR_CHAT_IDS=123456,987654
REQUEST_CATEGORIES=Общая консультация,Техническая проблема,Оплата,Другое
```
## 4) Run one-shot setup on server
```bash
cd /opt/max-support
chmod +x deploy/scripts/setup_variant_a.sh
APP_DIR=/opt/max-support APP_USER=$USER SERVICE_NAME=max-support ./deploy/scripts/setup_variant_a.sh
```
What this does:
1. Creates/updates `.venv`
2. Installs dependencies
3. Installs `systemd` unit
4. Installs `sudoers` rule for restart
5. Enables and starts `max-support`
6. Runs healthcheck on `127.0.0.1:${APP_PORT}`
## 5) Woodpecker secrets (required)
Create these repository secrets:
1. `deploy_host` - server host (IP or domain), e.g. `203.0.113.10`
2. `deploy_user` - SSH user on server, e.g. `deploy`
3. `deploy_port` - SSH port, usually `22`
4. `deploy_ssh_key` - private SSH key (PEM, multiline)
No registry secrets are needed in Variant A.
## 6) SSH and sudo requirements
`deploy_user` must be able to:
1. `cd /opt/max-support && git pull --ff-only`
2. `sudo systemctl restart max-support`
The setup script already installs sudoers rule in `/etc/sudoers.d/max-support-restart`:
```text
<deploy_user> ALL=(ALL) NOPASSWD:/bin/systemctl restart max-support,/bin/systemctl status max-support
```
## 7) CI/CD flow now
On each push to `main/master`:
1. Tests run
2. CI connects via SSH
3. Executes `git pull --ff-only`
4. Updates dependencies in `.venv`
5. Restarts `max-support` service
6. Checks `/health` using `APP_PORT` from `/opt/max-support/.env`
## 8) Useful commands on server
```bash
sudo systemctl status max-support
sudo journalctl -u max-support -n 200 --no-pager
sudo systemctl restart max-support
curl -fsS http://127.0.0.1:8000/health
```

17
app/run.py Normal file
View file

@ -0,0 +1,17 @@
from __future__ import annotations
import uvicorn
from app.config import get_settings
from app.main import create_app
def main() -> None:
settings = get_settings()
app = create_app(settings)
uvicorn.run(app, host=settings.app_host, port=settings.app_port)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# One-shot setup script for Variant A (no Docker runtime).
# It prepares venv, installs dependencies, installs systemd unit,
# installs sudoers rule for CI restart, and starts the service.
APP_DIR="${APP_DIR:-/opt/max-support}"
APP_USER="${APP_USER:-$USER}"
SERVICE_NAME="${SERVICE_NAME:-max-support}"
PYTHON_BIN="${PYTHON_BIN:-python3}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
SYSTEMD_TEMPLATE="${PROJECT_ROOT}/deploy/systemd/max-support.service.template"
SUDOERS_TEMPLATE="${PROJECT_ROOT}/deploy/sudoers/max-support-restart.template"
if [[ ! -d "${APP_DIR}" ]]; then
echo "ERROR: APP_DIR '${APP_DIR}' does not exist."
exit 1
fi
if [[ ! -f "${APP_DIR}/requirements.txt" ]]; then
echo "ERROR: '${APP_DIR}/requirements.txt' not found. Clone repo first."
exit 1
fi
if [[ ! -f "${APP_DIR}/.env" ]]; then
echo "ERROR: '${APP_DIR}/.env' not found. Create .env first."
exit 1
fi
echo "==> Ensuring ownership for ${APP_DIR}"
sudo chown -R "${APP_USER}:${APP_USER}" "${APP_DIR}"
echo "==> Creating/updating venv"
if [[ ! -d "${APP_DIR}/.venv" ]]; then
sudo -u "${APP_USER}" "${PYTHON_BIN}" -m venv "${APP_DIR}/.venv"
fi
echo "==> Installing Python dependencies"
sudo -u "${APP_USER}" bash -lc "cd '${APP_DIR}' && . .venv/bin/activate && pip install --no-cache-dir -r requirements.txt"
echo "==> Installing systemd unit"
SERVICE_RENDERED="$(sed "s|__APP_USER__|${APP_USER}|g; s|__APP_DIR__|${APP_DIR}|g" "${SYSTEMD_TEMPLATE}")"
echo "${SERVICE_RENDERED}" | sudo tee "/etc/systemd/system/${SERVICE_NAME}.service" > /dev/null
echo "==> Installing sudoers rule for CI restart"
SUDOERS_RENDERED="$(sed "s|__APP_USER__|${APP_USER}|g" "${SUDOERS_TEMPLATE}")"
echo "${SUDOERS_RENDERED}" | sudo tee "/etc/sudoers.d/${SERVICE_NAME}-restart" > /dev/null
sudo chmod 0440 "/etc/sudoers.d/${SERVICE_NAME}-restart"
sudo visudo -cf "/etc/sudoers.d/${SERVICE_NAME}-restart"
echo "==> Reloading and starting service"
sudo systemctl daemon-reload
sudo systemctl enable "${SERVICE_NAME}"
sudo systemctl restart "${SERVICE_NAME}"
sudo systemctl --no-pager status "${SERVICE_NAME}" || true
echo "==> Health check"
set -a
# shellcheck disable=SC1090
source "${APP_DIR}/.env"
set +a
curl -fsS "http://127.0.0.1:${APP_PORT}/health" || {
echo "ERROR: health check failed."
exit 1
}
echo "Setup completed successfully."

View file

@ -0,0 +1,2 @@
__APP_USER__ ALL=(ALL) NOPASSWD:/bin/systemctl restart max-support,/bin/systemctl status max-support

View file

@ -0,0 +1,18 @@
[Unit]
Description=MAX Support Bot (FastAPI + MAX bot runtime)
After=network.target
[Service]
Type=simple
User=__APP_USER__
WorkingDirectory=__APP_DIR__
EnvironmentFile=__APP_DIR__/.env
ExecStart=__APP_DIR__/.venv/bin/python -m app.run
Restart=always
RestartSec=3
KillSignal=SIGINT
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target