Ноутбук засыпает, бесплатные тарифы усыпляют процесс. Разбираем, как поднять бота на VPS так, чтобы он сам вставал после падения и перезагрузки - с объяснением каждой команды.
Что нужно: VPS с Ubuntu 22.04 или 24.04 (1 vCPU и 512 МБ-1 ГБ RAM с запасом хватит), токен бота от @BotFather и минут пятнадцать. На выходе: бот переживает перезагрузки, падения и обрывы сети, а логи можно нормально читать.
Весь путь одной строкой: код кладётся в
/opt/mybotпод отдельным пользователем, рядом собирается виртуальное окружение, токен хранится в env-файле, для запуска пишется unitsystemdсRestart=always, потомsystemctl enable --now mybot. Логи смотрятся вjournalctl -u mybot. Всё ниже - то же самое, но по шагам и с объяснениями.
Бот на long polling (сам, в цикле, спрашивает у Телеграма новые сообщения) должен быть на связи каждую секунду: нет процесса - нет ответов. Ноутбук для этого не годится: засыпает, теряет Wi-Fi, уходит в перезагрузку ради обновлений. Бесплатные тарифы «always-on» для веб-сервисов усыпляют процесс обычно через 15-30 минут без входящих HTTP-запросов, а у бота на long polling их нет вообще - он засыпает и просыпается с задержкой. VPS за 2-3 доллара просто работает. Вдобавок вы получаете нормальную систему запуска служб (шаг 4), и «держать бота живым» становится одним конфигом вместо набора костылей. Бот здесь - частный случай общего приёма «запустить любой процесс как службу systemd»; если тот же подход понадобится для не-бота, есть отдельный разбор как развернуть сервис на сервере и держать его 24/7.
@BotFather в Телеграме - строка вида 123456:AA....Все команды ниже выполняются от root. Если вы вошли обычным пользователем, добавляйте перед ними sudo.
Сначала защитите сервер. На боевой машине первым делом сделайте минимум: заведите пользователя с sudo вместо root, включите вход по SSH-ключу и отключите вход по паролю, поднимите файрвол, который пускает только SSH (и порт 443, если позже пойдёте через webhook). Подробности - в статье «Защита свежего VPS».
Первым делом ставим системные пакеты: Python с поддержкой виртуальных окружений и git, чтобы забрать код.
apt update && apt install -y python3-venv python3-pip git
Дальше кладём код в /opt/mybot. Клонируйте репозиторий до создания пользователя: если сначала завести пользователя, его домашняя папка уже будет непустой, и git clone в неё откажется работать.
mkdir -p /opt/mybot
git clone https://your.repo/mybot.git /opt/mybot
Теперь заводим отдельного непривилегированного пользователя, под которым бот будет жить. Смысл простой: если в боте окажется дыра, она упрётся в права этого пользователя и не дотянется до остального сервера. Флаг --system создаёт служебную учётную запись без пароля; войти под ней через su - mybot не получится (у неё оболочка /usr/sbin/nologin), а запускать команды от её имени можно через sudo -u mybot - так и будем отлаживать.
adduser --system --group --home /opt/mybot mybot
chown -R mybot:mybot /opt/mybot
chown -R отдаёт каталог новому пользователю - иначе он не сможет писать в собственную папку. Дальше собираем виртуальное окружение: отдельная папка с личной копией Python и библиотек, чтобы зависимости бота не смешивались с системными и не ломались при обновлении Ubuntu. Команды - от имени mybot с флагом -H, иначе pip засыпает лог предупреждениями о чужом домашнем каталоге.
sudo -H -u mybot python3 -m venv /opt/mybot/.venv
sudo -H -u mybot /opt/mybot/.venv/bin/pip install --upgrade pip
sudo -H -u mybot /opt/mybot/.venv/bin/pip install -r /opt/mybot/requirements.txt
Обновление pip первой строкой убирает половину странных ошибок при сборке пакетов. Если pip install прошёл без красных строк - зависимости на месте.
В requirements.txt закрепите версию библиотеки бота, например python-telegram-bot==22.8 или aiogram==3.31.1. Актуальным python-telegram-bot v22 и aiogram 3.31 нужен Python 3.10 или новее: на Ubuntu 24.04 (Python 3.12) и Debian 12 (3.11) всё в порядке, на Ubuntu 20.04 и Debian 11 - уже нет, там сначала ставят более новый Python (deadsnakes или pyenv).
Токен - это пароль от бота: кто им владеет, тот управляет ботом. Поэтому в исходниках, тем более в публичном репозитории, ему не место. Держим его в env-файле - обычном текстовом файле со строками вида ИМЯ=значение, который читает оболочка или сам код при старте. Создайте /opt/mybot/.env:
BOT_TOKEN=123456:AA...ваш-токен
TZ=Europe/Amsterdam
chown mybot:mybot /opt/mybot/.env
chmod 600 /opt/mybot/.env
chmod 600 закрывает файл от всех, кроме владельца. Код при этом читает токен из окружения (os.environ["BOT_TOKEN"]), а не из строки в исходнике. Для публичного репозитория это разница между рабочим ботом и угнанным.
Файл .env лежит внутри рабочей копии git, поэтому неосторожная команда git add -A легко утащит токен в историю. Подстрахуйтесь один раз:
echo '.env' >> /opt/mybot/.gitignore
Про TZ в этом же файле: он влияет только на «наивные» объекты datetime в коде Python. Метки времени в journalctl останутся в системном часовом поясе сервера - если хотите, чтобы и они совпадали, задайте пояс всей машине: timedatectl set-timezone Europe/Amsterdam.
Перед тем как оформлять бота в службу, запустите его руками - убедиться, что он стартует и отвечает. Наивный вариант sudo -u mybot --preserve-env .venv/bin/python bot.py не сработает: --preserve-env сохраняет окружение root, а не mybot, и файл .env всё равно никто не читает. Env-файл - не магия: переменные из него должен кто-то загрузить. В итоге os.environ["BOT_TOKEN"] падает с KeyError, и бот умирает на каждом запуске. Правильная строка подгружает .env сама:
sudo -u mybot bash -c 'set -a; cd /opt/mybot; . ./.env; exec .venv/bin/python bot.py'
set -a - всё, что дальше присваивается, автоматически становится переменной окружения; без этого строки из .env не увидит дочерний процесс Python.. ./.env - точка (команда source) выполняет файл в текущей оболочке и подставляет переменные; без неё .env - просто текст.exec - заменяет оболочку процессом Python, лишний процесс не висит.Напишите боту в Телеграме. Ответил, в терминале нет трейсбека - остановите бота через Ctrl+C и переходите к службе. Молчит или сыплет ошибками - чините сейчас: обёртка-служба сломанного бота не починит, она будет только перезапускать его по кругу.
systemd - штатная система запуска служб в Linux: она поднимает процесс при загрузке сервера, следит за ним и перезапускает, если он упал. Именно эта часть и делает «24/7». Описание службы - это текстовый файл (unit). Создайте /etc/systemd/system/mybot.service:
[Unit]
Description=My Telegram bot
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=exec
User=mybot
WorkingDirectory=/opt/mybot
EnvironmentFile=/opt/mybot/.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/mybot/.venv/bin/python /opt/mybot/bot.py
Restart=always
RestartSec=5
TimeoutStopSec=25
[Install]
WantedBy=multi-user.target
Type=exec - для долгоживущих служб современный systemd советует именно его, а не Type=simple: служба считается запущенной только после успешного exec(), поэтому опечатку в пути к Python или скрипту видно сразу как ошибку старта, а не как «запустился и тут же умер». Проверено на реальном сервере.Restart=always и RestartSec=5 - если процесс завершился сам (упал или вышел штатно), systemd поднимает его через пять секунд. Исключение - явная команда systemctl stop: после неё бот не перезапускается, это осознанная остановка.TimeoutStopSec=25 - после systemctl stop / restart systemd столько ждёт чистого завершения, прежде чем послать SIGKILL. Дефолтные 90 секунд тормозят деплой; боту хватает нескольких секунд. python-telegram-bot и aiogram на Linux уже сами ловят SIGTERM и корректно завершаются, так что свой обработчик сигналов в коде не нужен.StartLimitIntervalSec=60 и StartLimitBurst=5 - разрешают не больше пяти попыток запуска за минуту; после этого юнит встаёт в failed, и сломанный бот (неверный токен, отсутствующая зависимость) виден сразу, а не крутится в тихом цикле рестартов. У systemd есть и глобальный дефолт (DefaultStartLimitIntervalUSec=10s, DefaultStartLimitBurst=5), явные значения в юните просто делают поведение предсказуемым. Если бот обязан подниматься любой ценой и failed вам не нужен - поставьте StartLimitIntervalSec=0, но тогда крон-падение будет молча забивать журнал.After=network-online.target и Wants=network-online.target - не запускать бота раньше, чем поднялась сеть. «Сеть поднялась» не гарантирует, что работает DNS или отвечает Телеграм, поэтому код всё равно должен переживать сетевую ошибку и повторять попытку.EnvironmentFile=/opt/mybot/.env - systemd сам прочитает env-файл и передаст переменные боту. Пути указаны без ведущего дефиса намеренно: без токена бот бесполезен, поэтому если .env пропал или потерял права на чтение, служба должна честно не стартовать, а не тихо запускаться без токена. Проверено на реальном сервере - при отсутствующем файле в журнале видно Failed to load environment files: No such file or directory. Дефис (EnvironmentFile=-путь) уместен только для действительно необязательного файла.Environment=PYTHONUNBUFFERED=1 - без этого print() буферизуется, и journalctl -f показывает вывод с задержкой, что убивает весь смысл живого лога.ExecStart - только абсолютные пути: и до Python в виртуальном окружении, и до скрипта.Перечитайте конфигурацию systemd и включите службу - разом на запуск сейчас и на автозапуск при загрузке:
systemctl daemon-reload
systemctl enable --now mybot
Проверка: systemctl status mybot должен показать active (running). Если там failed - смотрите логи (шаг 5), причина всегда там.
Заработало - укрепите. Добавьте в
[Service]:NoNewPrivileges=yes,ProtectSystem=strict(вся файловая система только на чтение; venv при этом остаётся читаемым),ProtectHome=yes,PrivateTmp=yes,RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX(оставьтеAF_UNIX, иначе сломаются резолвинг DNS и запись в journald),SystemCallFilter=@system-service,CapabilityBoundingSet=(пусто),MemoryMax=256M. Если бот пишет файл SQLite или локальные логи, добавьте ещёReadWritePaths=/opt/mybot/data-ProtectSystem=strictблокирует запись во всё остальное, и типичный симптом забытой строки -unable to open database file. Проверьте результат командойsystemd-analyze security mybot: этот набор поднял тестового бота с незащищённого дефолта до уровня уязвимости 4.0 OK на живом сервере.
Последняя проверка, которую обычно пропускают: перезагрузите весь сервер целиком, переподключитесь и убедитесь, что служба поднялась сама.
systemctl status mybot
journalctl -u mybot -f
journalctl заменяет лог-файл. Флаг -f показывает строки вживую, -u mybot - только записи нашей службы. journalctl -u mybot -b покажет логи с текущей загрузки сервера (не «последней» - именно нынешней); предыдущая загрузка - это -b -1. Ротацию логов journald делает сам. Со стороны бота достаточно выводить всё в stdout (или направить туда стандартный logging Python) - и это попадёт сюда. Благодаря PYTHONUNBUFFERED=1 из шага 4 строки появляются сразу, а не пачками.
Одна оговорка, если бот на python-telegram-bot или aiogram: их HTTP-клиент (httpx или aiohttp) на уровне INFO пишет URL каждого запроса, а в URL - токен бота открытым текстом, и он попадает в journalctl. Кинули потом кому-то лог за помощью - отдали и токен. Заглушите логгер в коде в первую очередь: logging.getLogger("httpx").setLevel(logging.WARNING) (или "aiohttp"). Проверено на живом сервере - без этой строки каждый вызов getUpdates печатает полный URL api.telegram.org/bot<токен>/....
Два способа получать сообщения. При long polling бот сам, в цикле, спрашивает у Телеграма новые апдейты - наружу открывать ничего не нужно. При webhook Телеграм сам присылает апдейты на ваш HTTPS-адрес: задержка ниже, но нужен домен, сертификат и открытый порт.
Long polling | Webhook | |
|---|---|---|
Настройка | Ничего - бот сам ходит в Телеграм | Обычно HTTPS-домен и обратный прокси (можно и IP с самоподписанным сертификатом, редко) |
Входящие порты | Не нужны | 443 (или 80, 88, 8443), всегда по TLS |
Задержка | Низкая - апдейт приходит по открытому | Низкая - Телеграм сам делает входящий запрос |
Соединение | Бот держит исходящий | Телеграм делает входящие HTTPS-запросы к вам |
Кому подходит | Простому боту, один экземпляр, домена нет | Нескольким экземплярам/сервисам, веб-архитектуре с масштабированием |
Начинайте с long polling. Ему не нужен ни домен, ни открытые порты; задержка при этом не обязательно выше, чем у webhook - Телеграм отдаёт апдейт сразу по уже открытому запросу getUpdates. На webhook переходят из-за архитектуры (несколько экземпляров бота за балансировщиком, общий домен с другими сервисами), а не потому что polling «медленный». Тогда перед ботом ставят обратный прокси - Caddy или Nginx - и регистрируют адрес через метод setWebhook. Порт для webhook - обычно 443, допустимы также 80, 88 и 8443, но даже на порту 80 соединение должно идти по TLS, это не обычный HTTP.
Забрать новый код, доустановить зависимости, перезапустить службу - всё от имени mybot:
sudo -H -u mybot git -C /opt/mybot pull
sudo -H -u mybot /opt/mybot/.venv/bin/pip install -r /opt/mybot/requirements.txt
systemctl restart mybot
После systemctl restart загляните в journalctl -u mybot -f и убедитесь, что бот поднялся и отвечает.
journalctl -u mybot -b (логи текущей загрузки; предыдущая - -b -1). Обычно это неверный токен, недостающая зависимость или сеть не поднялась к моменту старта. После правки - systemctl restart mybot.409 Conflict: terminated by other getUpdates request; make sure that only one bot instance is running. Один токен опрашивают сразу два экземпляра - например, забытый ручной запуск из шага 3 или второй сервер. Опрашивать может только кто-то один, механизма «перехвата» нет - единственное решение это «ровно один экземпляр». Лишний остановить. Одна-две ошибки 409 сразу после systemctl restart - это норма и проходит само: длинный запрос старого экземпляра ещё несколько секунд висит на стороне Телеграма, потом новый забирает опрос. В проде не сбрасывайте накопленные апдейты (это поведение по умолчанию), тогда Телеграм после рестарта до-доставит очередь примерно за сутки. Воспроизведено на живом сервере: второй поллер на токене работающей службы сразу получает HTTP 409 и telegram.error.Conflict, дальше оба экземпляра толкаются, пока один не выйдет.TZ= в env-файле для кода или задайте пояс всей машине через timedatectl set-timezone.Restart=always её маскирует. Пока ищете причину, добавьте в unit RuntimeMaxSec=86400 - раз в сутки служба перезапустится начисто. Отсчёт идёт от последнего запуска, а не от круглого часа, поэтому момент перезапуска будет плавать; нужен точный час - это уже systemd.timer. Как смотреть память и нагрузку на самом сервере - в разборе диагностики Linux-сервера.Тип бота | vCPU / RAM |
|---|---|
Текстовые команды, небольшие группы | 1 / 512 МБ |
Inline-запросы, база данных, несколько тысяч пользователей | 1 / 1 ГБ |
Обработка медиа, конвертация картинок или аудио | 2 / 2 ГБ и запас по диску |
У большинства ботов CPU простаивает, и следить стоит только за RAM. Подробнее - в разборе сколько CPU и RAM реально вам нужно.
Поставить его на VPS и запустить как службу systemd с Restart=always. systemd запускает бота при загрузке сервера и поднимает за секунды после падения; токен лежит в env-файле, логи идут в journalctl.
Самый маленький VPS. 1 vCPU и 512 МБ-1 ГБ RAM тянут большинство ботов (CPU почти простаивает). 2 vCPU и больше памяти нужно только ботам с тяжёлым медиа.
Создать unit systemd, где ExecStart абсолютными путями указывает на Python из виртуального окружения и на скрипт бота, User= - на отдельную учётную запись, секреты грузятся через EnvironmentFile, а Restart=always держит бота живым. Затем systemctl enable --now mybot.
Для старта - long polling: ни домена, ни открытых портов, хватает на тысячи пользователей. На webhook переходить только тогда, когда задержка или объём апдейтов становятся реальным ограничением.
Да. Отдельный пользователь, отдельный каталог и отдельный unit systemd на каждого бота. VPS на 1 ГБ спокойно несёт горсть небольших ботов.
systemd с Restart=always - в этом и есть вся суть «24/7».chmod 600, никогда в коде.journalctl -u mybot, отдельные лог-файлы вести не нужно.