Мы много пишем о ботах, AI-агентах и других сервисах, которые делают жизнь и работу легче. Для учебных задач все эти штуки допустимо и удобно запускать на локальной машине — буквально без регистраций и смс. К тому же не нужно платить за сервер.
Если вы пишете Telegram-бота или AI-агента для реальных задач, держать его на ноутбуке неудобно: процесс уснёт вместе с системой или пропадёт после перезагрузки. Поэтому сегодня поговорим о том, как задеплоить свои сервисы на VPS, чтобы они работали круглые сутки. Мы пройдём весь путь: выберем сервер, подключимся к нему, закроем дыры в безопасности, упакуем агента в Docker, настроим автозапуск, мониторинг и бэкапы. В конце у вас будет AI-агент, который переживёт перезагрузку сервера, падение процесса и попытку потенциального хакера постучаться в открытый порт.
Если своего агента у вас пока нет — ничего страшного, дальше будет ссылка на материал, где его собирают с нуля. А здесь мы сосредоточимся на развёртывании. Статья получилась подробная, так что запаситесь терпением, снеками и напитками.
ВАМ ПРИШЛО ПРИГЛАШЕНИЕ 💌
Приходите к нам в соцсети поделиться своим мнением и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!
Что нужно, чтобы перейти от локального скрипта к продакшену
Локальный скрипт на ноутбуке живёт ровно до того момента, пока включён ноутбук. Закрыли крышку — процесс уснул вместе с системой. Прилетело обновление с перезагрузкой — скрипт не запустится обратно сам. Выключили свет — и всё, бот пропал из эфира, пока вы не вернётесь и не запустите его руками. Для пет-проекта это терпимо, но как только вы хотите, чтобы агент реально работал в любое время, нужна машина, которая не выключается. Эту роль и берёт на себя VPS — виртуальный сервер, который крутится в дата-центре и работает 24/7.
Но перенести агента на сервер — это полдела. Еще полдела — защитить агента от неприятностей: перезагрузок сервера, падений, ботов-сканеров и прочих сетевых неурядиц. Результат, к которому мы придём — AI-агент на VPS, который сам поднимается после перезагрузки, перезапускается после падения и не сливает секреты наружу.
Если вы ещё не написали самого агента, начните с материала “Как собрать AI-парсер вакансий — пошагово, с кодом” — там разбираем, как он устроен внутри. Из того материала понадобится файл agent.py, сохраните user ID, токены для телеграм-бота и API-ключа OpenRouter (что это, зачем и откуда брать — мы там же написали). Если вы уже собрали бота и успешно запустили локально, то идем дальше.

