Как держать Телеграм-бота онлайн 24/7 на VPS

Ноутбук засыпает, бесплатные тарифы усыпляют процесс. Разбираем, как поднять бота на VPS так, чтобы он сам вставал после падения и перезагрузки - с объяснением каждой команды.

Что нужно: VPS с Ubuntu 22.04 или 24.04 (1 vCPU и 512 МБ-1 ГБ RAM с запасом хватит), токен бота от @BotFather и минут пятнадцать. На выходе: бот переживает перезагрузки, падения и обрывы сети, а логи можно нормально читать.

Весь путь одной строкой: код кладётся в /opt/mybot под отдельным пользователем, рядом собирается виртуальное окружение, токен хранится в env-файле, для запуска пишется unit systemd с Restart=always, потом systemctl enable --now mybot. Логи смотрятся в journalctl -u mybot. Всё ниже - то же самое, но по шагам и с объяснениями.

Почему VPS, а не ноутбук и не бесплатный тариф

Бот на long polling (сам, в цикле, спрашивает у Телеграма новые сообщения) должен быть на связи каждую секунду: нет процесса - нет ответов. Ноутбук для этого не годится: засыпает, теряет Wi-Fi, уходит в перезагрузку ради обновлений. Бесплатные тарифы «always-on» для веб-сервисов усыпляют процесс обычно через 15-30 минут без входящих HTTP-запросов, а у бота на long polling их нет вообще - он засыпает и просыпается с задержкой. VPS за 2-3 доллара просто работает. Вдобавок вы получаете нормальную систему запуска служб (шаг 4), и «держать бота живым» становится одним конфигом вместо набора костылей. Бот здесь - частный случай общего приёма «запустить любой процесс как службу systemd»; если тот же подход понадобится для не-бота, есть отдельный разбор как развернуть сервис на сервере и держать его 24/7.

Что понадобится

  • VPS с чистой Ubuntu 22.04 или 24.04 и доступом root (или пользователь с правами sudo).
  • Токен бота от @BotFather в Телеграме - строка вида 123456:AA....
  • Код бота в git-репозитории или готовый к загрузке на сервер.

Все команды ниже выполняются от root. Если вы вошли обычным пользователем, добавляйте перед ними sudo.

Сначала защитите сервер. На боевой машине первым делом сделайте минимум: заведите пользователя с sudo вместо root, включите вход по SSH-ключу и отключите вход по паролю, поднимите файрвол, который пускает только SSH (и порт 443, если позже пойдёте через webhook). Подробности - в статье «Защита свежего VPS».

Шаг 1. Пользователь, код и виртуальное окружение

Первым делом ставим системные пакеты: 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).

Шаг 2. Токен - вне кода

Токен - это пароль от бота: кто им владеет, тот управляет ботом. Поэтому в исходниках, тем более в публичном репозитории, ему не место. Держим его в 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.

Шаг 3. Первый запуск вручную

Перед тем как оформлять бота в службу, запустите его руками - убедиться, что он стартует и отвечает. Наивный вариант 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 и переходите к службе. Молчит или сыплет ошибками - чините сейчас: обёртка-служба сломанного бота не починит, она будет только перезапускать его по кругу.

Шаг 4. Служба systemd

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 на живом сервере.

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

Шаг 5. Логи и состояние службы

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

Два способа получать сообщения. При long polling бот сам, в цикле, спрашивает у Телеграма новые апдейты - наружу открывать ничего не нужно. При webhook Телеграм сам присылает апдейты на ваш HTTPS-адрес: задержка ниже, но нужен домен, сертификат и открытый порт.

Long polling

Webhook

Настройка

Ничего - бот сам ходит в Телеграм

Обычно HTTPS-домен и обратный прокси (можно и IP с самоподписанным сертификатом, редко)

Входящие порты

Не нужны

443 (или 80, 88, 8443), всегда по TLS

Задержка

Низкая - апдейт приходит по открытому getUpdates

Низкая - Телеграм сам делает входящий запрос

Соединение

Бот держит исходящий getUpdates

Телеграм делает входящие 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, дальше оба экземпляра толкаются, пока один не выйдет.
  • Задачи по расписанию срабатывают не в тот час. Сервер живёт по UTC. Пропишите 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 реально вам нужно.

Вопросы и ответы

Как держать Телеграм-бота онлайн 24/7?

Поставить его на 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.

Webhook или long polling?

Для старта - long polling: ни домена, ни открытых портов, хватает на тысячи пользователей. На webhook переходить только тогда, когда задержка или объём апдейтов становятся реальным ограничением.

Можно ли держать несколько ботов на одном VPS?

Да. Отдельный пользователь, отдельный каталог и отдельный unit systemd на каждого бота. VPS на 1 ГБ спокойно несёт горсть небольших ботов.

Коротко

  • VPS плюс служба systemd с Restart=always - в этом и есть вся суть «24/7».
  • Бот работает под отдельным непривилегированным пользователем; токен - в env-файле с chmod 600, никогда в коде.
  • Сначала long polling; webhook - только когда перерастёте.
  • Логи - journalctl -u mybot, отдельные лог-файлы вести не нужно.
  • Хватит самого дешёвого VPS; единственное число, за которым стоит следить, - это RAM.
PUBLISHED
AUTHOR
HIP-HOSTING
LANGUAGES
EN · RU