Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Webhook и деплой
webhook_deployment

Webhook и деплой

Настройка webhook, SSL, nginx, Docker, CI/CD, production-деплой

Webhook и деплой

Webhook — рекомендуемый способ получения обновлений для production-ботов. В этой теме вы научитесь настраивать webhook, SSL, nginx, Docker и CI/CD для надёжного деплоя.

#Long Polling vs Webhook

ПараметрLong PollingWebhook
Простота настройки✅ Очень просто⚠️ Требует HTTPS
Задержка1-2 секундыМгновенно
РесурсыПостоянное соединениеТолько при событии
Масштабирование❌ Сложно✅ Легко
Production❌ Не рекомендуется✅ Рекомендуется

#Настройка webhook

#Базовая настройка

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()

#SSL сертификаты

#Self-signed для тестов

# Создать самоподписанный сертификат 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

#Let's Encrypt для production

# Установить certbot sudo apt install certbot python3-certbot-nginx # Получить сертификат sudo certbot --nginx -d your-domain.com # Автоматическое обновление sudo certbot renew --dry-run

#Использование в боте

import 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()

#Nginx как reverse proxy

#Базовая конфигурация

# /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 nginx

#Docker контейнеризация

#Dockerfile

FROM 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 --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 # Запуск CMD ["python", "-m", "bot.main"]

#docker-compose.yml

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:

#.env.example

# 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.key

#Health Check endpoint

from 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)

#CI/CD с GitLab CI

#.gitlab-ci.yml

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: always

#Логирование в production

import 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)

Далее: Масштабирование и очереди задач