Discord-бот на VPS 24/7: discord.py, Docker Compose и закрытый брандмауэр

Создание приложения в Developer Portal, интенты и ссылка-приглашение, минимальный бот на discord.py и discord.js, Dockerfile и compose.yaml с restart: unless-stopped, логи, обновление и шардинг. Ubuntu 24.04 / Debian 12.

9 октября 2026 г.·5 мин чтения·discord, python, docker, bot

Discord-бот отвечает, только пока его процесс держит соединение с gateway. На ноутбуке он выключается вместе с крышкой; на VPS — работает круглосуточно, перезапускается после падения и переживает ребут. Сначала разберём сторону Discord (приложение, токен, интенты, приглашение), затем соберём минимальный бот на discord.py и запустим его через Docker Compose. Для Node.js есть короткий вариант на discord.js.

Бот сам открывает исходящее WebSocket-соединение к Discord и получает события через него, а отвечает по HTTPS. Снаружи к серверу никто не подключается, поэтому не нужны ни открытые порты, ни домен, ни TLS. Исключение — боты на interactions endpoint URL (webhook вместо gateway), их здесь не рассматриваем.

Что нужно

  • Сервер на tanaka.host с Ubuntu 24.04 или Debian 12. Боту хватает очень мало памяти.
  • Docker с плагином Compose — Установка Docker.
  • Доступ по SSH — Подключение по SSH.
  • Аккаунт Discord и сервер, где у вас есть право Manage Server.

1. Приложение и бот

Откройте Discord Developer Portal → New Application, назовите и примите условия.

  • На странице General Information скопируйте Application ID — он нужен для ссылки-приглашения.
  • На странице Bot задайте имя и аватар, нажмите Reset Token и скопируйте токен. Discord показывает его один раз; потеряли — сбрасывайте снова и обновляйте на сервере.
  • Если бот только для ваших серверов, выключите Public Bot.

Токен — это пароль бота. Кто его имеет, может войти от имени бота и делать всё, что боту разрешено, на всех серверах, куда его добавили.

2. Интенты

Интенты определяют, какие события Discord присылает боту. Большинство включается в коде, но три привилегированных нужно дополнительно включить в портале (Bot → Privileged Gateway Intents):

Интент Включать, если бот
Presence Intent нужен онлайн-статус или активность участников
Server Members Intent отслеживает вход/выход участников или нужен полный список
Message Content Intent читает текст сообщений — префиксные команды вроде !hello, модерация

Без Message Content бот получает сообщения, но текст, embed’ы и вложения приходят пустыми (кроме личных сообщений, упоминаний бота и его собственных сообщений). Slash-командам этот интент не нужен — поэтому Discord их и рекомендует.

Пример ниже содержит slash-команду и одну префиксную, так что включите Message Content Intent. Остальные два оставьте выключенными.

После 100 серверов бот должен пройти верификацию, а привилегированные интенты — отдельное одобрение. Новые боты стройте на slash-командах.

3. Ссылка-приглашение

OAuth2 → URL Generator:

  • Scopes: bot и applications.commands. Первый добавляет пользователя бота на сервер, второй разрешает регистрировать slash-команды.
  • Bot Permissions — только нужное. Для примера: View Channels, Send Messages, Read Message History.

Откройте полученную ссылку в браузере, выберите сервер, подтвердите. Выглядит она так (вместо APP_ID — ваш Application ID):

https://discord.com/oauth2/authorize?client_id=APP_ID&permissions=68608&scope=bot+applications.commands

68608 — сумма трёх прав выше. Administrator не давайте: при утечке токена такой бот удалит каналы и забанит всех на каждом сервере.

4. Код на discord.py

mkdir -p /opt/discord-bot && cd /opt/discord-bot

Если бот уже в Git-репозитории — клонируйте его сюда и переходите к шагу 5. Иначе requirements.txt с закреплённой версией:

discord.py==2.7.1

bot.py:

import os

import discord
from discord.ext import commands

intents = discord.Intents.default()
intents.message_content = True

class Bot(commands.Bot):
    async def setup_hook(self) -> None:
        synced = await self.tree.sync()
        print(f"Synced {len(synced)} slash commands")

bot = Bot(command_prefix="!", intents=intents)

@bot.event
async def on_ready() -> None:
    print(f"Logged in as {bot.user} in {len(bot.guilds)} servers")

@bot.tree.command(description="Check that the bot is online")
async def ping(interaction: discord.Interaction) -> None:
    await interaction.response.send_message(f"Pong! Gateway latency {round(bot.latency * 1000)} ms")

@bot.command()
async def hello(ctx: commands.Context) -> None:
    await ctx.send(f"Hello, {ctx.author.display_name}!")

