こんにちは、ナナオです。

サーバー維持費、気になりますよね。

今回は無料で使えるサービスでDiscordのBotを作っていきたいと思います。

初期設定

まずは土台作りとして、Discordとボットのコードを実装していきます。

Discord側の初期設定

以下のURLからデベロッパーコンソールにアクセスします。

Discord Developer Portal

ログインすると以下の画面になります。

「New Application」をクリックして新しくアプリケーションを作成します。

作成したアプリケーションのBotタブに行き、Reset Tokenでトークンを発行し、コピーしておきます。

Message Content Intentもオンにしておきます。

これをしないとBotがメッセージを読むことができないです。

ボットをサーバーに招待します。

InstallationタブからGuild InstallのScopesにbotを追加し、権限を設定します。(ここ重要)

今回は管理者権限にしました。

Install Linkをコピーし、ブラウザに貼り付けます。

インストールに成功すると、以下のようなウィンドウが表示されます。

サーバー側にも通知が飛びます。

Pythonの実装

コードを書いていきましょう。

パッケージをUVで作成します。

uv init --package discord-bot-playground

giboでignorefileに追記します。

gibo dump Python >> .gitignore

.envファイルを作成し、先ほど作成したDiscord botのトークンを貼り付けておきます。

DISCORD_BOT_TOKEN="xxx..."

必要なライブラリを追加します。

uv add discord.py dotenv

コードを書きます。

ここでは簡単なコマンド実行をしていきます。

デコレーターでめちゃくちゃ簡単に実装できます。

import os

from dotenv import load_dotenv
import discord


load_dotenv()

DISCORD_BOT_TOKEN = os.environ.get("DISCORD_BOT_TOKEN")
DISCORD_GUILD_ID = int(os.environ.get("DISCORD_GUILD_ID"))

guild_id = discord.Object(id=DISCORD_GUILD_ID)

intents = discord.Intents.default()

client = discord.Client(intents=intents)
tree = discord.app_commands.CommandTree(client)


@client.event
async def on_ready():
    await tree.sync(guild=guild_id)


@tree.command()
async def hello(interaction: discord.Interaction):
    """Says hello!"""
    await interaction.response.send_message(f"Hi, {interaction.user.mention}")

client.run(DISCORD_BOT_TOKEN)

ポイントとしてはon_readyでsyncする際にギルドIDをしているところです。

ギルドIDを指定することで、通常コマンドの反映に数十分ほどかかるところを即時反映することができます。

グローバルに使ってほしいボットを作る場合はsync関数の引数を消してください。

これでコマンドを使えるようになります。

あとは適当にGithubにリポジトリ作って公開します。

ghコマンド使えば楽に作れます。

❯ gh repo create
? What would you like to do? Push an existing local repository to github.com
? Path to local repository .
? Repository name discord-bot-playground
? Repository owner satodaiki
? Description
? Visibility Public
✓ Created repository satodaiki/discord-bot-playground on github.com
  https://github.com/satodaiki/discord-bot-playground
? Add a remote? Yes
? What should the new remote be called? origin
✓ Added remote git@github.com:satodaiki/discord-bot-playground.git

コミット、プッシュします。

git add --all && git commit -m "first commit" && git push origin main

koyebで公開する

ということでKoyebで公開したいと思います。

Koyebに公開するにあたり、ヘルスチェックエンドポイントが必要だということが分かりました。

追加実装します。

ライブラリを追加します。

uv add aiohttp

先ほどの実装に以下の実装を追加します。

import os

from aiohttp import web
from dotenv import load_dotenv
import discord

# ...中略...

# --- ヘルスチェック用のWebサーバー設定 ---
async def health_check(request):
    return web.Response(text="OK", status=200)

async def start_server():
    app = web.Application()
    app.router.add_get("/health", health_check)
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, "0.0.0.0", 8000)
    await site.start()
    print("Health check server started on port 8000")

@client.event
async def on_ready():
    # ヘルスチェックサーバーの開始
    await start_server()
    await tree.sync(guild=guild_id)
# ...中略...

あとはこれを動作させるためのDockerfileを用意します。

pyproject.tomlに以下のスクリプトを定義します。

[project.scripts]
main = "discord_bot_playground:main"

Dockerfileを定義します。

配置はdocker/Dockerfileにしました。

# ==========================================
# Stage 1: Builder
# ==========================================
FROM python:3.13.9-slim AS builder

WORKDIR /app

COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/

COPY pyproject.toml uv.lock README.md ./

# 依存ライブラリのみインストール
RUN uv sync --frozen --no-dev --no-install-project --compile

COPY src src

# プロジェクト本体をインストール
RUN uv sync --frozen --no-dev --compile

# ==========================================
# Stage 2: Runner
# ==========================================
FROM python:3.13.9-slim

WORKDIR /app

# builderステージで作った仮想環境をコピー
COPY --from=builder /app/.venv /app/.venv

# ソースコード等をコピー
COPY src src
COPY pyproject.toml .

# パスを通す
ENV PATH="/app/.venv/bin:$PATH"

# pyproject.tomlで定義されたコマンドを実行
CMD ["main"]

これで準備ができました。

koyeb側でwebアプリを作成します。

koyebのWebコンソールを開き、「Web Service」を選択して「Github」を選択します。

Public Github repositoryで先ほどプッシュしたリポジトリのURLを入力し、importをクリックします。

Dockerfileを選択し、Customize Dockerfile settingsからDockerfileの場所を選択します(Dockerfileをプロジェクトのルートディレクトリに配置している場合、この作業は不要です)

Nextをクリックします。

CPU EcoのFreeインスタンスを選択し、Nextをクリックします。

環境変数を入力します。

ヘルスチェックの情報を入力し、Deployをクリックします。

デプロイに成功しました。

サーバーが落ちないようにする

ここまで実装できましたが、koyebのFreeインスタンスは1時間以上経過すると落ちてしまいます。

Instances | Koyeb

なので定期的なエンドポイントへのアクセスが必要があります。

こんな時はGASを使いましょう。

Apps Script  |  Google for Developers

以下のような実装を行います。(URLをフェッチするだけの簡単な実装です)

// Discordサーバーが落ちないようにアクセスします。
function main() {
  const res = UrlFetchApp.fetch("https://<インスタンスのURL>/health")
  console.log(res.getContentText())
}

実装したら、トリガーを追加します。

一時間で落ちるので、30分に一回くらいで実行してあげるようにしましょう。

これで落ちなくなります。

感想

Dockerfileを用意するだけでパパっとデプロイできてしまいました。

恐ろしいほど簡単です。

アカウント一つにつき一つしかアプリを作ることができないようなので、その点は気を付けましょう。

参考

https://zenn.dev/amano_spica/articles/24c5f288cf9595

クイックスタート

Discord.py の Intents について #Python - Qiita

Discord botを作成するためのToken取得方法について|aki