init 2
This commit is contained in:
parent
a29c45ee32
commit
6808521be0
7 changed files with 258 additions and 35 deletions
|
|
@ -1,6 +1,9 @@
|
|||
APP_ENV=dev
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8000
|
||||
APP_PUBLIC_IP=
|
||||
APP_PUBLIC_DOMAIN=
|
||||
APP_BASE_URL=
|
||||
|
||||
DB_PATH=data/app.db
|
||||
SESSION_SECRET=change-me
|
||||
|
|
@ -22,4 +25,3 @@ EXTERNAL_API_CREATE_URL_WITH_KEY=
|
|||
|
||||
OPERATOR_CHAT_IDS=
|
||||
REQUEST_CATEGORIES=Общая консультация,Техническая проблема,Оплата,Другое
|
||||
|
||||
|
|
|
|||
|
|
@ -6,47 +6,29 @@ steps:
|
|||
image: python:3.11-slim
|
||||
commands:
|
||||
- pip install --no-cache-dir -r requirements.txt
|
||||
- pytest -q
|
||||
- pytest -q tests -p no:cacheprovider
|
||||
|
||||
docker_build_push:
|
||||
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:
|
||||
deploy_no_docker:
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host:
|
||||
from_secret: deploy_host
|
||||
from_secret: forgejo_host_ssh
|
||||
username:
|
||||
from_secret: deploy_user
|
||||
from_secret: forgejo_user_ssh
|
||||
key:
|
||||
from_secret: deploy_ssh_key
|
||||
from_secret: forgejo_key_ssh
|
||||
port:
|
||||
from_secret: deploy_port
|
||||
from_secret: forgejo_port_ssh
|
||||
script:
|
||||
- docker login ${CI_REGISTRY} -u ${REGISTRY_USER} -p ${REGISTRY_PASSWORD}
|
||||
- docker pull ${CI_REGISTRY_IMAGE}:stable
|
||||
- docker rm -f max-support-bot || true
|
||||
- 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
|
||||
- curl -fsS http://127.0.0.1:8000/health
|
||||
environment:
|
||||
REGISTRY_USER:
|
||||
from_secret: registry_user
|
||||
REGISTRY_PASSWORD:
|
||||
from_secret: registry_password
|
||||
- cd /home/caps/intra_max_bot
|
||||
- git pull --ff-only
|
||||
- if [ ! -d .venv ]; then python3 -m venv .venv; fi
|
||||
- . .venv/bin/activate
|
||||
- pip install --no-cache-dir -r requirements.txt
|
||||
- sudo systemctl restart max-support
|
||||
- set -a
|
||||
- . /home/caps/intra_max_bot/.env
|
||||
- set +a
|
||||
- curl -fsS http://127.0.0.1:${APP_PORT}/health
|
||||
when:
|
||||
- branch: [main, master]
|
||||
|
||||
|
|
|
|||
130
DEPLOY_VARIANT_A.md
Normal file
130
DEPLOY_VARIANT_A.md
Normal 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
17
app/run.py
Normal 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()
|
||||
|
||||
72
deploy/scripts/setup_variant_a.sh
Normal file
72
deploy/scripts/setup_variant_a.sh
Normal 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."
|
||||
|
||||
2
deploy/sudoers/max-support-restart.template
Normal file
2
deploy/sudoers/max-support-restart.template
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
__APP_USER__ ALL=(ALL) NOPASSWD:/bin/systemctl restart max-support,/bin/systemctl status max-support
|
||||
|
||||
18
deploy/systemd/max-support.service.template
Normal file
18
deploy/systemd/max-support.service.template
Normal 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
|
||||
|
||||
Loading…
Add table
Reference in a new issue