私は大学のサークルのDiscordでBotを開発・運用しています。
最近、BotのサーバーをOracle Cloud Infrastructure (OCI)に移動しました。
OCIはLinuxをターミナルから操作する必要があり、苦戦したので備忘録・サークルの引き継ぎ資料としてこの記事を書きます。
👇サーバーに関する記事はこちら
目次
前書き
・想定している読者
・OCIの登録
・AIに頼ろう
環境構築
・メモリスワップ
・タイムアウト対策
・Dockerの作成
・シングルコアの対策
push後の自動反映
・push後に自動で反映させる
デプロイ準備
・リポジトリのクローンと環境定数
常時起動
・Botを常時起動させる
コードの変更
・Unicornを消す
運用に向けて
・運用・保守コマンド
解決する問題
・環境構築
・Botの常時起動
・push後の自動再起動
・キーの管理
・Dockerの作成手順
・シングルコアの対策
想定している読者
・今からDiscord Botを開発する人
・OCIの操作に迷っている人
・OCIを使っているがBotがうまく動作しない人
OCIの登録
この記事では、OCIの登録は省略します。
👇この記事が分かりやすくておすすめです。
この記事では、それらができた上で行うべき準備や環境構築などを紹介します。
AIに頼ろう
コマンドは必要事項に書き換えたらコピペでできると思います。
エラーが出た場合は、その部分をコピペしてChat GPTなどに投げるといいと思います。
人によって環境が異なる場合があるため、記事のURLをAIに投げて進めると良いかもしれません。
環境構築
メモリスワップ
OCIの無料枠はメモリが1GBしかないため、pipでもフリーズしてしまいます。
そのため、OCIの無料枠で使える200GBのストレージのうち、幾らかをメモリにスワップします。
私は10GBに拡張してBotを運用しています。
1. 既存のスワップを確認(何も出なければなし)
free -h
2. 10GBのスワップファイルを作成
sudo dd if=/dev/zero of=/swapfile bs=1M count=10240
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
3. 再起動後も有効にする
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
パッケージの更新とインストール
Dockerコマンドを使えるようにしますが、実体は Podman がインストールされます。
sudo dnf update -y
sudo dnf install -y git nano docker
sudo systemctl enable --now docker
# ユーザーをdockerグループに追加(sudoなしで実行するため)
# ※ Oracle Linuxでは以下の手順が必要な場合がある
sudo usermod -aG docker opc
newgrp docker
タイムアウト対策
以前、OCIではなくKoyebとUptimeRobotで運用していた時と比べ、OCIではBotの動作が不安定だと感じることがあります。
そもそも、処理に3秒以上かかるとタイムアウトしてしまうため、無料で使える弱小CPUなどでは対策が必要になります。
Botのコード側での対策
これを防ぐため、コマンド処理の最初にdefer、最後にfollowupという構造で記述することをおすすめします。
@bot.slash_command(name="my_command", description="安全なコマンドのテンプレ")
async def my_command(interaction: discord.Interaction):
# 【最初】にdefer() を呼ぶ。
# これにより「考え中...」が表示され、猶予が15分に伸びる。
# ephemeral=True にすると、コマンドを読んだ人にだけ見えるメッセージになる。
await interaction.response.defer(ephemeral=False)
try:
# --- ここにメインの処理を書く ---
# 結果は必ずfollowupで送信する。
# deferの後はresponse.send_messageが使えない。
await interaction.followup.send(content=result_text)
except Exception as e:
# エラー時に「考え中...」のままになることを防ぐためにtry-exceptでキャッチする。
await interaction.followup.send(f"エラーが発生しました: {e}")
サーバー側での対策
OCIのMTUサイズ(9000)とIPv6設定が原因だったりすることもあるそうです。
(正直、効果があったのかわからない...)
現在の接続名を確認 (例: "Wired connection 1" や "ens3" など)
nmcli connection show
表示された接続名に対してMTUを設定
#例) sudo nmcli connection modify "Wired connection 1" 802-3-ethernet.mtu 1500
sudo nmcli connection modify (接続名) 802-3-ethernet.mtu 1500
変更されたことを確認する
# 接続名の行が "mtu 1500" となっているか確認する
ip addr | grep mtu
IPv6を無効にする
設定ファイルの作成
sudo bash -c 'cat <<EOF > /etc/sysctl.d/99-disable-ipv6.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOF'
設定を適用
sudo sysctl --system
ハードウェアオフロードとDNS設定
sudo dnf install -y ethtool
sudo ethtool -K ens3 tx off rx off tso off gso off gro off
DNSをGoogle Public DNSに変更
sudo nmcli connection modify "Wired connection 1" ipv4.dns "8.8.8.8 8.8.4.4"
sudo nmcli connection modify "Wired connection 1" ipv4.ignore-auto-dns yes
設定の反映
sudo nmcli connection up "Wired connection 1"
Dockerの作成
DockerFileはBotのソースコード上に書いてください。
ここにライブラリなどの設定を書いていきます。
(詳しくない人はAIに任せた方が確実だと思います)
以下にテンプレを載せます。
FROM python:3.9
WORKDIR /bot
# --- 1. 日本語環境とタイムゾーン設定 ---
# ログの文字化け(豆腐化)を防ぎ、時間をJST(日本時間)にします
ENV LANG=ja_JP.UTF-8 \
TZ=Asia/Tokyo \
PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y locales tzdata && \
localedef -f UTF-8 -i ja_JP ja_JP.UTF-8 && \
rm -rf /var/lib/apt/lists/*
# --- 2. 必須ツールのインストール ---
# ヘルスチェック用の curl などを入れます
# ※ TeXなど、特定の重いツールが必要な場合はここに追加してください
RUN apt-get update && apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
# --- 3. Pythonライブラリの導入 ---
COPY requirements.txt /bot/
RUN pip install --no-cache-dir -r requirements.txt
# ソースコードのコピー
COPY . /bot
# --- 4. ヘルスチェックと起動 ---
# SystemdがBotの生存確認(死活監視)をするために必要です
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl --fail http://localhost:8000 || exit 1
# Webサーバー(Uvicorn)ごとBotを起動
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
シングルコア環境での最適化
OCIの無料枠ではCPUがシングルコアなこともあり、処理によっては落ちてしまうことがあります。
そのため、上に書いたdeferと、さらに別の対策を加えます。
非同期化
スプレッドシートの読み書きなどは run_in_executor を使い、イベントループを止めないようにします。
import asyncio
# 良い例:ブロッキング処理を別スレッドへ
loop = asyncio.get_running_loop()
cell_value = await loop.run_in_executor(None, lambda: worksheet.acell("A1").value)
# CPUを他のタスクに譲るための短いsleep
await asyncio.sleep(0.1)
レートリミット対策
「429 Too Many Requests」エラーが出た場合、待機してからリトライする仕組みを入れます。
async def safe_reaction(message, emoji, retry=3):
try:
await message.add_reaction(emoji)
except discord.HTTPException as e:
# 429エラー または エラー文言に "rate" が含まれる場合
if retry > 0 and (e.status == 429 or "rate" in str(e).lower()):
await asyncio.sleep(2.0) # 2秒待機
await safe_reaction(message, emoji, retry - 1)
push後に自動で反映
現状、pushしてもサーバー上のBotは更新されません。
そこで、GitHub Actionを用いて、自動で再起動するようにします。
サーバーではなく、Botのコードの方に.githubを作成し、その中にworkflowsというフォルダを作成してください。そして、その中にdeploy.ymlというファイルを作成してください。
以下にdeploy.ymlのテンプレを載せます。
name: Deploy to OCI
on:
push:
branches:
- main # mainブランチにpushされたら実行
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: SSH Remote Commands
uses: appleboy/ssh-action@v1.0.3
with:
# 手順3で設定したシークレットを読み込みます
host: ${{ secrets.HOST_IP }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.PRIVATE_KEY }}
port: 22
timeout: 15m # Microインスタンスはビルドが遅いので長めに確保
script: |
# 1. リポジトリのディレクトリへ移動
# ★重要: 下のディレクトリ名は実際の環境に合わせて書き換えてください
cd /home/opc/Your-Repo
# 2. 最新のコードを取得
git pull origin main
# 3. Dockerイメージを再ビルド
# OCIはdockerコマンドでpodmanが動くようになっています
docker build -t my-discord-bot .
# 4. サービスを再起動して変更を反映
sudo systemctl restart discord-bot.service
# 5. お掃除 (溜まった古いイメージを削除)
docker image prune -f
ここにあるsecrets.HOST、ecrets.USERNAME、secrets.PRIVATE_KEYは以下の手順で確認できます。
認証情報の準備
GitHubがサーバーと接続できるように鍵を設定します。
ssh-keygen -t ed25519 -f github_action_key -N "" -C "github-actions"
これを実行すると、以下のようなものが出たら成功です。
Generating public/private ed25519 key pair.
Your identification has been saved in github_action_key
Your public key has been saved in github_action_key.pub
The key fingerprint is:
SHA256:rU1g+... (ランダムな文字列) ... github-actions
The key's randomart image is:
+--[ED25519 256]--+
| |
| . |
| o |
| . + + |
| o S o |
| . + O . |
| . B = . |
| o * + |
| E.o. |
+----[SHA256]-----+
これで公開鍵と秘密鍵が生成でき、それぞれをサーバーとGitHubに登録します。
公開鍵をサーバーに登録する
# サーバーではなくパソコン上で実行し、`ssh-ed25519 AAAAC3NzaC... github-actions`を改行を含まず**全て**コピーしてください。
cat github_action_key.pub
その後、以下のコマンドを実行し、一番下の行に付け加えてください。
nano ~/.ssh/authorized_keys
最後に、以下のコマンドを実行してPCと接続できれば成功です。
# サーバーではなくパソコン上で実行
ssh -i github_action_key opc@(サーバーIPアドレス)
#例) ssh -i github_action_key opc@123.4.56.789
秘密鍵をGitHubに登録する
以下のコマンドを執行し、-----BEGIN OPENSSH PRIVATE KEY-----から-----END OPENSSH PRIVATE KEY-----まで、最初と最後の行を含めて全てコピーしてください
# サーバーではなくパソコン上で実行
cat github_action_key
その後、リポジトリのページのSettings > Secrets and variables > Actions > New repository secretを開き、ワークフローのNameにPRIVATE_KEYを記入し、Secretにコピーした内容をペーストしてください。
これでキーの設定が完了しました。
その後、pushしたら反映され、それ以降、pushがあると自動で再起動されます。
デプロイ準備
リポジトリのクローンと環境定数
Bot自体のソースコードはGitHubで管理していることを前提に書きます。
リポジトリからcloneをしてソースコードはGitHubから引っ張れるので、キーなどの環境定数はOCI上に書く必要があります。
クローン
git clone (リポジトリのURL)
cd Your-Repo
.envの作成
nano .env
nanoを実行すると何もない画面に切り替わると思います。ここに、以下のように環境定数を書いていきます。
注意して欲しいのは、=の間に を入れないでください。
BOT_KEY=123456789
CHANNEL_ID=123456789
ビルド
docker build -t my-discord-bot .
# テスト起動 (Ctrl+Cで停止)
docker run --rm \
--env-file .env \
-v $(pwd)/google_secret.json:/bot/google_secret.json:Z \
my-discord-bot
Systemdによる常時起動
docker run -d だけではサーバーとパソコンの接続が切れた時にBotも一緒に落ちてしまいます。
そこで、systemdを用いて常時起動させます。
ファイルの作成
sudo nano /etc/systemd/system/discord-bot.service
nanoエディター内で以下のコードをコピペしてください。
[Unit]
Description=Discord Bot Container Service
Requires=network-online.target
After=network-online.target
[Service]
# コンテナをフォアグラウンドで起動し、systemdに監視させる
Type=simple
Restart=always
# 停止しても5秒後に自動で再起動
RestartSec=5s
User=opc
# ★重要: ディレクトリ名は実際の環境に合わせて変更してください
WorkingDirectory=/home/opc/Your-Repo
# Podman起動コマンド
# --rm: 停止時にコンテナを削除 (名前衝突防止)
# -p 8000:8000: Uvicornのポートをホスト側と繋ぐ (ヘルスチェック用)
# --memory: Microインスタンス(1GB)用に調整。Ampereなら4gでもOK
ExecStart=/usr/bin/podman run --name my-bot --rm \
-p 8000:8000 \
--env-file /home/opc/Your-Repo/.env \
--memory 900m \
my-discord-bot
# 停止コマンド (10秒待って終了しなければ強制終了)
ExecStop=/usr/bin/podman stop -t 10 my-bot
[Install]
WantedBy=multi-user.target
有効にして起動
# 設定の反映と自動起動の有効化
sudo systemctl daemon-reload
sudo systemctl enable discord-bot.service
# 起動
sudo systemctl start discord-bot.service
# 状態確認 (Active: active (running) ならOK)
sudo systemctl status discord-bot.service
コードの変更
私のようなKoyebを利用したデプロイをOCIに切り替える時、コードの変更は不要ですが、一点だけ変更が必要です。
Unicornを消す
main.pyの一番下とかに以下のようなコードがあれば消してください。
私はこれを消すのを忘れており、処理がすぐにタイムアウトしてするなどの問題が発生していました。
import uvicorn
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
運用・保守コマンド
サーバーに接続する(SSH接続)
ssh -i (SSH接続のKeyを保存しているパス) opc@(サーバーのIPアドレス)
#例) ssh -i '/Users/tanami/Oracle_Cloud_Key.key' opc@123.4.56.789
Botのディレクトリに移動
cd Discord_Bot
Botの起動
docker stop my-bot
docker rm my-bot
sudo systemctl daemon-reload
sudo systemctl start discordbot
sudo systemctl enable discordbot
実行状況の確認
sudo systemctl status discordbot
ログを見る
sudo journalctl -u discordbot -f
# Ctrl+Cで閉じれます
サーバー再起動
sudo reboot
現在のコミットを確認
git log -1 --oneline
git rev-parse HEAD
git show --oneline -s HEAD