Выбираем VPS
Если погуглить «какой VPS выбрать для AI-агента», выдача завалит вас рейтингами хостингов с огромными таблицами и звёздочками. Значительная часть таких публикаций — это партнёрские подборки: автор получает вознаграждение за переход по ссылке, поэтому цифры в баннере там важнее, чем технические критерии. Поэтому давайте сами разберёмся в критериях, а потом вы сможете выбрать провайдера под них.
Для Telegram-агента важны три вещи. Первая — стабильность сети: если у провайдера регулярно отваливается соединение, бот будет терять связь с Telegram и с источником вакансий, и часть уведомлений просто не дойдёт. Вторая — наличие автоматических бэкапов или снапшотов на стороне провайдера: они спасают, когда вы сломали что-то в системе и нужно откатиться на вчерашнее состояние одним кликом. Третья — возможность самому управлять firewall и SSH: некоторые сверхдешёвые тарифы дают урезанный доступ, где вы не можете нормально настроить безопасность.
А теперь о том, на что смотреть не нужно. Для бота, который раз в несколько минут опрашивает API и шлёт сообщения, не нужен GPU — на сервере не крутится тяжёлая нейросеть, всю модель за вас держит API провайдера ИИ. Не нужен и запас в 32 ГБ оперативной памяти: реальное потребление такого агента вместе с базой — это сотни мегабайт, а не десятки гигабайт. Минимальной конфигурации с 1–2 виртуальными ядрами и 1–2 ГБ оперативной памяти хватает с большим запасом. Узким местом почти всегда будет не сервер, а внешний API: лимиты на запросы упрутся раньше, чем закончатся ресурсы машины.
У кого именно арендовать сервер, не так важно. Если вы находитесь в России, попробуйте Yandex Cloud или VK Cloud — IT-гиганты часто дают новым пользователям скидки или другие плюшки. Если вы не в России, то рекомендуем Amazon Web Servers — новым пользователям Amazon даёт стартовые кредиты на эксперименты.
Подключение к серверу и первая защита
Мы исходим из того, что на вашем компьютере установлена ОС Linux. Если у вас есть только машина под управлением Windows, то предварительно установите WSL — инструмент от Microsoft, позволяющий запускать полноценное окружение Linux прямо внутри Windows. Установка описана в документации Microsoft.
Когда вы оплатили VPS, провайдер пришлёт IP-адрес сервера и доступ для входа — обычно это пользователь root с паролем или сразу с ключом. Первое подключение делается по SSH, протоколу для безопасной работы с удалённой машиной из терминала.
Заходить и работать под root каждый день не стоит: этот пользователь может в системе всё, и любая ваша опечатка в команде выполняется без вопросов и предохранителей. Поэтому сразу создаём отдельного непривилегированного пользователя, которому будем выдавать права администратора через sudo только по необходимости.
Зайдите на сервер под root. Для этого выполните:
ssh root@<ip-адрес сервера>ра>
Далее выполните:
# создаём нового пользователя deploy, система спросит пароль и пару вопросов
adduser deploy
# добавляем deploy в группу sudo, чтобы он мог выполнять команды с правами администратора
usermod -aG sudo deploy
Дальше настраиваем вход по ключам вместо пароля. SSH-ключ — это пара из приватного и публичного ключа: приватный лежит у вас на ноутбуке и никуда не уходит, а публичный кладётся на сервер. Подобрать пароль перебором боты умеют, а вот подобрать ключ практически невозможно. Сгенерируйте пару на своей машине (не на сервере) и скопируйте публичный ключ новому пользователю:
# генерируем пару ключей по современному алгоритму ed25519, на вопросы можно нажимать Enter
ssh-keygen -t ed25519 -C "deploy@vacancy-bot"
# копируем публичный ключ на сервер для пользователя deploy (введите его пароль)
ssh-copy-id deploy@203.0.113.10
Теперь зайдите под новым пользователем и убедитесь, что вход по ключу работает — система пустит вас без запроса пароля:
# подключаемся к серверу уже под deploy
ssh deploy@203.0.113.10

