Телеграм-бот на Python: от команды до деплоя

27 августа 2026
Автор

Антон Ларичев

Что мы будем строить

В этой статье пройдём весь путь создания Telegram-бота на Python — от регистрации бота у BotFather до запуска в продакшене. Используем библиотеку aiogram 3, которая поддерживает асинхронное программирование через asyncio и предоставляет удобный интерфейс для работы с Telegram Bot API.

Бот будет уметь:

  • Обрабатывать команды (/start, /help, /profile)
  • Работать с инлайн-клавиатурами и callback-запросами
  • Вести многошаговые диалоги через FSM (Finite State Machine)
  • Запускаться как системный сервис через systemd или Docker

Курс по теме

Курс Python с нуля

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Подготовка окружения

Создайте виртуальное окружение и установите зависимости:

python -m venv venv
source venv/bin/activate  # Linux/macOS
venv\Scripts\activate     # Windows

pip install aiogram==3.7.0 python-dotenv

Создайте файл .env для хранения токена:

BOT_TOKEN=ваш_токен_бота

Структура проекта:

telegram-bot/
├── .env
├── bot.py
├── handlers/
│   ├── __init__.py
│   ├── commands.py
│   └── messages.py
├── keyboards/
│   ├── __init__.py
│   └── inline.py
├── states/
│   └── user_states.py
└── requirements.txt

Получение токена у BotFather

Перед написанием кода нужно зарегистрировать бота:

  1. Откройте Telegram и найдите @BotFather
  2. Отправьте команду /newbot
  3. Укажите имя бота, например My Test Bot
  4. Укажите username, который должен заканчиваться на bot, например my_test_bot
  5. Скопируйте полученный токен вида 1234567890:ABCdefGHIjklMNOpqrSTUvwxyz

Вставьте токен в файл .env.

Точка входа и инициализация бота

Создайте файл bot.py:

import asyncio
import logging
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from dotenv import load_dotenv
import os

from handlers.commands import router as commands_router
from handlers.messages import router as messages_router

load_dotenv()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


async def main():
    bot = Bot(token=os.getenv("BOT_TOKEN"))
    dp = Dispatcher(storage=MemoryStorage())

    dp.include_router(commands_router)
    dp.include_router(messages_router)

    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

Dispatcher — центральный объект aiogram, который маршрутизирует входящие обновления к нужным обработчикам. MemoryStorage хранит состояния FSM в памяти процесса — для продакшена стоит заменить на Redis.

Обработка команд

Создайте файл handlers/commands.py:

from aiogram import Router
from aiogram.types import Message, CallbackQuery
from aiogram.filters import Command, CommandStart
from keyboards.inline import main_menu_keyboard, back_keyboard

router = Router()


@router.message(CommandStart())
async def cmd_start(message: Message):
    await message.answer(
        f"Привет, {message.from_user.first_name}!\n"
        "Я учебный бот. Используй /help для списка команд."
    )


@router.message(Command("help"))
async def cmd_help(message: Message):
    help_text = (
        "Доступные команды:\n"
        "/start — приветствие\n"
        "/help — список команд\n"
        "/profile — ваш профиль\n"
        "/menu — главное меню\n"
        "/register — регистрация"
    )
    await message.answer(help_text)


@router.message(Command("profile"))
async def cmd_profile(message: Message):
    user = message.from_user
    profile_text = (
        f"Ваш профиль:\n"
        f"ID: {user.id}\n"
        f"Имя: {user.full_name}\n"
        f"Username: @{user.username or 'не указан'}"
    )
    await message.answer(profile_text)


@router.message(Command("menu"))
async def cmd_menu(message: Message):
    await message.answer("Главное меню:", reply_markup=main_menu_keyboard())


@router.callback_query(lambda c: c.data.startswith("menu:"))
async def process_menu_callback(callback: CallbackQuery):
    action = callback.data.split(":")[1]

    if action == "main":
        await callback.message.edit_text(
            "Главное меню:", reply_markup=main_menu_keyboard()
        )
    elif action == "news":
        await callback.message.edit_text(
            "Раздел новостей (в разработке)",
            reply_markup=back_keyboard()
        )
    elif action == "settings":
        await callback.message.edit_text(
            "Настройки (в разработке)",
            reply_markup=back_keyboard()
        )
    elif action == "about":
        await callback.message.edit_text(
            "Учебный бот, созданный на aiogram 3",
            reply_markup=back_keyboard()
        )

    await callback.answer()

