Discord Bot は、自分のパソコンで動かしている間しか応答しません。パソコンを閉じても動き続けるようにするには、24時間動いているサーバーに置く必要があります。
この記事では、Bot を VPS の Docker Compose で常時動かす構成を作ります。discord.py(Python)と discord.js(Node.js)の両方で最小の Bot を作り、メモリの使用量と、落ちたときに自動で戻るかを実際に確かめました。
先に結論
- 最小の Bot なら、メモリは 50MiB もかかりません。 手元では discord.py が約33MiB、discord.js が約41MiB でした。VPS はメモリ 1GB の最も安いプランで足ります。
restart: unless-stoppedを付ければ、落ちても VPS を再起動しても自動で戻りました。 ただしdocker killで止めたものは戻りませんでした。- discord.js では、
errorを受け取る処理を必ず書いてください。 書かずにいたところ、ネットワークが切れた瞬間にプロセスごと終了し、通信が戻るまでに12回も再起動を繰り返しました。処理を足すと、同じ条件でもプロセスは終了しませんでした。 - 再起動を繰り返しすぎると、トークンが無効になります。 Discord への接続の開始は24時間に1000回までで、超えるとトークンがリセットされます(Discord 公式ドキュメント)。
Bot を動かすサーバーに必要なもの
Discord Bot は、Bot の側から Discord に接続しに行き、その接続を保ったまま命令を受け取ります。そのため、外から接続を受け付けるポートも、ドメインも、HTTPS の証明書も要りません。 VPS のファイアウォールは、SSH 以外をすべて閉じたままで動きます。
例外:Interactions Endpoint URL を使う場合
Discord には、命令を HTTP で Bot に送ってもらう方式(Interactions Endpoint URL)もあります。この方式では、外から接続できる公開の URL が必要です。discord.py や discord.js で普通に作る Bot は、この方式を使いません。2つの方式は同時に使えないため、Developer Portal の「Interactions Endpoint URL」は空欄のままにしておきます。
VPS を借りたら、先に VPS の初期設定(SSH の鍵ログイン、ufw、自動更新)を済ませておいてください。
実測:Bot のメモリ使用量
/ping に応答するだけの最小の Bot を、1つのサーバーに参加させた状態で動かし、10分以上待ってから測りました。
どちらも、メモリ 1GB のプランの数%しか使いません。参加するサーバーが増えたり、機能を足したりすればメモリは増えますが、個人や小さなコミュニティで使う Bot なら、1GB のプランで余裕があります。1台の VPS に、複数の Bot を同居させることもできます。
構成:Docker Compose で動かす
ここでは discord.py の例を載せます。discord.js 版は、この節の最後に折りたたんで載せています。
ファイルの構成
discord-bot/
├── compose.yaml
├── .env.local ← トークン(Git に載せない)
└── py/
├── Dockerfile
├── requirements.txt
└── bot.py
Bot 本体(py/bot.py)
/ping に応答するだけの Bot です。特別な権限(特権インテント)は使いません。
import logging
import os
import discord
from discord import app_commands
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
client = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(client)
@tree.command(name="ping", description="ボットの応答速度を返します")
async def ping(interaction: discord.Interaction) -> None:
await interaction.response.send_message(f"pong({client.latency * 1000:.0f}ms)")
synced = False
@client.event
async def on_ready() -> None:
global synced
# on_ready は接続し直すたびに呼ばれることがあるため、コマンドの登録は起動ごとに1回だけ
if not synced:
synced = True
for guild in client.guilds:
tree.copy_global_to(guild=guild)
await tree.sync(guild=guild)
logging.info("ready: %s, guilds=%d", client.user, len(client.guilds))
client.run(os.environ["DISCORD_TOKEN"], log_handler=None)
起動すると、PyNaCl is not installed と davey is not installed という警告が出ます。どちらも音声通話の機能に使うもので、音声を使わない Bot なら無視して構いません。
Dockerfile と requirements.txt
FROM python:3.13-slim
RUN useradd --create-home bot
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py .
USER bot
CMD ["python", "-u", "bot.py"]
requirements.txt には discord.py==2.7.1 の1行だけを書きます。Bot は root ではなく、専用のユーザー(bot)で動かしています。
compose.yaml
services:
bot:
build: ./py
env_file: .env.local
restart: unless-stopped
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
restart: unless-stoppedは、落ちたときや VPS を再起動したときに、自動で立ち上げ直す設定です。loggingは、ログの大きさを 10MB×3ファイルまでに抑える設定です。Docker の公式ドキュメントによると、初期設定のログは大きさに上限がなく、放っておくとディスクを使い切ることがあります。
トークンを置いて起動する
トークンは、Discord Developer Portal の「Bot」の画面にある「トークンをリセット」で発行します。発行したトークンは、そのときしか表示されません。
cd discord-bot
nano .env.local
開いた画面に DISCORD_TOKEN=発行したトークン の1行を書いて保存します(nano では Ctrl+O、Enter、Ctrl+X)。コマンドの引数にトークンを書くと、コマンドの履歴に残るため避けます。
chmod 600 .env.local
docker compose up -d --build
docker compose logs -f
ログに ready: と表示されれば、Discord への接続に成功しています。Discord で /ping を実行し、pong(○○ms) と返ってくることを確かめてください。
discord.js 版の bot.mjs、package.json、Dockerfile(検証で使ったもの)
import { Client, Events, GatewayIntentBits, SlashCommandBuilder } from 'discord.js';
const ping = new SlashCommandBuilder().setName('ping').setDescription('ボットの応答速度を返します');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, async (ready) => {
for (const guild of ready.guilds.cache.values()) {
await guild.commands.set([ping.toJSON()]);
}
console.log(`ready: ${ready.user.tag}, guilds=${ready.guilds.cache.size}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== 'ping') return;
await interaction.reply(`pong(${client.ws.ping}ms)`);
});
// 切断と再接続をログに残す
client.on(Events.ShardReconnecting, (id) => console.log(`shard ${id} reconnecting`));
client.on(Events.ShardReady, (id) => console.log(`shard ${id} ready`));
client.on(Events.ShardError, (error, id) => console.log(`shard ${id} error: ${error.message}`));
// error を受け取る処理がないと、通信エラーでプロセスごと終了する
client.on(Events.Error, (error) => console.log(`client error: ${error.message}`));
client.login(process.env.DISCORD_TOKEN);{
"name": "discord-bot",
"private": true,
"type": "module",
"dependencies": {
"discord.js": "14.27.0"
}
}FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY bot.mjs .
USER node
CMD ["node", "bot.mjs"]package-lock.json は、npm install --package-lock-only で作ります。compose.yaml の build: は ./js に変えます。
トークンは Git に載せない
トークンがあれば、誰でもあなたの Bot を操作できます。.env.local は .gitignore に書き、GitHub などに載せないでください。漏れたと思ったら、すぐに「トークンをリセット」で作り直します。古いトークンは使えなくなります。なお、GitHub は公開リポジトリに載った Discord のトークンを検出する仕組みを持っています。
実測:落ちたとき、VPS を再起動したときに戻るか
discord.py と discord.js の両方で、Bot が止まる場面を再現しました。
| 試したこと | 結果(両方とも同じ) |
|---|---|
| Bot のプロセスを強制終了(クラッシュの代わり) | 自動で再起動した |
| VPS(マシン)を再起動 | 自動で起動し、Discord に接続した |
docker kill で止める |
再起動しなかった。VPS を再起動しても止まったまま |
docker compose stop で止めてから VPS を再起動 |
止まったまま(discord.js 版で確認) |
docker kill や docker compose stop で止めたものは、「自分で止めた」とみなされ、自動では戻りません。止めた Bot を動かすときは、docker compose start か docker compose up -d を実行してください。
ネットワークが切れたとき
コンテナからインターネットへの通信を、一定の時間だけ止めて試しました。
| 止めた時間 | discord.py | discord.js(error の処理あり) |
|---|---|---|
| 30秒 | 切断されなかった | 切断されなかった |
| 3分 | 切断され、通信が戻ると自動で接続し直した | 接続を何度も試し、通信が戻ると接続し直した |
どちらも、プロセスを再起動せずに復帰しました。ただし discord.js は、次の処理を書いていないと結果が変わります。
discord.js で必ず書く処理
discord.js で最初に試したときは、コンテナをネットワークから3分外すと、次のエラーでプロセスごと終了しました。
Error: getaddrinfo EAI_AGAIN discord.com
Emitted 'error' event on Client instance at:
discord.js は、通信のエラーを error という出来事(イベント)で知らせます。Node.js では、このイベントを受け取る処理がないと、プロセスが終了します。Docker が再起動してくれましたが、通信が戻るまでに12回も再起動を繰り返しました。
次の処理を足してから同じ条件で試すと、プロセスは終了せず(再起動0回)、ネットワークが戻ったところで接続し直しました。
client.on(Events.Error, (error) => console.log(`client error: ${error.message}`));
client.on(Events.ShardError, (error, id) => console.log(`shard ${id} error: ${error.message}`));
1行目がプロセスの終了を防ぐ処理で、2行目は接続のエラーをログに残す処理です。手元では2つを同時に足して試しました。
再起動の繰り返しで、トークンが無効になる
Discord の公式ドキュメントによると、Bot が Discord への接続を新しく始められるのは、24時間に1000回までです。上限に達すると、Bot の接続はすべて切られ、トークンがリセットされます(オーナーにはメールで知らせが届きます)。
手元でも、Bot を1つだけ動かして、残りの回数を Discord の API で確かめました。起動したときと、3分の切断でセッションが無効になり新しく接続し直したときに、1つずつ減りました。短い切断から元のセッションを再開できた場合は、数えられません(Discord 公式ドキュメント)。
Docker の自動再起動は、失敗が続くと間隔を延ばしますが、最大でも1分おきです。Discord に接続した後で落ちる状態を放っておくと、1分おきでも1日に約1400回になり、上限を超える計算です。10秒以上動いてから落ちる場合は間隔が短く戻るため、さらに多くなります。次の点に気をつけてください。
- Bot を更新したら、
docker compose logsでready:が出るのを確かめる - 再起動の回数を
docker inspect -f '{{.RestartCount}}' $(docker compose ps -q bot)でときどき確かめる - discord.js では、上の
errorの処理を必ず書く
restart は on-failure ではなく unless-stopped に
再起動の回数に上限を付けられる on-failure という設定もあります。ただし Docker の公式ドキュメントによると、on-failure は Docker 自体が再起動したとき(VPS の再起動時など)には Bot を立ち上げ直しません。24時間動かす Bot には unless-stopped が向いています。
なお、検証の途中では、Bot を2つ同時に動かし、接続し直すたびにコマンドを Discord に登録していたため、登録がレート制限(429)にかかりました。上の discord.py の例では、登録を起動ごとに1回にしています。手元では、3分の切断から接続し直しても登録し直さないことを確かめました。
特別な権限(特権インテント)を使う場合
メンバーの一覧、オンライン状態、サーバー内の一般のメッセージの本文を読む Bot は、Developer Portal で「Privileged Gateway Intents」を有効にする必要があります。スラッシュコマンドで動く Bot には不要です。
Discord は 2026年6月に、この権限の扱いを変えました。現在は、Bot が参加しているすべてのサーバーで、Bot から見える利用者が合わせて1万人に達するまでは、Developer Portal でオンにするだけで使えます。1万人に達したら申請が必要です。個人や小さなコミュニティの Bot なら、申請はまず不要です。
VPS を選ぶ
Bot だけを動かすなら、メモリ 1GB のプランで足ります。
| サービスとプラン | 1ヶ月契約 | 最長契約の月額 | ファイアウォール | 新規申込 |
|---|---|---|---|---|
| WebARENA Indigo1GB、1コア、SSD 20GB | 449円 | 449円なし(時間課金のみ) | 未確認セキュリティグループ | 停止中 |
| KAGOYA CLOUD VPS1GB、1コア、NVMe SSD 100GB | 550円 | 506円12ヶ月(年額) | 初期状態は全許可セキュリティグループ | 停止中 |
| シンVPS大容量メモリ 1GB、1コア、NVMe SSD 30GB | 750円 | 451円36ヶ月 | 未確認パケットフィルター | 受付中 |
| ConoHa VPS1GB、2コア、SSD 100GB | 763円 | 542円36ヶ月初回 466円 | 初期状態で拒否セキュリティグループ | 受付中 |
| さくらのVPS1G、2コア、SSD 50GB | 880円 | 807円12ヶ月 | 一部だけ制限パケットフィルター | 一部のみ受付 |
に各社の公式ページで確認した価格です。1ヶ月契約は更新時の価格で、時間課金・日額課金しかない会社は月額の上限を載せています(ConoHa VPS は一括前払いの1ヶ月の価格で、時間課金はこれより上限が高くなります)。最長契約の月額は、最も長い契約期間の料金を1ヶ月あたりに換算した額です。ファイアウォールは、VPS の管理画面で設定できるものの初期状態です。申込の受付状況は変わりやすいため、契約の前に公式ページで確かめてください。
- ConoHa VPS: 初回の価格は 2026年10月29日までのキャンペーン価格で、申し込み時だけ適用されます
無料で使えるクラウドもありますが、24時間つなぎっぱなしにする Bot には向かないものが多いです。たとえば Render の無料プランは、Web サービスなどが対象で、Bot のような常駐型のサービス(Background Worker)は対象外です。Web サービスも、15分間アクセスがないと止まります(Render の公式ドキュメント)。
VPS なら、Bot のほかに n8n などを同じサーバーで動かすこともできます。各社の違いは セルフホスト用 VPS の選び方で比べています。