Когда вход по ключу налажен, можно закрутить гайки в конфигурации SSH-сервера. Откройте конфиг на сервере:
# открываем конфигурацию SSH-сервера в редакторе nano
sudo nano /etc/ssh/sshd_config
Найдите и приведите к такому виду четыре параметра (строки могут быть закомментированы знаком # — уберите его):
# меняем стандартный порт 22 на нестандартный, чтобы отсечь массовые сканеры
Port 2222
# запрещаем вход под root по SSH -- работаем только под deploy
PermitRootLogin no
# отключаем вход по паролю, оставляем только вход по ключу
PasswordAuthentication no
# явно разрешаем вход по публичному ключу
PubkeyAuthentication yes
Смена порта с 22 на нестандартный не делает сервер неприступным, но значительно снижает шум: автоматические сканеры долбятся в порт 22 тысячами попыток в сутки, и переезд на другой порт убирает почти весь этот мусор из логов. Отключение входа по паролю закрывает самый частый способ взлома — перебор пароля. А запрет входа под root означает, что даже если кто-то подберёт доступ, он попадёт в систему обычным пользователем без прав администратора.
Есть нюанс с Ubuntu 24.04: в ней портом SSH управляет не sshd_config, а отдельный системный сокет ssh.socket, поэтому простая смена Port в конфиге не сработает. Чтобы новый порт применился, переключите SSH на классический сервис:
# на Ubuntu 24.04 отключаем сокет, который слушает порт 22
sudo systemctl disable --now ssh.socket
# включаем обычный сервис SSH, который теперь возьмёт порт из sshd_config
sudo systemctl enable --now ssh.service
На Ubuntu 22.04 и более ранних достаточно перезапустить службу:
# применяем изменения конфигурации, перезапустив SSH-сервер
sudo systemctl restart ssh
Важный момент касаемо безопасности при любых правках SSH: не закрывайте текущую сессию, пока не проверите, что новая работает. Откройте второе окно терминала и подключитесь по новому порту:
# подключаемся по новому порту 2222 (флаг -p задаёт порт)
ssh -p 2222 deploy@203.0.113.10
Если новое окно пустило вас на сервер — всё хорошо, старую сессию можно закрывать. Если нет — у вас остаётся рабочая первая сессия, в которой можно откатить изменения. Этот приём убережёт вас от ситуации, когда неудачная настройка выкидывает с сервера без возможности вернуться.
Полезный блок со скидкой
Запустить AI-агента локально — это половина дела. Чтобы он работал по-настоящему, нужны сервер, Docker, переменные окружения, мониторинг, бэкапы и понимание, где у проекта слабые места.
Если хотите системно разобраться в Python, backend-разработке, инфраструктуре или нейросетях, у Практикума есть курсы под разные уровни. Промокод: KOD (можно просто нажать) даст скидку на платную программу.
Firewall — закрываем всё, кроме нужного
Firewall решает, какой сетевой трафик пускать на сервер, а какой отбрасывать. На Ubuntu для этого есть UFW — надстройка над системным механизмом фильтрации, с понятными командами. Логика простая: по умолчанию запрещаем все входящие соединения и точечно открываем только те порты, которые реально используем.
Перед включением firewall обязательно разрешите свой SSH-порт. Если этого не сделать и сразу включить UFW, сервер отбросит ваше же SSH-соединение, и вы потеряете доступ к машине — придётся идти в веб-консоль провайдера и разбираться оттуда. Поэтому порядок такой:
# по умолчанию запрещаем все входящие соединения
sudo ufw default deny incoming
# по умолчанию разрешаем все исходящие соединения (бот должен ходить наружу)
sudo ufw default allow outgoing
# разрешаем входящие на наш новый SSH-порт -- иначе после включения потеряем доступ
sudo ufw allow 2222/tcp
# включаем firewall (система переспросит -- отвечаем y)
sudo ufw enable
# смотрим, какие правила активны
sudo ufw status verbose

Для нашего агента этого набора правил достаточно: бот сам инициирует все соединения наружу, и снаружи к нему стучаться не нужно. Если позже вы добавите агенту веб-интерфейс, вы откроете порты 80 и 443 такими же командами ufw allow. Подробности по командам UFW есть в официальной документации Ubuntu.
Устанавливаем Docker и переносим агента на сервер
Docker упаковывает агента вместе со всеми его зависимостями в контейнер — изолированную коробку, которая одинаково запускается и на вашем ноутбуке, и на сервере.
Установим Docker скриптом из официальной документации:
# скачиваем и запускаем официальный установочный скрипт Docker
curl -fsSL https://get.docker.com | sudo sh
# добавляем своего пользователя в группу docker, чтобы не писать sudo перед каждой командой
sudo usermod -aG docker $USER
Чтобы добавление в группу docker применилось, нужно перезайти в сессию:
# выходим с сервера
exit
# и подключаемся снова, теперь группа docker действует
ssh -p 2222 deploy@203.0.113.10
Проверим, что Docker и Docker Compose на месте:
# выводим версию Docker
docker --version
# выводим версию Docker Compose (встроен как подкоманда)
docker compose version
Дальше переносим проект на сервер. Если код агента лежит в Git-репозитории, проще всего клонировать его прямо на сервере:
# клонируем репозиторий с агентом
git clone https://github.com/username/vacancy-bot.git
# переходим в папку проекта
cd vacancy-bot
Если репозитория нет, скопируйте папку проекта с локальной машины командой scp (выполняется на ноутбуке, не на сервере):
# копируем папку проекта на сервер по нашему SSH-порту (-P с большой буквы задаёт порт)
scp -P 2222 -r ./vacancy-bot deploy@203.0.113.10:~/
Теперь самое важное про секреты. Токен Telegram-бота, пароль от базы и ключи к внешним API нельзя хранить в коде и тем более отправлять в публичный репозиторий. Для этого их выносят в файл .env рядом с проектом и создают его прямо на сервере:
nano .env
# токен Telegram-бота, который вы получили у @BotFather
BOT_TOKEN=123456:AA-замените-на-свой
# имя пользователя базы данных
DB_USER=botuser
# пароль пользователя базы данных
DB_PASSWORD=надёжный-пароль
# имя базы данных
DB_NAME=vacancydb
И сразу защищаемся от самой частой утечки. Файл .env ни в коем случае не должен попасть в Git: если он окажется в публичном репозитории на GitHub, токены из него вытащат автоматические сканеры в течение нескольких минут — по сети постоянно ходят боты, которые ищут в свежих коммитах ключи и токены. Добавьте .env в список игнорируемых файлов:
# добавляем .env в .gitignore, чтобы Git его не отслеживал и не отправлял в репозиторий
echo ".env" >> .gitignore
Запускаем агента. Docker Compose описывает всю связку сервисов в одном файле: бота, базу данных, переменные окружения, тома и правила запуска. Минимальный docker-compose.yml для нашего бота с базой выглядит так:
# описываем сервисы нашего агента
services:
# сервис самого бота
bot:
# собираем образ из Dockerfile в текущей папке
build: .
# понятное имя контейнера
container_name: vacancy-bot
# секреты и настройки берём из файла .env
env_file: .env
# стартуем после контейнера с базой
depends_on:
- db
# сервис базы данных
db:
# официальный образ PostgreSQL фиксированной версии
image: postgres:16
# понятное имя контейнера с базой
container_name: vacancy-db
# логин, пароль и имя базы подставляются из .env
environment:
# имя пользователя базы
POSTGRES_USER: ${DB_USER}
# пароль пользователя
POSTGRES_PASSWORD: ${DB_PASSWORD}
# имя самой базы
POSTGRES_DB: ${DB_NAME}
# данные базы храним в именованном томе, чтобы они пережили пересоздание контейнера
volumes:
- pgdata:/var/lib/postgresql/data
# объявляем именованный том для данных PostgreSQL
volumes:
# имя тома
pgdata:
Небольшая, но важная деталь: depends_on гарантирует только то, что контейнер с базой стартует раньше бота, но не то, что база успеет полностью подняться и принять подключения. Поэтому в коде агента стоит сделать так, чтобы при первом подключении к базе он пару раз повторил попытку с небольшой паузой, а не падал сразу. Запускаем:
# собираем образы и запускаем контейнеры в фоновом режиме (-d -- detached)
docker compose up -d --build
# смотрим, какие контейнеры запущены и в каком они состоянии
docker compose ps

Главная дыра в безопасности, которую все забывают
Дальше идёт момент, который редко встречается в гайдах для новичков, хотя именно он чаще всего оставляет сервер открытым. Многие думают, что раз UFW настроен и всё лишнее закрыто, то опубликованный порт контейнера тоже под защитой firewall. Это не так, и причина — в том, как Docker работает с фильтрацией трафика.
Когда вы публикуете порт контейнера в docker-compose.yml строкой вроде ports: – “5432:5432”, Docker сам добавляет правила в системную таблицу фильтрации и заворачивает трафик к контейнеру через свою цепочку DOCKER-USER ещё до того, как до него доберутся правила UFW. На практике это значит, что порт базы данных может быть открыт всему интернету, даже если вы явно запретили его в UFW и команда ufw status показывает «всё закрыто». Firewall пакет до контейнера просто не видит. Это задокументированное поведение Docker, а не баг, но для безопасности сервера последствия серьёзные: незащищённую базу находят сканеры, после чего её могут вычитать или стереть с требованием выкупа.
Решение зависит от того, нужен ли порт снаружи. Для нашего агента базе вообще не нужен опубликованный порт: бот общается с ней по внутренней сети Docker, где контейнеры видят друг друга по именам сервисов. Поэтому самый надёжный вариант — просто не писать секцию ports у базы. Тогда снаружи к ней попасть нельзя в принципе, потому что порт нигде не слушается на внешнем интерфейсе.
Если же доступ к базе с самого сервера всё-таки нужен (например, чтобы зайти в неё с хоста для отладки), привяжите порт только к localhost:
# привязываем порт только к localhost -- снаружи он недоступен, доступен только с самого сервера
ports:
- "127.0.0.1:5432:5432"
А в редком случае, когда порт действительно должен быть доступен извне с конкретного IP, правило нужно добавлять не в UFW, а в ту самую цепочку DOCKER-USER:
# разрешаем доступ к порту 5432 только с доверенного IP, всем остальным запрещаем
# (eth0 -- внешний сетевой интерфейс, имя можно посмотреть командой ip a)
sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 5432 ! -s 203.0.113.55 -j DROP
Проверять надёжнее всего снаружи: с другой машины попробуйте подключиться к порту и убедитесь, что он недоступен. Команда nc -zv 203.0.113.10 5432 сообщит, открыт порт или нет. Способ настройки правил для DOCKER-USER описан в документации Docker по работе с iptables.

Настройка автозапуска и восстановления после падения
Restart policy — это правило, по которому Docker решает, перезапускать ли контейнер, если тот остановился. Без неё после перезагрузки сервера контейнеры останутся лежать, и агент будет молчать, пока вы вручную не зайдёте и не поднимете его. Для продакшена подходит политика unless-stopped: она поднимает контейнер после перезагрузки сервера или падения процесса, но уважает вашу ручную остановку. От похожей политики always она отличается одним: если вы сами остановили контейнер командой docker stop, то после перезагрузки unless-stopped оставит его остановленным, а always поднимет снова, проигнорировав ваше решение.
Вторая половина устойчивости — это healthcheck, проверка здоровья контейнера. Контейнер может числиться запущенным, но при этом бот внутри завис и ничего не делает. Healthcheck регулярно запускает маленькую проверку и помечает контейнер как unhealthy, если что-то не так. Для бота на long polling удобно сделать проверку через файл heartbeat: бот на каждом цикле обновляет дату у файла, а проверка смотрит, давно ли это было. Скрипт проверки healthcheck.py кладём в проект:
# подключаем модули для работы со временем и файлами
import os
import time
import sys
# путь к файлу-биению, который бот обновляет на каждом цикле
HEARTBEAT = "/tmp/heartbeat"
# если файла ещё нет -- бот ни разу не отметился, считаем это сбоем
if not os.path.exists(HEARTBEAT):
sys.exit(1)
# узнаём, сколько секунд прошло с последнего обновления файла
age = time.time() - os.path.getmtime(HEARTBEAT)
# если бот молчит дольше 120 секунд -- значит, завис
if age > 120:
sys.exit(1)
# иначе всё в порядке: код выхода 0 означает healthy
sys.exit(0)
В самом боте для этого достаточно одной строки в основном цикле — обновлять файл, например Path(“/tmp/heartbeat”).touch(). Теперь соберём итоговый docker-compose.yml, в котором уже есть и автозапуск, и проверка здоровья, и ограничение логов:
# описываем сервисы нашего агента
services:
# сервис самого бота
bot:
# собираем образ из Dockerfile в текущей папке
build: .
# понятное имя контейнера
container_name: vacancy-bot
# секреты и настройки берём из файла .env
env_file: .env
# автозапуск с уважением к ручной остановке
restart: unless-stopped
# стартуем после контейнера с базой
depends_on:
- db
# проверка здоровья: контейнер «живой», если бот недавно обновлял файл-биение
healthcheck:
# запускаем скрипт проверки внутри контейнера
test: ["CMD", "python", "healthcheck.py"]
# как часто проверять
interval: 30s
# сколько ждать ответа, прежде чем счесть проверку проваленной
timeout: 10s
# сколько провалов подряд нужно, чтобы пометить контейнер unhealthy
retries: 3
# сколько времени дать контейнеру на запуск до начала проверок
start_period: 40s
# ограничиваем размер логов, чтобы они не забили диск
logging:
# стандартный драйвер логов Docker
driver: "json-file"
# настройки ротации логов
options:
# один файл лога -- не больше 10 МБ
max-size: "10m"
# храним максимум 3 файла, старые удаляются
max-file: "3"
# сервис базы данных
db:
# официальный образ PostgreSQL фиксированной версии
image: postgres:16
# понятное имя контейнера с базой
container_name: vacancy-db
# порт наружу не публикуем -- бот ходит в базу по внутренней сети Docker
# логин, пароль и имя базы подставляются из .env
environment:
# имя пользователя базы
POSTGRES_USER: ${DB_USER}
# пароль пользователя
POSTGRES_PASSWORD: ${DB_PASSWORD}
# имя самой базы
POSTGRES_DB: ${DB_NAME}
# данные базы храним в именованном томе
volumes:
- pgdata:/var/lib/postgresql/data
# та же политика автозапуска
restart: unless-stopped
# объявляем именованный том для данных PostgreSQL
volumes:
# имя тома
pgdata:
Есть и обратная сторона, о которой стоит знать заранее. Бездумный автоперезапуск на сломанной конфигурации создаёт бесконечный цикл. Если бот падает сразу на старте — например, в .env опечатка в токене, — то unless-stopped будет поднимать его снова и снова. Docker увеличивает паузу между попытками, но контейнер всё равно крутится вхолостую, забивает логи одинаковыми ошибками и маскирует настоящую причину сбоя, а в случае с ботом ещё и долбится в Telegram неверным токеном, за что можно получить временную блокировку. Поэтому правило такое: сначала чините причину, потом включаете автоперезапуск. Если контейнер падает на старте, не оставляйте его в цикле — посмотрите логи командой docker compose logs, найдите ошибку и только после этого возвращайте restart. Все детали по политикам перезапуска и healthcheck есть в официальной документации Docker.

Мониторинг и логи — как узнать, что агент упал, раньше пользователей
Хороший мониторинг бота на сервере не требует тяжёлых систем. Для небольшого VPS достаточно трёх вещей: смотреть логи, не дать им забить диск и получать оповещение, когда агент падает. Начнём с логов. Чтобы посмотреть, что бот пишет прямо сейчас, есть команда:
# показываем логи контейнера и продолжаем выводить новые строки в реальном времени (-f -- follow)
docker compose logs -f

Ротацию логов мы уже включили в docker-compose.yml параметрами max-size и max-file. Без неё логи растут бесконечно, и через месяц-другой на маленьком сервере просто кончится место на диске — а когда заканчивается диск, перестают работать и сам бот, и база. Ограничение в три файла по 10 МБ держит этот рост под контролем.
Теперь оповещения. healthcheck помечает зависший контейнер как unhealthy, но сам по себе вам об этом не сообщит. Добавим простой скрипт-монитор, который запускается по расписанию, проверяет состояние контейнера и шлёт сообщение в Telegram, если что-то не так. Для оповещений заведите отдельного бота-сторожа через @BotFather, чтобы не зависеть от основного. Скрипт monitor.sh:
# указываем, что скрипт выполняется интерпретатором bash
#!/bin/bash
# имя контейнера, который проверяем
CONTAINER="vacancy-bot"
# токен бота-сторожа, через которого шлём оповещения
ALERT_TOKEN="123456:AA-замените-на-свой"
# chat_id, куда придёт оповещение (ваш личный чат с ботом-сторожем)
CHAT_ID="111111111"
# спрашиваем у Docker состояние контейнера: running, exited, restarting и так далее
STATE=$(docker inspect -f '{{.State.Status}}' "$CONTAINER" 2>/dev/null)
# спрашиваем статус проверки здоровья: healthy, unhealthy или пусто
HEALTH=$(docker inspect -f '{{.State.Health.Status}}' "$CONTAINER" 2>/dev/null)
# если контейнер не запущен или помечен как unhealthy -- формируем и шлём тревогу
if [ "$STATE" != "running" ] || [ "$HEALTH" = "unhealthy" ]; then
# собираем текст сообщения с именем контейнера и его состоянием
TEXT="Агент ${CONTAINER}: статус ${STATE:-не найден}, health ${HEALTH:-нет}"
# отправляем сообщение через Telegram Bot API
curl -s -X POST "https://api.telegram.org/bot${ALERT_TOKEN}/sendMessage" \
-d chat_id="${CHAT_ID}" \
--data-urlencode "text=${TEXT}" > /dev/null
fi
Делаем скрипт исполняемым и добавляем его в расписание cron, чтобы он запускался каждые пять минут:
# даём скрипту право на исполнение
chmod +x /home/deploy/monitor.sh
# открываем редактор расписания cron
crontab -e
В открывшемся файле добавляем одну строку:
# запускать скрипт-монитор каждые 5 минут
*/5 * * * * /home/deploy/monitor.sh

Отдельно — про метрики, на которые стоит поглядывать даже на маленьком сервере. Их три. Место на диске: когда оно заканчивается, падают и бот, и база, поэтому за ним следят в первую очередь. Использование оперативной памяти: если памяти не хватает, система начнёт убивать процессы, и первым под нож часто идёт самый прожорливый контейнер. Число перезапусков контейнера: если оно быстро растёт, значит, бот попал в тот самый цикл падений из прошлого раздела. Смотрят их так:
# сколько места осталось на диске
df -h
# сколько оперативной памяти занято и свободно
free -h
# сколько раз перезапускался контейнер бота
docker inspect -f '{{.RestartCount}}' vacancy-bot
Если после пет-проекта вам захочется глубже разобраться в серверах, инфраструктуре и автоматизации, посмотрите гайд «DevOps с нуля» — там про вход в профессию, которая как раз про всё это.
Бэкапы — что и как часто сохранять
Бэкап нужен для одного: вернуть агента к жизни, когда что-то пошло не так — сломалось обновление, повредились данные или сервер вообще пропал. Для нашего агента важны два уровня. Первый — ежедневная резервная копия базы данных, где хранится состояние бота: какие вакансии он уже отправлял, на какие запросы подписан пользователь. Если база — это PostgreSQL в контейнере, дамп делается так:
# выгружаем дамп базы из контейнера в файл, добавляя дату в имя
docker compose exec -T db pg_dump -U botuser vacancydb > /home/deploy/backups/vacancydb_$(date +%F).sql
Если агент хранит состояние в SQLite, нельзя просто копировать файл базы во время работы — копия может оказаться повреждённой, если в этот момент шла запись. Используйте встроенную команду безопасного копирования:
# делаем согласованную копию базы SQLite, безопасную даже при активной записи
sqlite3 /home/deploy/vacancy-bot/data/bot.db ".backup '/home/deploy/backups/bot_$(date +%F).db'"
Эту команду тоже стоит повесить на cron раз в сутки — по той же схеме, что и скрипт-монитор. Второй уровень — снапшот всего сервера на стороне провайдера. Он сохраняет не только базу, а весь диск целиком: систему, настройки, контейнеры. Снапшот спасает, когда вы сломали что-то в самой системе и проще откатить всё разом, чем чинить по частям. Большинство провайдеров умеют делать снапшоты по расписанию — включите их в панели управления.
И главное, о чём забывают новички: бэкап нужно периодически проверять восстановлением, а не просто складывать файлы и надеяться. Копия, которую ни разу не разворачивали, может оказаться битой, неполной или снятой в неудачный момент посреди записи — и вы узнаете об этом ровно тогда, когда бэкап понадобится по-настоящему, в худший возможный момент. Поэтому раз в неделю или месяц возьмите свежий дамп и разверните его на тестовом сервере, убедившись, что данные на месте:
# на тестовом сервере заливаем дамп в чистую базу и проверяем, что данные на месте
psql -U botuser -d vacancydb_test < /home/deploy/backups/vacancydb_2026-06-20.sql

Типичные ошибки новичков при первом деплое
- Оставить открытым порт без аутентификации. Это случается, когда базу или какую-нибудь веб-панель управления публикуют наружу строкой
portsбез привязки кlocalhost, в полной уверенности, что UFW прикроет. Мы уже разобрали, почему Docker обходит UFW, и вот к чему это приводит: боты-сканеры обходят весь диапазон адресов IPv4 за считаные минуты, находят открытую базу PostgreSQL или Redis и либо подбирают к ней доступ перебором, либо стирают данные с требованием выкупа. Лечится тем, что мы уже сделали: не публиковать порты, которые не нужны снаружи, а нужные привязывать кlocalhost. - Хранить токены и ключи прямо в коде. Часто так делают на этапе разработки «чтобы быстрее», а потом забывают вынести в
.envперед отправкой кода в репозиторий. Дальше работает та же механика, что и с утечкой.env: токены из публичного репозитория собирают автоматические сканеры в течение минут, и ваш Telegram-бот или ключ к API провайдера ИИ оказывается у чужих людей. Кстати, риски такого рода особенно выросли с распространением AI-кодинга, когда код пишется быстро и не всегда вычитывается, — об этом есть отдельный материал про vibe-coding и безопасность. - Не ограничить повторные запросы при сбое внешнего API. Если источник вакансий или Telegram вернул ошибку, а агент в ответ мгновенно повторяет запрос снова и снова без пауз, он начинает заваливать сервис обращениями. Telegram на это отвечает кодом 429 и заголовком, в котором прямо написано, сколько секунд подождать, и если этот ответ игнорировать, бота временно заблокируют. Поэтому повторы делают с нарастающей паузой и ограничением на число попыток, а не циклом «попробовать ещё раз немедленно».
- Запускать процесс от
rootбез необходимости. Если контейнер или приложение внутри работает под root и его удастся скомпрометировать через уязвимость, злоумышленник получает максимальные права. Когда же процесс работает под обычным пользователем, последствия взлома куда скромнее. В Docker для этого вDockerfileуказывают директивуUSERс непривилегированным пользователем, чтобы внутри контейнера бот не работал от root.
Частые вопросы
Хватит ли минимального VPS для одного Telegram-агента? Да, с большим запасом. Для бота на long polling, который раз в несколько минут опрашивает API, достаточно 1 виртуального ядра и 1 ГБ оперативной памяти. Реально такой агент вместе с базой занимает сотни мегабайт, и упрётесь вы скорее в лимиты внешнего API, чем в ресурсы сервера.
Нужен ли домен и SSL-сертификат, если у агента нет веб-интерфейса? Нет. Бот на long polling сам подключается к Telegram наружу, и входящие соединения к нему не идут, поэтому ни домен, ни сертификат не нужны. Они понадобятся только если вы переведёте бота на webhook или добавите ему веб-страницу — тогда нужен адрес, по которому Telegram или пользователь смогут к нему обратиться по HTTPS.
Как обновить агента на сервере без простоя? Для одного бота полноценный деплой без простоя — это перебор. На практике обновление выглядит так: забираете свежий код через git pull, затем пересобираете и перезапускаете контейнер командой docker compose up -d –build. Контейнер недоступен буквально несколько секунд на время пересборки и перезапуска, и для бота уведомлений это незаметно — он просто продолжит работу с обновлённым кодом.
Что делать, если сервер всё же скомпрометирован? Не пытаться вычистить взломанную систему изнутри — гарантий, что вы убрали всё, нет. Правильнее поднять сервер заново с чистого образа или из снапшота, сделанного заведомо до взлома. Все токены и ключи считайте утёкшими и перевыпустите: токен бота через @BotFather, ключи к API — в личном кабинете провайдера. Данные восстановите из проверенного бэкапа, снятого до момента компрометации.
Можно ли запускать несколько агентов на одном VPS? Да. Каждый агент живёт в своих контейнерах и не мешает соседям, а Docker Compose позволяет описать их в одном файле или развести по отдельным папкам. Следите за оперативной памятью: пока суммарное потребление помещается в объём сервера, всё работает, а как только захотите десяток тяжёлых агентов — добавьте памяти или возьмите сервер побольше.
Заключение
Мы прошли весь путь деплоя AI-агента на VPS: от выбора сервера и первого подключения по SSH до автозапуска, мониторинга и бэкапов. Главное, что стоит вынести: развернуть агента на сервере — это не «скопировал файлы и запустил», а несколько простых, но обязательных шагов, каждый из которых закрывает конкретную проблему. Смена SSH-порта и вход по ключам убирают шум сканеров и перебор пароля. Привязка портов к localhost закрывает дыру, через которую Docker обходит firewall. Политика unless-stopped и healthcheck поднимают агента после перезагрузки и падения. Мониторинг даёт узнать о сбое раньше пользователей, а проверенные бэкапы возвращают агента к жизни, когда что-то ломается.
Теперь у вас есть AI-агент на VPS, который работает сам по себе и переживает неприятности, а не требует, чтобы вы караулили его у ноутбука. Дальше можно спокойно добавлять ему новые источники вакансий, фильтры и команды — фундамент уже надёжный.
Советуем дополнительно почитать по теме:
- ИИ-навыки разработчика: 10 требований для 2026 — промпты, AI-агенты, LLM API, RAG, выбор моделей, безопасность и критическая проверка AI-вывода. Хорошее продолжение после деплоя своего агента.
- 12 AI GitHub-репозиториев 2026: Ollama, n8n, Claude Code и OpenHands — подборка open-source-инструментов для локальных моделей, автоматизации, MCP, агентных сценариев и AI-разработки.
- Роадмап Golang: путь от нуля до джуниора в 2026 — в роадмапе есть Docker, Docker Compose, Linux, терминал, CI/CD, мониторинг и развёртывание приложения на виртуальном сервере. Подходит тем, кто хочет расти в инфраструктурный backend.
- Что такое API — базовое объяснение API для тех, кто хочет лучше понимать, как агент общается с Telegram, OpenRouter, внешними сервисами и базами.
- Прокачиваем телеграм-бота: пишем полноценный бэкенд — материал про Telegram-бота, backend-логику и SQLite. Хорошее продолжение для тех, кто хочет усложнить агента и хранить состояние нормально.
Бонус для читателей
Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая.
