Коротко. Добавьте mysql2 в своего бота discord.js, при запуске бота создайте один пул подключений из переменных окружения и выполняйте все запросы через него с плейсхолдерами ?. На PlayNHost каждый тариф для бота включает одну базу MySQL: создайте её на странице Базы данных, вставьте её данные во вкладку Окружение, и код ниже заработает как есть. База принимает подключения с ваших серверов PlayNHost и из phpMyAdmin, но не с вашего компьютера. Мы проверили этот код 6 ноября 2026 года с discord.js 14.28.1, mysql2 3.24.6 и Node.js 22 на MariaDB 10.11 с учётной записью базы, настроенной как у нас (20 подключений, лимит запроса 120 секунд): запуск, таблицу и обработчик /warn, но не настоящий вход в Discord.
Откройте сервер вашего бота в панели и нажмите Базы данных.
Нажмите Новая база данных. Поле Метка необязательно; в поле Будет создана как показано полное имя. Нажмите Создать базу данных.
Откроется страница новой базы (позже её можно открыть в разделе Ваши базы данных). На карточке Подключение указаны Хост, Порт, База данных, Имя пользователя и Пароль. В разделе Готовый конфиг в списке Конфиг для есть сниппет Node.js (mysql2) с теми же уже подставленными значениями.
2. Сохраните данные в разделе Окружение
Откройте страницу Бот, вкладку Окружение, и нажмите Вставить файл .env.
Вставьте эти пять строк со значениями с карточки Подключение:
const { Client, Events, GatewayIntentBits, InteractionContextType, MessageFlags, REST, Routes, SlashCommandBuilder } = require('discord.js');
const mysql = require('mysql2/promise');
// One pool for the whole bot. The values come from the Environment pane.
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 5,
});
const commands = [
new SlashCommandBuilder()
.setName('warn')
.setDescription('Warn a member and count their warnings')
.setContexts(InteractionContextType.Guild)
.addUserOption((o) => o.setName('user').setDescription('Who to warn').setRequired(true))
.addStringOption((o) => o.setName('reason').setDescription('Why').setRequired(true).setMaxLength(200))
.toJSON(),
];
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, async () => {
await new REST().setToken(process.env.DISCORD_TOKEN).put(Routes.applicationCommands(client.user.id), { body: commands });
console.log(`Logged in as ${client.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== 'warn' || !interaction.guildId) return;
const user = interaction.options.getUser('user', true);
const reason = interaction.options.getString('reason', true);
try {
await pool.execute('INSERT INTO warnings (guild_id, user_id, reason) VALUES (?, ?, ?)', [interaction.guildId, user.id, reason]);
const [rows] = await pool.execute('SELECT COUNT(*) AS total FROM warnings WHERE guild_id = ? AND user_id = ?', [interaction.guildId, user.id]);
await interaction.reply(`${user} has ${rows[0].total} warning(s).`);
} catch (err) {
console.error(err);
if (!interaction.replied) await interaction.reply({ content: 'Could not save the warning. Try again later.', flags: MessageFlags.Ephemeral }).catch(console.error);
}
});
async function main() {
await pool.query(`CREATE TABLE IF NOT EXISTS warnings (
id INT AUTO_INCREMENT PRIMARY KEY,
guild_id VARCHAR(20) NOT NULL,
user_id VARCHAR(20) NOT NULL,
reason VARCHAR(200) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY guild_user (guild_id, user_id)
)`);
console.log('Database ready');
await client.login(process.env.DISCORD_TOKEN);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Что делает код: при запуске бот создаёт таблицу warnings, если её нет, выводит Database ready и затем входит в Discord. /warn сохраняет предупреждение и отвечает общим числом предупреждений участника. discord.js отдаёт ID Discord строками, потому что они слишком велики для числа JavaScript, поэтому они хранятся как текст. Плейсхолдеры ? отправляют текст участника в базу отдельно от SQL, поэтому причина вроде '); DROP TABLE warnings; -- сохраняется как обычный текст.
Две строки не дают боту упасть. setContexts(InteractionContextType.Guild) и проверка !interaction.guildId запрещают /warn в личных сообщениях, где нет сервера, к которому можно привязать предупреждение. Блок try/catch отвечает коротким приватным сообщением, если запрос не удался. Без него discord.js передаёт ошибку своему событию error, а если его никто не слушает, Node.js останавливает бота. В нашем тесте мы удалили таблицу и выполнили /warn: бот записал в лог ER_NO_SUCH_TABLE, ответил Could not save the warning. Try again later. и продолжил работать.
4. Запуск
Загрузите index.js и package.json в разделе Файлы. Не загружайте node_modules.
Нажмите Запустить и откройте Консоль. При запуске устанавливаются пакеты, затем появляются [PlayNHost] Starting your bot, Database ready и Logged in as ....
В Discord введите /warn. Чтобы посмотреть строки, откройте страницу базы данных и нажмите Открыть в phpMyAdmin.
Лимиты, которые важны для вашего пула
20 одновременных подключений на пользователя базы. Считается каждый процесс с тем же логином: ваш бот, его вторая копия, панель управления. Подключение 21 отклоняется с ошибкой ER_USER_LIMIT_REACHED. В нашем тесте пул с connectionLimit: 30 упал с этой ошибкой, как только 30 запросов выполнились одновременно. Пул из 5 никогда не открывает больше 5: лишние запросы ждут свободного подключения.
Запрос, который выполняется дольше 120 секунд, прерывается. MariaDB завершает его с ошибкой 1969, Query execution was interrupted (max_statement_time exceeded). В нашем тесте это произошло через 120.03 секунды. Запросы бота обычно занимают миллисекунды, поэтому это касается только зависшего запроса.
500 MB на базу данных. На странице Базы данных видно, сколько занято. При 500 MB запись блокируется, пока вы не удалите строки.
Резервные копии сервера не включают базу данных. Время от времени экспортируйте её из phpMyAdmin.
Ошибки и что они значат
Это точные сообщения, которые mysql2 выдал в нашем тесте:
ER_ACCESS_DENIED_ERROR ... Access denied for user: неверный DB_USER или DB_PASSWORD. После Сбросить пароль на карточке Подключение старый пароль перестаёт работать сразу: замените DB_PASSWORD и перезапустите бота.
ECONNREFUSED: по этому хосту и порту подключение никто не принял. Проверьте DB_HOST и DB_PORT. Отсутствующий DB_HOST тоже приводит сюда, потому что mysql2 тогда подключается к localhost, где никакая база не работает: переменной нет во вкладке Окружение, или бота не перезапустили после её добавления.
ENOTFOUND: DB_HOST написан с ошибкой, поэтому такого имени не существует.
ETIMEDOUT: за 10 секунд никто не ответил, это ожидание mysql2 по умолчанию. Какое бы ни было сообщение, подключение с вашего компьютера не сработает: база принимает подключения только с серверов PlayNHost и из phpMyAdmin.
ER_USER_LIMIT_REACHED ... has exceeded the 'max_user_connections' resource (current value: 20): открыто слишком много подключений, см. лимиты выше. Обычно это новое подключение на каждую команду, которое никогда не закрывается, или вторая копия бота.
Установка discord.js и mysql2 с нуля дала в нашем тесте пик в 147 MB памяти (кэш файлов не считается) в контейнере с лимитом 512 MB, как у нашего самого маленького тарифа для ботов. От запуска до момента, когда Discord отклонил наш тестовый токен, бот использовал не больше 31 MB; он создал таблицу, но так и не вошёл в Discord. Бот, который вошёл и находится на многих серверах, использует больше: Сколько RAM нужно Discord-боту
Мы используем файлы cookie, чтобы сайт работал, чтобы понимать, как им пользуются, и чтобы оценивать наш маркетинг. Вы можете принять всё, принять только необходимые файлы cookie или выбрать по категориям.Мы используем файлы cookie, чтобы сайт работал, чтобы понимать, как им пользуются, и чтобы оценивать наш маркетинг. Здесь они включены по умолчанию. Вы можете отключить всё, что не хотите.Читать политику конфиденциальности