bot.run(os.environ["DISCORD_TOKEN"])

Что здесь важно:

  • Intents.default() включает все непривилегированные интенты, message_content = True добавляет тот, что вы включили в портале. Если код просит интент, выключенный в портале, соединение падает с PrivilegedIntentsRequired.
  • setup_hook выполняется один раз до подключения и регистрирует /ping. Для маленького бота синхронизация на каждом старте нормальна; для большого — только при изменении списка команд, endpoint ограничен по частоте.
  • Токен читается из переменной DISCORD_TOKEN, в коде его нет.

5. Docker

Dockerfile:

FROM python:3.12-slim

ENV PYTHONUNBUFFERED=1
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
RUN useradd --create-home bot
USER bot

CMD ["python", "bot.py"]

PYTHONUNBUFFERED=1 — чтобы print сразу попадал в логи. Внутри контейнера бот работает от непривилегированного пользователя.

.dockerignore, чтобы токен и история Git не попали в образ:

.env
.git
__pycache__

Токен в .env, читать может только root:

nano .env
DISCORD_TOKEN=PASTE_YOUR_TOKEN_HERE
chmod 600 .env

compose.yaml:

services:
  bot:
    build: .
    restart: unless-stopped
    env_file: .env
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
Настройка Зачем
restart: unless-stopped поднимать после падения и после ребута сервера, если вы сами не остановили через docker compose stop
env_file: .env токен передаётся в рантайме, а не зашивается в образ
logging ротация логов по 10 МБ × 3 файла, диск не переполнится

Секции ports: нет — бот ничего не слушает.

6. Запуск

docker compose up -d --build
docker compose ps

Статус — running или Up. В Discord наберите /ping в канале, который видит бот. Если slash-команда не появилась сразу — перезапустите клиент Discord. Потом !hello.

Логи и обновление

docker compose logs -f
docker compose logs --tail 100
docker compose logs --since 1h

Редкие реконнекты в логе — норма: Discord просит ботов переподключаться во время обслуживания, библиотека делает это сама.

Обновление кода — pull и пересборка, старый контейнер работает, пока собирается новый:

cd /opt/discord-bot
git pull
docker compose up -d --build
docker image prune -f

Обновить discord.py — поменять версию в requirements.txt и пересобрать. Перед мажорной версией читайте changelog.

Реальное потребление памяти — docker stats --no-stream, когда бот поработает какое-то время. Шпаргалка по командам — Docker Compose.

Тот же бот на discord.js

Docker-обвязка та же, меняются код и образ. discord.js 14 требует Node.js 18+. npm install создаст package-lock.json, нужный для сборки образа:

npm init -y
npm install discord.js

index.js:

const { Client, Events, GatewayIntentBits } = require('discord.js');

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.once(Events.ClientReady, async (readyClient) => {
  await readyClient.application.commands.set([
    { name: 'ping', description: 'Check that the bot is online' },
  ]);
  console.log(`Logged in as ${readyClient.user.tag}`);
});

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand()) return;

  if (interaction.commandName === 'ping') {
    await interaction.reply(`Pong! Gateway latency ${client.ws.ping} ms`);
  }
});

client.login(process.env.DISCORD_TOKEN);

Используется только интент Guilds, привилегированные не нужны. Dockerfile:

FROM node:22-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .
USER node

CMD ["node", "index.js"]

В .dockerignore добавьте node_modules. compose.yaml и .env из шага 5 подходят без изменений.

Вариант без Docker — systemd

Один бот отлично живёт и обычной службой systemd с виртуальным окружением. Порядок полностью описан в Telegram-бот под systemd; для Discord меняются две вещи: в env-файле DISCORD_TOKEN вместо токена Telegram, а pip install ставит discord.py вместо aiogram.

Закройте все входящие порты

Боту нужны только исходящие соединения, поэтому брандмауэр может блокировать всё, кроме SSH:

ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw enable

Docker публикует порты в обход UFW, но здесь ничего не публикуется. Подробнее — Брандмауэр UFW и firewalld и fail2ban.

Шардинг для больших ботов

Одно соединение (шард) обслуживает максимум 2 500 серверов, дальше Discord требует разделения. В discord.py замените commands.Bot на commands.AutoShardedBot — все шарды в одном процессе. В discord.js — ShardingManager, каждый шард в отдельном процессе Node.js, память считайте на каждый. Большинство ботов до этого не доходит.

Храните токен в секрете. .env — в .gitignore и .dockerignore. Не вставляйте токен в скриншоты и чаты; Discord сам сбрасывает токены, найденные в публичных репозиториях, и бот уходит в офлайн. Утёк — сразу Reset Token в портале, новый токен в .env, docker compose up -d.