Discord BotをVPSで24時間動かす|Docker Composeでメモリと再起動を実測

PRこの記事にはアフィリエイト広告が含まれます。紹介したサービスに申し込まれると、運営者が報酬を受け取ることがあります。評価や順位は報酬で変えていません。広告表記について

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分以上待ってから測りました。

discord.py 2.7.1(Python 3.13)の最小のボットのメモリ使用量(アイドル時)合計 33 MiB。半分以上の余裕が残るのは 1GB のプランからです。OS や Docker 自体が使う分は含みません。
ボット(discord.py): 33 MiB1GB 余裕97%合計 33 MiB
  • ボット(discord.py)33 MiB
コンテナ別の内訳を表で見る
グループコンテナメモリ
ボット(discord.py)bot-py33 MiB

測定日 。macOS 上の OrbStack の Ubuntu 26.04 マシン(arm64)の Docker。docker stats で5回(60秒間隔)取得した中央値。/ping に応答するだけのボットを、1つのサーバーに参加させた状態で起動し、10分以上待って測定。特権インテントは使わない。

discord.js 14.27.0(Node.js 24)の最小のボットのメモリ使用量(アイドル時)合計 41 MiB。半分以上の余裕が残るのは 1GB のプランからです。OS や Docker 自体が使う分は含みません。
ボット(discord.js): 41 MiB1GB 余裕96%合計 41 MiB
  • ボット(discord.js)41 MiB
コンテナ別の内訳を表で見る
グループコンテナメモリ
ボット(discord.js)bot-js41 MiB

測定日 。macOS 上の OrbStack の Ubuntu 26.04 マシン(arm64)の Docker。docker stats で5回(60秒間隔)取得した中央値。/ping に応答するだけのボットを、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 のプランで足ります。

メモリ 1GB のプランの比較(税込み、1ヶ月契約の安い順)
サービスとプラン1ヶ月契約最長契約の月額ファイアウォール新規申込
WebARENA Indigo1GB、1コア、SSD 20GB449円449円なし(時間課金のみ)未確認セキュリティグループ停止中
KAGOYA CLOUD VPS1GB、1コア、NVMe SSD 100GB550円506円12ヶ月(年額)初期状態は全許可セキュリティグループ停止中
シンVPS大容量メモリ 1GB、1コア、NVMe SSD 30GB750円451円36ヶ月未確認パケットフィルター受付中
ConoHa VPS1GB、2コア、SSD 100GB763円542円36ヶ月初回 466円初期状態で拒否セキュリティグループ受付中
さくらのVPS1G、2コア、SSD 50GB880円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 の選び方で比べています。

出典

  1. Gateway(Discord Developer Documentation)(に確認)
  2. Getting Started(Discord Developer Documentation)(に確認)
  3. Change Log(Discord Developer Documentation)(に確認)
  4. Receiving and Responding(Discord Developer Documentation)(に確認)
  5. API Reference(discord.py)(に確認)
  6. discord.js 14.27.0 README(GitHub)(に確認)
  7. Start containers automatically(Docker 公式)(に確認)
  8. docker container run(Docker 公式)(に確認)
  9. Configure logging drivers(Docker 公式)(に確認)
  10. JSON File logging driver(Docker 公式)(に確認)
  11. Supported secret scanning patterns(GitHub Docs)(に確認)
  12. Deploy for Free(Render 公式ドキュメント)(に確認)