Здравствуйте! Самопроизвольное завершение Telegram-бота в контейнере **Podman** с получением сигнала **SIGTERM** — это распространенная проблема, которая может быть вызвана несколькими факторами, связанными с тем, как система управляет контейнерами и процессами. Поскольку бот работает стабильно вне контейнера, проблема, скорее всего, кроется в конфигурации окружения контейнера или его взаимодействии с хост-системой.
Логи `WARNING:aiogram.dispatcher:Received SIGTERM signal` и `INFO:aiogram.dispatcher:Polling stopped for bot ...` однозначно указывают, что приложение корректно получает и обрабатывает сигнал на завершение работы. Вопрос в том, что именно отправляет этот сигнал без вашего ведома.
### Возможные причины отправки SIGTERM
Наиболее вероятные причины неожиданного завершения контейнера связаны с внешними управляющими процессами, такими как **systemd**, или особенностями работы rootless-контейнеров.
#### 1. Управление контейнером через systemd
Если вы запускаете контейнер как сервис **systemd**, то именно **systemd** может быть источником сигнала `SIGTERM`.
* **Завершение пользовательской сессии:** Если вы используете rootless-контейнер и запускаете его от имени обычного пользователя, **systemd** может автоматически завершать все процессы этого пользователя при его выходе из системы (например, при закрытии SSH-сессии)[1]. Это стандартное поведение для предотвращения "осиротевших" процессов.
* **Конфигурация сервисного файла:** Сервисный файл, сгенерированный с помощью `podman generate systemd`, содержит директиву `ExecStop=/usr/bin/podman stop ...`[2]. При определенных условиях (например, при перезапуске системы или самого сервиса) **systemd** выполнит эту команду, которая, в свою очередь, отправит `SIGTERM` вашему контейнеру. По умолчанию `podman stop` ожидает 10 секунд, а затем отправляет `SIGKILL`, если процесс не завершился[3][4].
#### 2. Особенности работы rootless-контейнеров
Запуск контейнеров без прав `root` (rootless) является более безопасным и предпочтительным методом, но имеет свои нюансы[5].
* **Проблема 15 минут:** Некоторые пользователи сталкивались с тем, что rootless-контейнеры без видимых причин получают `SIGTERM` ровно через 15 минут после запуска[6]. Это может быть связано с системными таймерами или политиками, управляющими пользовательскими службами.
* **Завершение при выходе:** Как уже упоминалось, процессы, запущенные в сессии пользователя, могут быть автоматически остановлены при ее завершении[1].
#### 3. Неправильный основной процесс в контейнере (PID 1)
Среда выполнения контейнеров (Podman, Docker) отправляет сигналы процессу с идентификатором **PID 1** внутри контейнера[7][8].
* **Запуск через shell-скрипт:** Если в вашем `Dockerfile` используется инструкция вида `CMD ./start.sh`, то главным процессом (PID 1) становится оболочка `/bin/sh`, а ваш Python-скрипт запускается как дочерний процесс. Стандартные оболочки не всегда корректно перенаправляют сигналы дочерним процессам. В результате, при получении `SIGTERM`, завершается только оболочка, а Podman считает, что контейнер можно останавливать.
* Судя по вашим логам, библиотека `aiogram` *получает* сигнал, так что эта причина менее вероятна, но является важной для корректной настройки.
#### 4. Внешние системные процессы
* **OOM Killer (Out of Memory Killer):** Хотя обычно OOM Killer отправляет сигнал `SIGKILL` (безусловное завершение), возможно, на вашей системе настроены скрипты, которые при нехватке памяти сначала пытаются корректно остановить процессы с помощью `SIGTERM`[9].
* **Сторонние скрипты или службы:** Администратор системы мог настроить скрипты для очистки процессов или управления ресурсами, которые отправляют `SIGTERM` контейнерам.
### Решения и рекомендации
Чтобы решить проблему, необходимо обеспечить корректный запуск и управление жизненным циклом контейнера.
#### 1. Правильный запуск и управление контейнером
* **Используйте политику перезапуска:** Запускайте контейнер с флагом `--restart=always`. Это заставит Podman автоматически перезапускать контейнер в случае его остановки по любой причине.
```bash
podman run -d --restart=always --name my-tg-bot my-image
```
* **Убедитесь, что бот является PID 1:** Чтобы ваш бот был основным процессом, используйте в `Dockerfile` формат `exec`:
* *JSON-синтаксис (рекомендуется):*
```dockerfile
CMD ["python", "main.py"]
```
* *Использование `exec` в shell-скрипте:*
```sh
#!/bin/sh
# Другие команды
exec python main.py
```
* **Запуск в фоновом режиме:** Всегда используйте флаг `-d` (`--detach`) для запуска контейнера в фоновом режиме, чтобы он не был привязан к вашей текущей сессии терминала[10].
#### 2. Настройка для работы с systemd
Если вы хотите, чтобы контейнер запускался при старте системы и работал постоянно, используйте **systemd**.
* **Включите "linger" для пользователя:** Чтобы разрешить сервисам пользователя работать даже после его выхода из системы, выполните команду (заменив `` на ваше имя пользователя):
```bash
loginctl enable-linger
```
* **Создайте и настройте systemd-сервис:**
1. Создайте контейнер с именем: `podman create --name my-tg-bot my-image`.
2. Сгенерируйте сервисный файл: `podman generate systemd --name my-tg-bot --files`.
3. Переместите сгенерированный файл `container-my-tg-bot.service` в `~/.config/systemd/user/`.
4. Перезагрузите демона **systemd**: `systemctl --user daemon-reload`.
5. Включите и запустите сервис:
```bash
systemctl --user enable --now container-my-tg-bot.service
```
#### 3. Обработка SIGTERM в коде бота
Хотя это не решает причину отправки сигнала, вы можете модифицировать код бота для более "гибкой" реакции на `SIGTERM`[11]. Например, вы можете не завершать работу сразу, а сначала логировать получение сигнала и пытаться продолжить работу (хотя это может быть нежелательно, если система действительно пытается остановить процесс по веской причине).
Пример обработки сигнала в Python:
```python
import signal
import asyncio
import logging
# Настройка логирования
logging.basicConfig(level=logging.INFO)
# Флаг для корректного завершения
shutdown_flag = asyncio.Event()
def handle_sigterm(signum, frame):
logging.warning("Получен сигнал SIGTERM. Инициирую корректное завершение.")
# Устанавливаем событие, чтобы основной цикл мог завершиться
shutdown_flag.set()
async def main():
# Здесь ваш основной код инициализации бота (aiogram)
# bot = Bot(...)
# dp = Dispatcher(...)
# Устанавливаем обработчик сигнала
signal.signal(signal.SIGTERM, handle_sigterm)
logging.info("Бот запущен и готов к работе.")
try:
# Ваш основной цикл работы бота
# await dp.start_polling(bot)
# Вместо бесконечного цикла, ждем события о завершении
await shutdown_flag.wait()
finally:
# Корректное завершение
logging.info("Останавливаю бота...")
# await bot.session.close()
logging.info("Бот остановлен.")
if __name__ == '__main__':
try:
asyncio.run(main())
except (KeyboardInterrupt, SystemExit):
logging.info("Процесс прерван.")
```