Настройка webhook, SSL, nginx, Docker, CI/CD, production-деплой
Webhook — рекомендуемый способ получения обновлений для production-ботов. В этой теме вы научитесь настраивать webhook, SSL, nginx, Docker и CI/CD для надёжного деплоя.
| Параметр | Long Polling | Webhook |
|---|---|---|
| Простота настройки | ✅ Очень просто | ⚠️ Требует HTTPS |
| Задержка | 1-2 секунды | Мгновенно |
| Ресурсы | Постоянное соединение | Только при событии |
| Масштабирование | ❌ Сложно | ✅ Легко |
| Production | ❌ Не рекомендуется | ✅ Рекомендуется |
from aiogram import Bot, Dispatcher
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
from aiohttp import web
from bot.config import settings
from bot.dispatcher import create_dispatcher
async def on_startup(bot: Bot):
"""Устанавливает webhook при старте."""
await bot.set_webhook(
url=settings.webhook_url,
allowed_updates=Dispatcher.DEFAULT_UPDATE_TYPES
)
async def on_shutdown(bot: Bot):
"""Удаляет webhook при остановке."""
await bot.delete_webhook()
async def main():
bot = Bot(token=settings.bot_token.get_secret_value())
redis = Redis.from_url(settings.redis_url)
dp = create_dispatcher(bot, redis)
# Хуки старта/остановки
dp.startup.register(on_startup)
dp.shutdown.register(on_shutdown)
# Веб-сервер для webhook
app = web.Application()
webhook_requests_handler = SimpleRequestHandler(
dispatcher=dp,
bot=bot,
secret_token=settings.webhook_secret
)
webhook_requests_handler.register(app, path='/webhook')
# Запуск
await setup_application(app, dp, bot=bot)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, host='0.0.0.0', port=8000)
await site.start()
# Ждём остановки
while True:
await asyncio.sleep(3600)
if __name__ == '__main__':
asyncio.run(main())# bot/config.py
from pydantic_settings import BaseSettings
from pydantic import Field, SecretStr
class Settings(BaseSettings):
bot_token: SecretStr = Field(..., env='BOT_TOKEN')
# Webhook
webhook_url: str = Field(..., env='WEBHOOK_URL') # https://domain.com/webhook
webhook_secret: str = Field(..., env='WEBHOOK_SECRET')
webhook_host: str = Field(default='0.0.0.0', env='WEBHOOK_HOST')
webhook_port: int = Field(default=8000, env='WEBHOOK_PORT')
# SSL
ssl_cert_path: str = Field(default='/etc/ssl/certs/bot.crt', env='SSL_CERT_PATH')
ssl_key_path: str = Field(default='/etc/ssl/private/bot.key', env='SSL_KEY_PATH')
class Config:
env_file = '.env'
settings = Settings()# Создать самоподписанный сертификат
openssl req -newkey rsa:2048 -sha256 -nodes -x509 -days 365 \
-keyout private.key \
-out public.crt \
-subj '/CN=your-domain.com'
# Проверить сертификат
openssl x509 -in public.crt -text -noout# Установить certbot
sudo apt install certbot python3-certbot-nginx
# Получить сертификат
sudo certbot --nginx -d your-domain.com
# Автоматическое обновление
sudo certbot renew --dry-runimport ssl
from aiohttp import web
async def main():
bot = Bot(token=settings.bot_token.get_secret_value())
dp = create_dispatcher(bot, redis)
app = web.Application()
SimpleRequestHandler(dp, bot).register(app, path='/webhook')
# SSL контекст
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(
certfile=settings.ssl_cert_path,
keyfile=settings.ssl_key_path
)
runner = web.AppRunner(app)
await runner.setup()
# HTTPS сервер
site = web.TCPSite(runner, host='0.0.0.0', port=8443, ssl_context=ssl_context)
await site.start()
# HTTP для health checks
http_site = web.TCPSite(runner, host='0.0.0.0', port=8000)
await http_site.start()# /etc/nginx/sites-available/telegram-bot
server {
listen 80;
server_name your-domain.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL сертификаты
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# SSL настройки
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Webhook endpoint
location /webhook {
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-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Таймауты
proxy_connect_timeout 10s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# Для webhook важно
client_max_body_size 10M;
}
# Health check endpoint
location /health {
proxy_pass http://127.0.0.1:8000/health;
access_log off;
}
# Metrics endpoint (Prometheus)
location /metrics {
proxy_pass http://127.0.0.1:8000/metrics;
allow 10.0.0.0/8; # Только внутренняя сеть
deny all;
}
}# Проверить конфиг nginx
sudo nginx -t
# Перезагрузить nginx
sudo systemctl reload nginx
# Проверить статус
sudo systemctl status nginxFROM python:3.11-slim
# Переменные окружения
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Рабочая директория
WORKDIR /app
# Установка зависимостей
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Копирование файлов зависимостей
COPY pyproject.toml poetry.lock ./
# Установка Poetry и зависимостей
RUN pip install --no-cache-dir poetry && \
poetry config virtualenvs.create false && \
poetry install --only main --no-interaction --no-ansi
# Копирование кода
COPY . .
# Пользователь без root
RUN useradd --create-home --shell /bin/bash bot && \
chown -R bot:bot /app
USER bot
# Экспортируемый порт
EXPOSE 8000
# Health check
HEALTHCHECK \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# Запуск
CMD ["python", "-m", "bot.main"]version: '3.8'
services:
bot:
build:
context: .
dockerfile: Dockerfile
container_name: telegram-bot
restart: unless-stopped
ports:
- "8000:8000"
environment:
- BOT_TOKEN=${BOT_TOKEN}
- WEBHOOK_URL=${WEBHOOK_URL}
- WEBHOOK_SECRET=${WEBHOOK_SECRET}
- DATABASE_URL=postgresql+asyncpg://bot:password@db:5432/botdb
- REDIS_URL=redis://redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- bot-network
volumes:
- ./logs:/app/logs
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:15-alpine
container_name: bot-db
restart: unless-stopped
environment:
POSTGRES_DB: botdb
POSTGRES_USER: bot
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- bot-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bot -d botdb"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: bot-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- bot-network
nginx:
image: nginx:alpine
container_name: bot-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/ssl/bot:ro
- letsencrypt:/etc/letsencrypt
depends_on:
- bot
networks:
- bot-network
networks:
bot-network:
driver: bridge
volumes:
postgres_data:
redis_data:
letsencrypt:# Bot
BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
# Webhook
WEBHOOK_URL=https://your-domain.com/webhook
WEBHOOK_SECRET=your-secret-token
# Database
DATABASE_URL=postgresql+asyncpg://bot:password@db:5432/botdb
# Redis
REDIS_URL=redis://redis:6379/0
# SSL
SSL_CERT_PATH=/etc/ssl/bot/public.crt
SSL_KEY_PATH=/etc/ssl/bot/private.keyfrom aiohttp import web
from datetime import datetime
async def health_handler(request: web.Request) -> web.Response:
"""Health check endpoint."""
return web.json_response({
'status': 'ok',
'timestamp': datetime.utcnow().isoformat()
})
async def ready_handler(request: web.Request) -> web.Response:
"""Readiness check (зависимости доступны)."""
try:
# Проверка БД
await check_database()
# Проверка Redis
await check_redis()
# Проверка Telegram API
await check_telegram_api()
return web.json_response({'status': 'ready'})
except Exception as e:
return web.json_response(
{'status': 'not ready', 'error': str(e)},
status=503
)
def register_health_endpoints(app: web.Application):
app.router.add_get('/health', health_handler)
app.router.add_get('/ready', ready_handler)stages:
- lint
- test
- build
- deploy
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
DOCKER_IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
lint:
stage: lint
image: python:3.11-slim
before_script:
- pip install ruff mypy
script:
- ruff check bot/
- mypy bot/
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
test:
stage: test
image: python:3.11-slim
services:
- postgres:15-alpine
- redis:7-alpine
variables:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
DATABASE_URL: postgresql+asyncpg://test:test@postgres/testdb
REDIS_URL: redis://redis:6379/0
before_script:
- pip install poetry
- poetry install --with dev
script:
- poetry run pytest tests/ --cov=bot --cov-report=xml
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
build:
stage: build
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
- docker tag $DOCKER_IMAGE $DOCKER_IMAGE_LATEST
- docker push $DOCKER_IMAGE_LATEST
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $DEPLOY_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- ssh $DEPLOY_USER@$DEPLOY_HOST "cd /opt/telegram-bot && docker-compose pull && docker-compose up -d"
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: production
url: https://your-domain.com# Обновление системы
sudo apt update && sudo apt upgrade -y
# Установка Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Установка docker-compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Клонирование репозитория
git clone https://gitlab.com/your/telegram-bot.git /opt/telegram-bot
cd /opt/telegram-bot
# Копирование .env
cp .env.example .env
nano .env # Заполнить переменные# Первый запуск
docker-compose up -d
# Проверка логов
docker-compose logs -f bot
# Перезапуск
docker-compose restart bot
# Остановка
docker-compose down# docker-compose.yml
services:
bot:
restart: unless-stopped
# или always для критичных сервисов
# restart: alwaysimport logging
import sys
from logging.handlers import RotatingFileHandler
def setup_logging():
"""Настройка логирования для production."""
# Форматтер
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
# File handler с ротацией
file_handler = RotatingFileHandler(
'logs/bot.log',
maxBytes=10*1024*1024, # 10MB
backupCount=5
)
file_handler.setFormatter(formatter)
# Root logger
logging.basicConfig(
level=logging.INFO,
handlers=[console_handler, file_handler]
)
# Логгер aiogram
logging.getLogger('aiogram').setLevel(logging.WARNING)
logging.getLogger('aiohttp').setLevel(logging.WARNING)Правильная настройка webhook и деплоя — основа надёжного production-бота.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.