Один рабочий приём для Telegram-бота, Discord-бота, n8n или скрипта на Python: оформить как сервис systemd, а не запускать в терминале, который вы закроете. И проверить, что он пережил перезагрузку.
Бот написан, скрипт работает, тест пройден - и тут встаёт вопрос, ради которого вы вообще открыли эту статью: как развернуть сервис на сервере так, чтобы он не остановился в тот момент, когда вы закроете терминал. Ниже - один рабочий приём, который годится и для телеграм-бота, и для discord-бота, и для скрипта на Python, и для n8n: превратить процесс в сервис systemd. Дальше - подготовка сервера, код, unit-файл, проверка, что всё поднимается само после перезагрузки, и разбор long polling против webhook для тех, кто пишет ботов.
Коротко. Арендуйте VPS, заведите отдельного пользователя без прав администратора, код клонируйте в подкаталог внутри его домашнего каталога - не в сам домашний каталог, там уже лежат файлы из
/etc/skel, - а секрет держите в отдельном.envрядом. Опишите запуск в unit-файле systemd - файле вида/etc/systemd/system/имя.service- сRestart=alwaysиRestartSec=5, включите автозапуск командойsystemctl enable --now. Проверьте статус (systemctl status), живые логи (journalctl -u имя -f) и, главное, перезагрузите сервер и убедитесь, что сервис поднялся сам. Для Telegram-ботов начинайте с long polling - он не требует домена и сертификата, и задержка у него не обязательно выше, чем у webhook. Docker Compose подключайте не раньше, чем сервисов станет несколько.
Ноутбук выключается, роутер перезагружается после обновления прошивки, Wi-Fi падает на пять минут - и бот, который вы только что успешно потестировали, молчит. VPS (Virtual Private Server, виртуальный выделенный сервер) - это чужая машина в дата-центре, которая работает круглосуточно и не уходит в сон, когда вы закрываете крышку ноутбука. У неё обычно постоянный публичный IP-адрес или другой стабильный сетевой адрес - конкретно зависит от провайдера и тарифа, бывают и NAT, и IPv6-only варианты, - плюс постоянное подключение и в среднем более стабильный канал, чем домашний интернет.
Для одного бота на long polling или небольшого скрипта не нужен мощный сервер. Тариф за 2-3 доллара в месяц с 1 vCPU и 1 ГБ памяти - рабочая отправная точка: у многих провайдеров ресурсы позже можно нарастить без переустановки ОС, но это правило конкретного хостера и тарифа, а не свойство виртуализации самой по себе - уточняйте правила апгрейда заранее. Ориентир по памяти для типичных фоновых сервисов - в таблице ниже, но это иллюстративные диапазоны, а не гарантия для конкретного случая: реальный расход зависит от языка, библиотеки и того, что именно делает код.
Сервис | Типичный расход памяти |
|---|---|
Бот на long polling (Python/Node) | ~60-150 МБ, сильно зависит от библиотеки и языка |
Uptime Kuma (мониторинг) | ~80-150 МБ |
n8n (несколько сценариев) | ~200-400 МБ |
Небольшой API (FastAPI, Express) | ~100-250 МБ |
Из этого следует практический вывод: на сервере с 1 ГБ памяти бот на long polling и лёгкий мониторинг вроде Uptime Kuma уживаются вместе с запасом. А вот тяжёлые вещи - локальная языковая модель, сборка фронтенда, headless-браузер для автоматизации браузера - на такую машину не поедут, там счёт памяти идёт на гигабайты, а не на сотни мегабайт.
Если сервера ещё нет - есть пошаговая инструкция, как создать сервер, с созданием аккаунта, выбором тарифа и первым подключением по SSH. Дальше в статье считаем, что VPS уже арендован и вы можете зайти на него по SSH под root или sudo-пользователем.
Первое, что стоит сделать - не запускать бота от root. Если в коде найдётся дыра или утечёт токен, атакующий получит права того пользователя, от которого работал процесс. Заведите отдельного системного пользователя: без пароля, без входа в интерактивную оболочку, только для того, чтобы под ним жил сервис.
sudo useradd --system --create-home --home-dir /opt/mybot --shell /usr/sbin/nologin botuser
--system - создать системную учётную запись, а не обычного пользователя с UID для входа.--create-home --home-dir /opt/mybot - домашний каталог сразу там, где будет жить код, а не в /home.--shell /usr/sbin/nologin - запрещает интерактивный вход в систему под этим пользователем: он существует только для процесса.Проверьте, что пользователь создан и владеет своим каталогом.
id botuser
ls -ld /opt/mybot
Что вы должны увидеть. id botuser печатает строку с UID и GID без ошибки no such user. ls -ld /opt/mybot показывает владельца botuser botuser - если вместо этого там root root, поправьте владельца командой sudo chown -R botuser:botuser /opt/mybot.
--create-home на шаге 1 не просто создал пустой каталог /opt/mybot - он скопировал в него содержимое /etc/skel (.bashrc, .profile и подобное), это штатное поведение useradd. Каталог уже не пустой, и если склонировать репозиторий прямо в него, будет вот такая ошибка (проверено на реальном сервере):
fatal: destination path '/opt/mybot' already exists and is not an empty directory.
Поэтому код кладём в отдельный подкаталог app, а .env с секретом оставляем уровнем выше - код и секрет физически разделены:
/opt/mybot/
├── .env
└── app/
├── bot.py
├── requirements.txt
└── venv/
На свежем VPS git может быть не установлен - ставим его вместе с python3-venv сразу, одной командой:
sudo apt update
sudo apt install -y git python3-venv
Дальше доставляем код - через git clone, если репозиторий есть, или через scp, если проще скопировать файлы с локальной машины. Делаем это от имени сервисного пользователя, чтобы сразу не путаться с правами.
sudo -u botuser git clone https://github.com/you/mybot.git /opt/mybot/app
Если проект на Python, зависимости лучше ставить не в системный Python, а в виртуальное окружение - изолированный набор пакетов для конкретного проекта, который не конфликтует с тем, что уже стоит в системе и что нужно другим программам на этом сервере. На минимальных образах Ubuntu и Debian модуля venv может не быть в базовой поставке. Проверено на чистом Ubuntu 24.04: команда без установки пакета падает именно так -
The virtual environment was not created successfully because ensurepip is not
available. On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.
apt install python3.12-venv
- родовой пакет python3-venv (без привязки к версии, уже поставлен командой выше) ставит то же самое и тоже подтверждён рабочим:
cd /opt/mybot/app
sudo -u botuser python3 -m venv venv
sudo -u botuser ./venv/bin/pip install -r requirements.txt
python3 -m venv venv - создаёт каталог venv со своим интерпретатором и своим набором пакетов../venv/bin/pip install -r requirements.txt - ставит зависимости именно в это окружение, не в систему.Токен бота, ключ API или пароль от базы данных не пишите прямо в код - если репозиторий когда-нибудь станет публичным или попадёт не в те руки, секрет уйдёт вместе с ним. Положите такие значения в файл .env уровнем выше каталога с кодом - обычный текстовый файл вида ПЕРЕМЕННАЯ=значение, который сервис прочитает при старте. Создавайте его так, а не через echo с токеном прямо в команде - иначе токен на какое-то время осядет в истории команд шелла:
sudo -u botuser touch /opt/mybot/.env
sudo chmod 600 /opt/mybot/.env
sudo -u botuser nano /opt/mybot/.env
Впишите в открывшемся редакторе строку вида BOT_TOKEN=ваш_токен и сохраните. chmod 600 оставляет чтение и запись только владельцу - botuser; другие непривилегированные пользователи файл не прочитают (root, разумеется, может - от него на одной машине секрет не спрятать). touch не трогает файл, если он уже существует - в отличие от команды вида install ... /dev/null ..., которая при повторном запуске (например, если вы вернётесь к этому шагу через месяц) молча обнулит уже заполненный .env и сотрёт токен.
Для небольшого одноцелевого VPS .env с правами 600 - практичный базовый вариант. Если модель угроз строже (общий сервер, несколько администраторов, чувствительные production-секреты), передавайте секреты через LoadCredential= systemd или отдельный секрет-менеджер - переменные окружения для этого не лучший механизм, об этом прямо сказано в документации systemd.
systemd - штатная система запуска сервисов в Linux: она умеет стартовать процесс при загрузке машины, следить за ним и поднимать заново, если он упал. Вместо того чтобы вручную набирать python bot.py в терминале - процесс, запущенный так, может завершиться при разрыве терминала и в любом случае остаётся без автозапуска после перезагрузки, автоматического рестарта и централизованных логов - опишите запуск в unit-файле: текстовом конфиге, который читает systemd.
sudo nano /etc/systemd/system/mybot.service
Вставьте в него следующее, подставив свои пути и имя пользователя:
[Unit]
Description=My bot service
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=exec
User=botuser
WorkingDirectory=/opt/mybot/app
EnvironmentFile=/opt/mybot/.env
ExecStart=/opt/mybot/app/venv/bin/python /opt/mybot/app/bot.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
After=network-online.target и Wants=network-online.target откладывают старт до момента, когда сетевой менеджер считает сеть настроенной, а не сразу после загрузки ядра. На проверенном HIP-образе (Ubuntu 24.04) за это отвечает включённый systemd-networkd-wait-online.service, так что зависимость реально участвует в загрузке. Но «сеть настроена» не значит «работает DNS» или «Telegram отвечает» - код всё равно должен уметь пережить сетевую ошибку и повторить попытку, а не падать намертво.User=botuser - от чьего имени работает процесс. Не root - см. шаг 1.WorkingDirectory=/opt/mybot/app - рабочий каталог с кодом, откуда процесс видит свои относительные пути к файлам; секрет в .env лежит уровнем выше и подключается отдельно.EnvironmentFile=/opt/mybot/.env - подгружает переменные окружения из файла с секретами. Без токена в BOT_TOKEN сервис бессмыслен, поэтому путь указан без ведущего дефиса: если файл пропадёт, будет переименован или потеряет права на чтение, systemd сразу откажет в запуске с понятной ошибкой, а не тихо стартует бота без токена. Проверено на реальном сервере - при отсутствующем файле без дефиса в логе видна ровно такая строка: Failed to load environment files: No such file or directory. Дефис (EnvironmentFile=-путь) уместен только когда файл с переменными действительно необязателен.ExecStart - точная команда запуска, с полным путём до интерпретатора из виртуального окружения - так гарантированно используются нужные библиотеки, а не системный Python.Type=exec - для долгоживущих сервисов современный systemd рекомендует именно его, а не Type=simple: systemd считает сервис запущенным только после успешного exec(), поэтому ошибку вроде отсутствующего или неисполняемого файла в ExecStart видно сразу, а не «запустился и тут же упал».Restart=always - поднимать процесс заново при любом самостоятельном завершении: и при падении с ошибкой, и при обычном выходе. Исключение - явная команда systemctl stop: после неё systemd не перезапускает сервис, это осознанная остановка, а не сбой. Для бота, который должен работать постоянно и возвращаться даже после неожиданного штатного выхода (exit 0), Restart=always - разумный выбор. В общем случае для долгоживущих сервисов современный systemd рекомендует Restart=on-failure; always берут, когда сервис вообще не должен завершаться сам.RestartSec=5 - пауза в 5 секунд перед каждым перезапуском, чтобы не долбить упавший процесс без остановки.StartLimitIntervalSec=60 и StartLimitBurst=5 задают явный лимит запусков для этого сервиса: не больше пяти попыток за 60 секунд, а следующую попытку в этом же окне systemd отклоняет и помечает юнит как failed. У systemd есть и глобальный лимит на случай, если эти строки вообще не прописывать - на проверенном сервере это DefaultStartLimitIntervalUSec=10s и DefaultStartLimitBurst=5, - но его значения зависят от конфигурации менеджера и могут отличаться на другой машине. Явные параметры в unit-файле делают поведение предсказуемым независимо от системных настроек. Проверено на реальном сервере с уменьшенными лимитами (StartLimitIntervalSec=20, StartLimitBurst=3) - после третьей попытки запуска подряд systemd действительно отказывает следующей, и в journalctl видна ровно такая строка: Start request repeated too quickly, следом Failed with result 'exit-code'.После правки unit-файла systemd нужно перечитать его, включить автозапуск при загрузке и сразу запустить сервис - это делает одна команда с флагом --now.
sudo systemctl daemon-reload
sudo systemctl enable --now mybot
daemon-reload - обязателен после любого изменения файла в /etc/systemd/system, иначе systemd продолжит работать со старой версией юнита из памяти.enable --now - одновременно включает автозапуск сервиса при загрузке системы (enable) и стартует его прямо сейчас (--now).Мало запустить сервис - нужно убедиться, что он действительно работает, а не тихо упал через секунду после старта. Начните со статуса.
systemctl status mybot
Что вы должны увидеть. Строку Active: active (running) зелёным цветом и время, сколько сервис уже работает. Если вместо этого failed или activating (auto-restart) по кругу - смотрите логи.
journalctl -u mybot -f
-f держит вывод открытым и показывает новые строки логов в реальном времени, как tail -f. Дайте боту прислать сообщение или дёрните его командой в мессенджере и убедитесь, что в логе нет повторяющихся ошибок и рестартов. Остановить просмотр - Ctrl+C.
Самая надёжная практическая проверка - переживёт ли сервис перезагрузку сервера, а не только вашу текущую SSH-сессию.
sudo reboot
Подождите, пока сервер вернётся в сеть - на лёгком KVM-тарифе это обычно занимает секунды, на прогретой под нагрузкой машине может уйти дольше минуты, - подключитесь по SSH заново и повторите systemctl status mybot. Что вы должны увидеть. Снова active (running), но уже без вашего участия - именно это доказывает, что enable сработал и сервис действительно автономен, а не просто запущен вами вручную поверх systemd.
Если вы разворачиваете именно Telegram-бота, Bot API даёт два взаимоисключающих способа получать обновления: long polling и webhook. (Для Discord это устроено иначе - события приходят в реальном времени через отдельное WebSocket-соединение, Gateway, а не через эту пару режимов; всё остальное в статье - подготовка сервера, systemd, обновление без простоя - одинаково подходит и ему.) Long polling - бот раз за разом вызывает getUpdates с положительным таймаутом: если обновление уже есть, Telegram возвращает его сразу же по уже открытому запросу; если обновлений нет, запрос просто держится открытым до истечения таймаута, и уходит следующий. Webhook - наоборот, Telegram сам присылает POST-запрос на ваш сервер в момент события, и для этого сервер должен быть доступен снаружи по HTTPS.
Начинайте с long polling: не нужен домен, TLS-сертификат и открытый порт под входящие запросы - бот работает даже за NAT, лишь бы у него был исходящий доступ в интернет, а задержка при этом не обязательно выше, чем у webhook. На webhook переходят не потому что polling медленный, а из-за архитектуры: несколько инстансов бота за балансировщиком, общий домен с другими сервисами, нужен предсказуемый профиль входящей нагрузки. Тогда webhook добавляет постоянную инфраструктурную обвязку - обычно домен, сертификат и обратный прокси (технически Telegram принимает webhook и на IP с самоподписанным сертификатом, и приложение может терминировать TLS само, но на практике так делают редко). И даже разрешённый порт 80 для webhook всё равно должен работать по TLS, это не обычный HTTP. Всё это - оправданная цена за нужную архитектуру, а не следствие того, что long polling «слишком медленный».
Параметр | Long polling | Webhook |
|---|---|---|
Домен и сертификат | не нужны | нужен HTTPS-эндпоинт |
Задержка ответа | низкая - Telegram возвращает обновление сразу по уже открытому запросу | низкая - Telegram сам делает входящий запрос |
Соединение | бот держит исходящий | Telegram делает входящие HTTPS-запросы к вам |
Нагрузка | долгоживущий исходящий | запрос приходит только когда есть событие |
Когда выбирать | простой бот, один инстанс, домена ещё нет | несколько инстансов или сервисов, нужна веб-архитектура и масштабирование |
Если переходите на webhook, Telegram отправляет обновления на конкретный порт вашего сервера - Bot API поддерживает для этого порты 443, 80, 88 и 8443 (сверено с официальной документацией на 5 сентября 2026). Дальше запрос обычно принимает обратный прокси вроде Caddy или Nginx и передаёт его вашему процессу на localhost, как с любым другим веб-приложением.
Для одного процесса на одном языке Docker не обязателен - systemd прекрасно справляется сам, без лишнего слоя контейнеризации. Переходить на Docker и Docker Compose стоит, когда сервисов становится несколько и у них разные, иногда конфликтующие зависимости (одному нужен Node.js 18, другому - 22), либо когда важно, чтобы конфигурацию можно было перенести на другой сервер одной командой, не собирая окружение заново вручную.
Современный Docker Compose - это не отдельная утилита docker-compose через дефис, а плагин Docker CLI, вызывается как docker compose, без дефиса. Название пакета зависит от того, откуда ставите Docker. В штатном репозитории Ubuntu 24.04 пакет называется docker-compose-v2 и на сентябрь 2026 содержит Compose 2.40.3 (проверено вживую):
sudo apt install docker.io docker-compose-v2
docker compose version
Если подключаете официальный репозиторий Docker (обычно ради более свежей версии - у Docker Compose там уже ветка 5.x), пакет с тем же смыслом называется иначе - docker-compose-plugin. Команда в обоих случаях остаётся docker compose. Названия пакетов легко перепутать - перед установкой посмотрите, что реально видит apt:
apt-cache policy docker-compose-v2 docker-compose-plugin
С Debian ещё пестрее: в Debian 12 (Bookworm) штатный репозиторий даёт версию 1.29.2-3 - старую первую ветку Compose; в Debian 13 (Trixie) - уже 2.26.1-4, где Compose ставится в том числе как CLI-плагин Docker (/usr/libexec/docker/cli-plugins/docker-compose), так что docker compose там тоже работает (сверено по пакетному индексу Debian). Единой команды на все перечисленные дистрибутивы дать нельзя - смотрите вывод apt-cache policy для своей системы.
Что вы должны увидеть. Строку Docker Compose version ... с номером версии (из штатной Ubuntu - 2.40.x, из репозитория Docker - 5.x). Дальше несколько сервисов описываются в одном файле compose.yaml. Здесь важная деталь для темы этой статьи: Docker-демон и правда стартует через systemd после перезагрузки, но по умолчанию у Compose restart: "no" - и контейнеры сами обратно НЕ поднимутся. Чтобы сервис пережил reboot, каждому контейнеру нужна политика перезапуска:
services:
app:
image: your/image:tag
restart: unless-stopped
Проверено на реальном сервере: контейнер без restart: после перезапуска демона остаётся в состоянии Exited, с restart: unless-stopped - сам возвращается в Up. Поднимается всё одной командой docker compose up -d, отдельный unit-файл для каждого контейнера писать не нужно.
Для подавляющего большинства фоновых сервисов - бота, скрипта, одного небольшого API - обновление выглядит просто: остановили старую версию кода, подтянули новую, перезапустили сервис.
cd /opt/mybot/app
sudo -u botuser git pull
sudo -u botuser ./venv/bin/pip install -r requirements.txt
sudo systemctl restart mybot
systemctl restart останавливает и заново запускает процесс - простой длится ровно столько, сколько ваше приложение реально останавливается и стартует заново: от долей секунды до нескольких секунд и больше, в зависимости от того, что оно делает при старте (подключается к базе, прогревает кэш и т.п.). Для бота или личного сервиса это обычно не проблема.
Если нужен по-настоящему нулевой простой, способ зависит от типа сервиса. Для HTTP-сервиса или API работает blue/green: поднимают новую версию на соседнем порту и переключают на неё обратный прокси, когда она готова принимать трафик, а старую версию гасят следом. Для Telegram-бота на long polling так делать нельзя: активный getUpdates может быть только один на токен, и если старая и новая копии заработают одновременно, Telegram ответит той самой ошибкой 409 Conflict . Для такого бота вариантов два: смириться с коротким перерывом на systemctl restart или спроектировать бота под webhook с аккуратным переключением. Для одного сервера и одного процесса blue/green почти всегда лишняя сложность - несколько секунд даунтайма при рестарте обычно дешевле.
Не запускайте его вручную командой в терминале - процесс может завершиться при разрыве терминала (это зависит от shell, job control и того, как именно он запущен), и в любом случае у него нет нормального управления жизненным циклом: автозапуска после перезагрузки, автоматического перезапуска при падении, централизованных логов. Оформите его как сервис systemd - это всё даёт сразу. nohup и screen держат процесс живым при закрытии терминала, но это временное решение для теста, а не постоянная эксплуатация.
Это указание systemd поднимать процесс заново каждый раз, когда он завершился сам - неважно, с ошибкой или штатным выходом, - кроме явной остановки командой systemctl stop: после неё сервис не перезапускается, проверено на реальном сервере. Для фонового сервиса, который должен работать постоянно, Restart=always - правильный режим; для разовой задачи вместо него используют Restart=on-failure. Добавьте RestartSec=5, чтобы между попытками была пауза, и StartLimitIntervalSec/StartLimitBurst в секции [Unit], чтобы задать предсказуемый предел попыток запуска.
Для бота на long polling обычно хватает: расход памяти на практике держится в районе 60-150 МБ, но это сильно зависит от языка и библиотеки, а не строгая гарантия. На той же машине с 1 ГБ памяти уживётся ещё лёгкий сервис вроде Uptime Kuma. А вот тяжёлые вещи - локальная языковая модель, сборка фронтенда, headless-браузер - на такой конфигурации не поедут, там нужны гигабайты, а не сотни мегабайт.
Для Telegram - начинайте с long polling: не нужен домен, сертификат и открытый порт, бот сам ходит за обновлениями, а задержка при этом не обязательно выше, чем у webhook. На webhook переходят из-за архитектуры - несколько инстансов бота, общий домен с другими сервисами, - а не потому что polling медленный. Для большинства личных и небольших ботов long polling достаточно. Для Discord это неприменимо - там события идут через отдельный Gateway/WebSocket, а не через эту пару режимов.
systemctl status имя должен показывать active (running), а journalctl -u имя -f - живой поток логов без повторяющихся ошибок и рестартов по кругу. Для сетевого сервиса проверьте, что порт слушается: ss -tlnp | grep порт (в урезанных облачных образах утилиты может не быть из коробки - тогда ставится из пакета iproute2). Финальная проверка - перезагрузить сервер и убедиться, что сервис поднялся сам, без вашего участия.
Не обязательно. Один процесс на своём языке прекрасно живёт под systemd без контейнеров вообще. Docker и Docker Compose оправданы, когда сервисов несколько, у них разные зависимости или нужен предсказуемый перенос между машинами - это два разных подхода, а не обязательная ступень, через которую надо пройти в любом случае.
Для большинства фоновых сервисов достаточно systemctl restart - короткий перерыв, длина зависит от времени старта приложения. По-настоящему нулевой простой делают по-разному: для HTTP-сервиса - новая версия на другом порту плюс переключение обратного прокси; для Telegram-бота на long polling так нельзя (два одновременных getUpdates на один токен дают 409 Conflict), там либо короткий рестарт, либо архитектура на webhook. Для личного проекта или небольшого бота полноценная схема с репликами почти всегда избыточна.
.env-файл с секретами - обязательная гигиена, а не опция.Restart=always плюс RestartSec=5 поднимают упавший процесс; StartLimitIntervalSec/StartLimitBurst останавливают бесконечный цикл рестартов, если процесс падает раз за разом.systemctl status.restart: unless-stopped, иначе после перезагрузки они сами не поднимутся.Нужен дешёвый сервер, который просто работает - тарифы HIP от $2.40/мес, SSD серверного класса, установка меньше минуты. Если решаете, где физически разместить сервис под низкую задержку, посмотрите локацию в Хельсинки.