CommandStart() — специальный фильтр aiogram для команды /start. Всегда вызывайте await callback.answer() при обработке inline-кнопок — без этого пользователь видит бесконечный индикатор загрузки на кнопке.

Инлайн-клавиатуры

Инлайн-клавиатуры — кнопки, прикреплённые непосредственно к сообщению. Создайте keyboards/inline.py:

from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder


def main_menu_keyboard() -> InlineKeyboardMarkup:
    builder = InlineKeyboardBuilder()
    builder.button(text="Новости", callback_data="menu:news")
    builder.button(text="Настройки", callback_data="menu:settings")
    builder.button(text="О боте", callback_data="menu:about")
    builder.adjust(2)  # по 2 кнопки в ряду
    return builder.as_markup()


def back_keyboard() -> InlineKeyboardMarkup:
    builder = InlineKeyboardBuilder()
    builder.button(text="Назад", callback_data="menu:main")
    return builder.as_markup()

InlineKeyboardBuilder — удобный построитель клавиатур из aiogram. Метод adjust(2) равномерно раскладывает кнопки по 2 в ряд.

Машина состояний (FSM)

FSM позволяет вести многошаговые диалоги с пользователем. Каждый шаг — отдельное состояние. Создайте states/user_states.py:

from aiogram.fsm.state import State, StatesGroup


class RegistrationState(StatesGroup):
    waiting_for_name = State()
    waiting_for_age = State()
    waiting_for_city = State()

Создайте handlers/messages.py:

from aiogram import Router, F
from aiogram.types import Message
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from states.user_states import RegistrationState

router = Router()


@router.message(Command("register"))
async def cmd_register(message: Message, state: FSMContext):
    await state.set_state(RegistrationState.waiting_for_name)
    await message.answer("Введите ваше имя:")


@router.message(RegistrationState.waiting_for_name)
async def process_name(message: Message, state: FSMContext):
    await state.update_data(name=message.text)
    await state.set_state(RegistrationState.waiting_for_age)
    await message.answer("Введите ваш возраст:")


@router.message(RegistrationState.waiting_for_age)
async def process_age(message: Message, state: FSMContext):
    if not message.text.isdigit():
        await message.answer("Пожалуйста, введите число:")
        return

    await state.update_data(age=int(message.text))
    await state.set_state(RegistrationState.waiting_for_city)
    await message.answer("Введите ваш город:")


@router.message(RegistrationState.waiting_for_city)
async def process_city(message: Message, state: FSMContext):
    await state.update_data(city=message.text)
    data = await state.get_data()

    summary = (
        f"Регистрация завершена!\n"
        f"Имя: {data['name']}\n"
        f"Возраст: {data['age']}\n"
        f"Город: {data['city']}"
    )
    await message.answer(summary)
    await state.clear()


@router.message(F.text)
async def handle_text(message: Message, state: FSMContext):
    current_state = await state.get_state()
    if current_state is None:
        await message.answer(
            "Не понимаю эту команду. Используй /help для списка команд."
        )

Фильтр F.text срабатывает на любое текстовое сообщение. Важно разместить этот обработчик последним: aiogram проверяет роутеры в порядке подключения, и более специфичные фильтры должны идти первыми.

Middleware

Middleware выполняется до или после каждого обработчика. Используйте его для логирования, проверки прав доступа или ограничения частоты запросов:

# middleware.py
from typing import Callable, Dict, Any, Awaitable
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
import logging

logger = logging.getLogger(__name__)


class LoggingMiddleware(BaseMiddleware):
    async def __call__(
        self,
        handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
        event: TelegramObject,
        data: Dict[str, Any]
    ) -> Any:
        user = data.get("event_from_user")
        logger.info(f"Обновление от пользователя {user.id if user else 'unknown'}")
        result = await handler(event, data)
        return result

Регистрация middleware в bot.py:

from middleware import LoggingMiddleware

dp.message.middleware(LoggingMiddleware())
dp.callback_query.middleware(LoggingMiddleware())

Деплой через systemd

Для запуска бота на VPS как системного сервиса создайте файл /etc/systemd/system/telegram-bot.service:

[Unit]
Description=Telegram Bot
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/telegram-bot
EnvironmentFile=/opt/telegram-bot/.env
ExecStart=/opt/telegram-bot/venv/bin/python bot.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Активируйте и запустите сервис:

sudo systemctl daemon-reload
sudo systemctl enable telegram-bot
sudo systemctl start telegram-bot
sudo systemctl status telegram-bot

Просмотр логов в реальном времени:

sudo journalctl -u telegram-bot -f

Деплой через Docker

Создайте Dockerfile:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "bot.py"]

Создайте docker-compose.yml с Redis для хранения состояний FSM между перезапусками:

services:
  bot:
    build: .
    env_file: .env
    restart: unless-stopped
    depends_on:
      - redis
    environment:
      - REDIS_URL=redis://redis:6379/0

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data

volumes:
  redis_data:

Чтобы использовать Redis вместо MemoryStorage, установите дополнительный пакет и обновите bot.py:

pip install aiogram[redis]
from aiogram.fsm.storage.redis import RedisStorage
import os

storage = RedisStorage.from_url(os.getenv("REDIS_URL", "redis://localhost:6379/0"))
dp = Dispatcher(storage=storage)

Запуск контейнеров:

docker compose up -d --build
docker compose logs -f bot

Лимиты Telegram API и типичные ошибки

Лимиты на отправку сообщений

Telegram ограничивает количество сообщений: не более 30 сообщений в секунду для всего бота и не более 1 сообщения в секунду для одного чата. При массовой рассылке добавляйте задержки:

import asyncio

async def broadcast(bot: Bot, user_ids: list[int], text: str):
    for user_id in user_ids:
        try:
            await bot.send_message(user_id, text)
        except Exception as e:
            logger.error(f"Ошибка отправки {user_id}: {e}")
        await asyncio.sleep(0.05)  # 20 сообщений в секунду — безопасный темп

Порядок роутеров

aiogram проверяет роутеры в порядке их подключения. Более специфичные обработчики должны быть зарегистрированы первыми:

dp.include_router(commands_router)  # сначала команды
dp.include_router(messages_router)  # потом общие сообщения

Обработка исходящих ошибок

Пользователь может заблокировать бота — при этом send_message выбросит TelegramForbiddenError. Всегда оборачивайте массовые отправки в try/except.

Webhook вместо Polling

Polling — бот постоянно опрашивает серверы Telegram. Webhook — Telegram сам отправляет обновления на ваш URL. Webhook эффективнее для нагруженных ботов, но требует HTTPS-домена.

from aiohttp import web
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application

WEBHOOK_URL = "https://your-domain.com/webhook"


async def on_startup(bot: Bot):
    await bot.set_webhook(WEBHOOK_URL)


async def on_shutdown(bot: Bot):
    await bot.delete_webhook()


async def main():
    bot = Bot(token=os.getenv("BOT_TOKEN"))
    dp = Dispatcher(storage=MemoryStorage())

    dp.include_router(commands_router)
    dp.include_router(messages_router)
    dp.startup.register(on_startup)
    dp.shutdown.register(on_shutdown)

    app = web.Application()
    handler = SimpleRequestHandler(dispatcher=dp, bot=bot)
    handler.register(app, path="/webhook")
    setup_application(app, dp, bot=bot)

    web.run_app(app, host="0.0.0.0", port=8080)

Итог

Мы построили полноценного Telegram-бота на Python с aiogram 3:

  • Настроили структуру проекта с разделением обработчиков, клавиатур и состояний
  • Реализовали команды, инлайн-клавиатуры с callback-обработчиками
  • Добавили многошаговые диалоги через FSM с хранением данных
  • Добавили middleware для сквозного логирования
  • Задеплоили бота через systemd и Docker с Redis

Чтобы уверенно работать с асинхронным Python и строить сложные приложения — изучите Python с нуля до продвинутого уровня на курсе PurpleSchool.

Курс по Python на PurpleSchool

Фоновые задачи с Celery в PythonСтрелочка вправо

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonРабота с классами в PythonКак скачать Python на компьютерПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак работает интерпретатор PythonКак использовать print для строк в PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
Управление проектами на GitHub с PythonЗагрузка данных PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonСоздание собственных декораторов в PythonВложенные функции в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныОсновные методы Python и примеры их использованияПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonАргументы по умолчанию в PythonЧто делает функция id в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСоздание исполняемого файла Python в exeРазбор traceback в модуле PythonСтруктурирование кода в PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий