Привет, Хабр! Сегодня покажу, как создать простого Telegram-бота для управления Docker-контейнерами на удаленном сервере, а можно и на локальном даже под Windows (достаточно только интернета). Детище позволит быстро перезапустить сервис или посмотреть логи прямо из Telegram.

Не буду греха таить на Хабре есть подобная статья, но там сильно продвинутое решение с управлением конфигурациями и Docker Compose проектами, рекомендую ознакомиться, пригодится для развития бота. Наше решение фокусируется на простоте и базовой функциональности и я распишу все по шагам, любой начинающий сможет повторить.

Да, без нашумевшего вайб кодинга и тут не обошлось, хотя проект написан почти полностью нейронкой, у проекта другие смыслы. Мы сделаем реально работающее решение и поработаем с python-telegram-bot.

Мы будем через polling (опрос сервера Telegram), а не webhook'и, что позволит обойтись без публичного IP адреса, если это например домашний компьютер.

Идеи для развитие бота:

  • Развертка контейнера из GitHub;

  • Обновление контейнера при обновлении репозитория;

  • Использование Docker-compose;

Что уже есть:

  • Список контейнеров - показывает все контейнеры с статусом

  • Запуск контейнеров - кнопка "Запустить"

  • Остановка контейнеров - кнопка "Остановить"

  • Перезапуск контейнеров - кнопка "Перезапустить"

  • Статистика сервера - CPU, память, количество контейнеров

  • Логи контейнеров - последние 20 строк логов

  • Информация о контейнере - статус, образ, время создания

Не редки ситуацию, когда есть несколько проектов, развернутых в Docker-контейнерах и какой-то повис, сможете помочь ему прямо в пути)

И так начнем. Идем к @BotFather в Telegram, набираем /newbot и создаем бота, забираем Токен:

Заготовка бота
Заготовка бота

С помощью бота https://t.me/getmyid_bot получим свой ID в Telegram, понадобится для того, чтобы ограничить круг пользователей бота.

В момент дописывании статьи это бот перестал работать, пока убрал этот функционал, но оставил для истории. И нашел вот этого @userinfobot.

Для упрощения предусмотрен только локальный вариант работы бота, то есть бот сможет управлять контейнерами только на том сервере, на котором установлен, реализовано через монтирование var/run/docker.sock- это Unix-сокет, через который Docker демон общается с клиентами. На хосте Docker демон слушает на /var/run/docker.sock в контейнере, монтируем этот сокет внутрь контейнера, как результат, контейнер может управлять Docker на хосте.

На сервере должен быть установлен Docker. Если компьютер локальный то все просто, если сервер удаленный, то нужен SSH. В моем случае я использую Docker VPS, буду работать по SSH, все команды будут работать и в PowerShell на Windows, единственное нужно установить git, если нет.

Для коннект к серверу использую SSH клиент MobaXterm

Все мы на сервере, все готово, можем начинать.

И так код бота на всеобщее обозрение, технологии страшная сила:

Скрытый текст
import os
import asyncio
import subprocess
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes
from dotenv import load_dotenv

load_dotenv()

class DockerBot:
    def __init__(self):
        self.bot_token = os.getenv('BOT_TOKEN')
        self.server_host = os.getenv('SERVER_HOST')
        self.server_user = os.getenv('SERVER_USER')
        self.server_password = os.getenv('SERVER_PASSWORD')
        
    async def run_ssh_command(self, command):
        """Выполнить команду через SSH"""
        ssh_command = [
            'sshpass', '-p', self.server_password,
            'ssh', '-o', 'StrictHostKeyChecking=no',
            f'{self.server_user}@{self.server_host}',
            command
        ]
        
        process = await asyncio.create_subprocess_exec(
            *ssh_command,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
        
        stdout, stderr = await process.communicate()
        return stdout.decode() if process.returncode == 0 else stderr.decode()
    
    async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Команда /start"""
        keyboard = [
            [InlineKeyboardButton("? Список контейнеров", callback_data="list")],
            [InlineKeyboardButton("? Статистика", callback_data="stats")]
        ]
        reply_markup = InlineKeyboardMarkup(keyboard)
        
        await update.message.reply_text(
            "? *Docker Bot*\n\nВыберите действие:",
            reply_markup=reply_markup,
            parse_mode='Markdown'
        )
    
    async def button_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Обработка нажатий на кнопки"""
        query = update.callback_query
        await query.answer()
        
        if query.data == "list":
            await self.show_containers(query)
        elif query.data == "stats":
            await self.show_stats(query)
        elif query.data.startswith("container_"):
            await self.show_container_info(query)
        elif query.data.startswith("action_"):
            await self.handle_action(query)
    
    async def show_containers(self, query):
        """Показать список контейнеров"""
        result = await self.run_ssh_command("docker ps -a --format '{{.Names}}\t{{.Status}}\t{{.Image}}'")
        
        if not result.strip():
            await query.edit_message_text("? Контейнеры не найдены")
            return
        
        message = "? *Список контейнеров:*\n\n"
        keyboard = []
        
        for line in result.strip().split('\n'):
            if line:
                parts = line.split('\t')
                if len(parts) >= 3:
                    name, status, image = parts[0], parts[1], parts[2]
                    status_emoji = "?" if "Up" in status else "?"
                    
                    message += f"{status_emoji} `{name}`\n"
                    message += f"   Статус: {status}\n"
                    message += f"   Образ: {image}\n\n"
                    
                    keyboard.append([
                        InlineKeyboardButton(
                            f"{'⏹️' if 'Up' in status else '▶️'} {name}",
                            callback_data=f"container_{name}"
                        )
                    ])
        
        keyboard.append([InlineKeyboardButton("? Назад", callback_data="back")])
        reply_markup = InlineKeyboardMarkup(keyboard)
        
        await query.edit_message_text(message, reply_markup=reply_markup, parse_mode='Markdown')
    
    async def show_container_info(self, query):
        """Показать информацию о контейнере"""
        container_name = query.data.split("_")[1]
        
        # Получаем статус
        status_result = await self.run_ssh_command(f"docker ps -a --filter name={container_name} --format '{{.Status}}'")
        status = status_result.strip()
        
        message = f"? *{container_name}*\n\n"
        message += f"Статус: {status}\n\n"
        
        keyboard = []
        
        if "Up" in status:
            keyboard.append([InlineKeyboardButton("⏹️ Остановить", callback_data=f"action_stop_{container_name}")])
            keyboard.append([InlineKeyboardButton("? Перезапустить", callback_data=f"action_restart_{container_name}")])
        else:
            keyboard.append([InlineKeyboardButton("▶️ Запустить", callback_data=f"action_start_{container_name}")])
        
        keyboard.append([InlineKeyboardButton("? Логи", callback_data=f"action_logs_{container_name}")])
        keyboard.append([InlineKeyboardButton("? Назад", callback_data="list")])
        
        reply_markup = InlineKeyboardMarkup(keyboard)
        await query.edit_message_text(message, reply_markup=reply_markup, parse_mode='Markdown')
    
    async def handle_action(self, query):
        """Обработка действий с контейнерами"""
        data = query.data.split("_")
        action = data[1]
        container_name = "_".join(data[2:])
        
        if action == "start":
            await self.run_ssh_command(f"docker start {container_name}")
            await query.edit_message_text(f"✅ Контейнер {container_name} запущен")
        elif action == "stop":
            await self.run_ssh_command(f"docker stop {container_name}")
            await query.edit_message_text(f"⏹️ Контейнер {container_name} остановлен")
        elif action == "restart":
            await self.run_ssh_command(f"docker restart {container_name}")
            await query.edit_message_text(f"? Контейнер {container_name} перезапущен")
        elif action == "logs":
            logs = await self.run_ssh_command(f"docker logs --tail 20 {container_name}")
            if len(logs) > 3000:
                logs = logs[-3000:] + "\n\n... (показаны последние 20 строк)"
            
            message = f"? *Логи {container_name}:*\n\n```\n{logs}\n```"
            keyboard = [[InlineKeyboardButton("? Назад", callback_data=f"container_{container_name}")]]
            reply_markup = InlineKeyboardMarkup(keyboard)
            
            await query.edit_message_text(message, reply_markup=reply_markup, parse_mode='Markdown')
    
    async def show_stats(self, query):
        """Показать статистику"""
        result = await self.run_ssh_command("docker stats --no-stream --format '{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}'")
        
        message = "? *Статистика сервера:*\n\n"
        
        if result.strip():
            lines = result.strip().split('\n')
            if lines and lines[0]:
                parts = lines[0].split('\t')
                if len(parts) >= 3:
                    cpu = parts[0].replace('%', '')
                    memory = parts[2].replace('%', '')
                    message += f"?️ CPU: {cpu}%\n"
                    message += f"? Память: {memory}%\n"
        
        # Подсчет контейнеров
        containers_result = await self.run_ssh_command("docker ps -a --format '{{.Names}}'")
        total_containers = len([line for line in containers_result.strip().split('\n') if line.strip()])
        
        running_result = await self.run_ssh_command("docker ps --format '{{.Names}}'")
        running_containers = len([line for line in running_result.strip().split('\n') if line.strip()])
        
        message += f"? Контейнеры: {running_containers}/{total_containers}\n"
        
        keyboard = [[InlineKeyboardButton("? Назад", callback_data="back")]]
        reply_markup = InlineKeyboardMarkup(keyboard)
        
        await query.edit_message_text(message, reply_markup=reply_markup, parse_mode='Markdown')
    
    def run(self):
        """Запуск бота"""
        application = Application.builder().token(self.bot_token).build()
        
        application.add_handler(CommandHandler("start", self.start))
        application.add_handler(CallbackQueryHandler(self.button_handler))
        
        print("Бот запущен...")
        application.run_polling()

if __name__ == "__main__":
    bot = DockerBot()
    bot.run()

Я не буду заставлять вас все повторять, все есть на GitHub.

И так клонируем репозиторий:

git clone https://github.com/aleksandrvolk/python-telegram-bot

Если все прошло ОК, то в каталоге root появился python-telegram-bot

Войдем в директорию python-telegram-bot и переименуем env.example в .env

Пишем в .env наш ТОКЕН, ограничение по пользователям пока отключено.

Пробуем запустить нашего бота:

cd python-telegram-bot
docker-compose up -d

Посмотрим логи:

docker-compose logs -f telegram-docker-bot

Проверяем :)

ИИ все сделал не с первого раза. Много чего можно улучшить, но базовый, который хотели, реализовали.

Комментарии (0)


  1. vsof
    16.09.2025 10:50

    Интересно, использовать бота, чтобы пересобрать образ и контейнер можно?


    1. aleksxx Автор
      16.09.2025 10:50

      Точно можно.


  1. CloudlyNosound
    16.09.2025 10:50

    Идея для стартапа: Телеграм-бот для управления контейнером, в котором он запущен.


  1. proDream
    16.09.2025 10:50

    Полтора года назад писал подобного бота на стриме без ии!)) Так и не дошли руки его доработать и превратить во что-то интересное) Правда вместо библиотеки докер использовал прописанные команды, зато есть БДшка со списком избранных команд и возможность выполнять пользовательские команды)

    Если интересно, расприватил репо: https://git.pressanybutton.ru/proDream/serverbot

    P.S. В клиенте тг на пк, в экспериментальных настройках можно включить отображение id в профиле пользователя и боты будут не нужны)