Как развернуть AI-агента на VPS — от установки до продакшена

Что делать с AI-агентом, который уже слишком взрослый для ноутбука

Как развернуть AI-агента на VPS — от установки до продакшена

Мы много пишем о ботах, AI-агентах и других сервисах, которые делают жизнь и работу легче. Для учебных задач все эти штуки допустимо и удобно запускать на локальной машине — буквально без регистраций и смс. К тому же не нужно платить за сервер.

Если вы пишете Telegram-бота или AI-агента для реальных задач, держать его на ноутбуке неудобно: процесс уснёт вместе с системой или пропадёт после перезагрузки. Поэтому сегодня поговорим о том, как задеплоить свои сервисы на VPS, чтобы они работали круглые сутки. Мы пройдём весь путь: выберем сервер, подключимся к нему, закроем дыры в безопасности, упакуем агента в Docker, настроим автозапуск, мониторинг и бэкапы. В конце у вас будет AI-агент, который переживёт перезагрузку сервера, падение процесса и попытку потенциального хакера постучаться в открытый порт.

Если своего агента у вас пока нет — ничего страшного, дальше будет ссылка на материал, где его собирают с нуля. А здесь мы сосредоточимся на развёртывании. Статья получилась подробная, так что запаситесь терпением, снеками и напитками.

ВАМ ПРИШЛО ПРИГЛАШЕНИЕ 💌
Приходите к нам в соцсети поделиться своим мнением и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!

Что нужно, чтобы перейти от локального скрипта к продакшену

Локальный скрипт на ноутбуке живёт ровно до того момента, пока включён ноутбук. Закрыли крышку — процесс уснул вместе с системой. Прилетело обновление с перезагрузкой — скрипт не запустится обратно сам. Выключили свет — и всё, бот пропал из эфира, пока вы не вернётесь и не запустите его руками. Для пет-проекта это терпимо, но как только вы хотите, чтобы агент реально работал в любое время, нужна машина, которая не выключается. Эту роль и берёт на себя VPS — виртуальный сервер, который крутится в дата-центре и работает 24/7.

Но перенести агента на сервер — это полдела. Еще полдела — защитить агента от неприятностей: перезагрузок сервера, падений, ботов-сканеров и прочих сетевых неурядиц. Результат, к которому мы придём — AI-агент на VPS, который сам поднимается после перезагрузки, перезапускается после падения и не сливает секреты наружу.

Если вы ещё не написали самого агента, начните с материала “Как собрать AI-парсер вакансий — пошагово, с кодом” — там разбираем, как он устроен внутри. Из того материала понадобится файл agent.py, сохраните user ID, токены для телеграм-бота и API-ключа OpenRouter (что это, зачем и откуда брать — мы там же написали). Если вы уже собрали бота и успешно запустили локально, то идем дальше.

Схема работы агента, развернутого на VPS
Схема работы агента, развернутого на VPS

Выбираем 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
Вход под deploy по ключу: сервер пускает сразу, без запроса пароля. Если в приглашении появилось deploy@vacancy-bot, ключ настроен правильно.
Вход под deploy по ключу: сервер пускает сразу, без запроса пароля. Если в приглашении появилось deploy@vacancy-bot, ключ настроен правильно.

Когда вход по ключу налажен, можно закрутить гайки в конфигурации 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
UFW включён и пропускает только SSH на порту 2222. Всё остальное закрыто по умолчанию — ровно то состояние, в котором сервер должен жить.
UFW включён и пропускает только SSH на порту 2222. Всё остальное закрыто по умолчанию — ровно то состояние, в котором сервер должен жить.

Для нашего агента этого набора правил достаточно: бот сам инициирует все соединения наружу, и снаружи к нему стучаться не нужно. Если позже вы добавите агенту веб-интерфейс, вы откроете порты 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
Оба контейнера подняты, у бота статус healthy. Агент развернулся и работает на сервере.
Оба контейнера подняты, у бота статус healthy. Агент развернулся и работает на сервере.

Главная дыра в безопасности, которую все забывают

Дальше идёт момент, который редко встречается в гайдах для новичков, хотя именно он чаще всего оставляет сервер открытым. Многие думают, что раз 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.

Слева UFW уверяет, что порт 5432 закрыт. Справа этот же порт спокойно отвечает с внешней машины. Docker пробросил его мимо firewall — та самая дыра в безопасности.
Слева UFW уверяет, что порт 5432 закрыт. Справа этот же порт спокойно отвечает с внешней машины. Docker пробросил его мимо firewall — та самая дыра в безопасности.

Настройка автозапуска и восстановления после падения

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.

Контейнер в цикле перезапуска: статус Restarting, а в логах повторяется одна и та же ошибка старта. Посмотрите на метки времени — паузы между попытками растут, это Docker притормаживает падающий контейнер.
Контейнер в цикле перезапуска: статус Restarting, а в логах повторяется одна и та же ошибка старта. Посмотрите на метки времени — паузы между попытками растут, это 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
Бот-сторож поймал падение и написал в Telegram.
Бот-сторож поймал падение и написал в Telegram.

Отдельно — про метрики, на которые стоит поглядывать даже на маленьком сервере. Их три. Место на диске: когда оно заканчивается, падают и бот, и база, поэтому за ним следят в первую очередь. Использование оперативной памяти: если памяти не хватает, система начнёт убивать процессы, и первым под нож часто идёт самый прожорливый контейнер. Число перезапусков контейнера: если оно быстро растёт, значит, бот попал в тот самый цикл падений из прошлого раздела. Смотрят их так:

# сколько места осталось на диске
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
Логика работы бэкапов
Логика работы бэкапов

Типичные ошибки новичков при первом деплое

  1. Оставить открытым порт без аутентификации. Это случается, когда базу или какую-нибудь веб-панель управления публикуют наружу строкой ports без привязки к localhost, в полной уверенности, что UFW прикроет. Мы уже разобрали, почему Docker обходит UFW, и вот к чему это приводит: боты-сканеры обходят весь диапазон адресов IPv4 за считаные минуты, находят открытую базу PostgreSQL или Redis и либо подбирают к ней доступ перебором, либо стирают данные с требованием выкупа. Лечится тем, что мы уже сделали: не публиковать порты, которые не нужны снаружи, а нужные привязывать к localhost.
  2. Хранить токены и ключи прямо в коде. Часто так делают на этапе разработки «чтобы быстрее», а потом забывают вынести в .env перед отправкой кода в репозиторий. Дальше работает та же механика, что и с утечкой .env: токены из публичного репозитория собирают автоматические сканеры в течение минут, и ваш Telegram-бот или ключ к API провайдера ИИ оказывается у чужих людей. Кстати, риски такого рода особенно выросли с распространением AI-кодинга, когда код пишется быстро и не всегда вычитывается, — об этом есть отдельный материал про vibe-coding и безопасность.
  3. Не ограничить повторные запросы при сбое внешнего API. Если источник вакансий или Telegram вернул ошибку, а агент в ответ мгновенно повторяет запрос снова и снова без пауз, он начинает заваливать сервис обращениями. Telegram на это отвечает кодом 429 и заголовком, в котором прямо написано, сколько секунд подождать, и если этот ответ игнорировать, бота временно заблокируют. Поэтому повторы делают с нарастающей паузой и ограничением на число попыток, а не циклом «попробовать ещё раз немедленно».
  4. Запускать процесс от 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, который работает сам по себе и переживает неприятности, а не требует, чтобы вы караулили его у ноутбука. Дальше можно спокойно добавлять ему новые источники вакансий, фильтры и команды — фундамент уже надёжный.

Советуем дополнительно почитать по теме: 

Бонус для читателей

Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая.

Вам может быть интересно
easy