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.

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.