Discord Bot を Docker で常駐運用する

はじめに

前回の記事「Docker環境の構築」で Docker 環境までは構築できたので、今回は 実際のアプリをコンテナ化します。題材は「Discord 上で動く ChatGPT Bot(Node.js)」です(厳密には “Discord サーバ” ではなく “Discord Bot” のコンテナ化です)。
この記事のゴールは次の 3 つです。

なぜコンテナ化するのか

今までの「サーバに入ってターミナルで npm start」運用は、次のような “事故りポイント” がありました。

Docker 化すると、起動と管理が 「コンテナ」 という単位に揃うので、運用がかなり楽になります。

Bot の事前確認事項

使用する Bot のリポジトリは こちら(GitHub) です。
この Bot は以下を前提にしています(Docker 化に効いてくる部分だけ抜粋)。

アーキテクチャ図

今回、構築予定の全体像です。黄色がホスト(Ubuntu Server + Docker Engine)、緑が Bot コンテナです。
.envsettings.json はホスト側で管理して、起動時に渡す/マウントします。

discord-chatgpt-bot architecture

Dockerfile(コンテナの設計図)

今回の Dockerfile は「Node.js の公式イメージをベースに、依存関係を入れて起動する」というシンプルな形です。ポイントは キャッシュが効く順序と、root ではなく node ユーザーで動かすことです。

FROM node:18-slim

WORKDIR /app

# 依存関係だけ先にコピー(ここがキャッシュに効く)
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# アプリ本体をコピー
COPY . .

# 非 root 実行のために権限を調整
RUN chown -R node:node /app
USER node

CMD ["npm", "start"]

Tips: なぜ COPY package*.json を先にやるのか

アプリのソースコードが変わっても、依存関係が変わらない限り npm ci のレイヤーをキャッシュできるため、ビルドが速くなります。
(逆に最初に COPY . . してしまうと、ちょっとした変更でも毎回依存インストールが走りがちです)。

Tips: なぜ非 root(USER node)で動かすのか

コンテナの中とはいえ root で動かすと、万一の侵入時に影響が大きくなります。公式の node イメージには node ユーザーが最初から用意されているので、素直にそれを使うのが手軽です。

.dockerignore(ビルドに不要なものを除外する)

Docker はビルド時に “ビルドコンテキスト” としてディレクトリ一式を送るので、不要なものは最初から.dockerignoreで除外します。とくに .envnode_modules を入れないのが重要です。

node_modules
npm-debug.log
.env
.git
.gitignore
workspace

Tips: .env.dockerignore に入れる理由

環境変数の渡し方(そして Dockerfile に含めない理由)

この Bot に必要な環境変数DISCORD_TOKENOPENAI_API_KEY です。これらは Dockerfile に書かず、起動時に渡します。例:.env

DISCORD_TOKEN=xxxxxxxx
OPENAI_API_KEY=xxxxxxxx

起動(--env-file を使う)

docker run -d --name discord-chatgpt-bot \
  --env-file .env \
  -v "$PWD/settings.json:/app/settings.json" \
  --restart unless-stopped \
  discord-chatgpt-bot:1.0

Tips: なぜ環境変数を Dockerfile に含めないのか

結論:秘密情報をイメージに焼き込まないためです。

設定ファイルの永続化(settings.json

この Bot はモデル選択や履歴 ON/OFF を settings.json に保存します。コンテナは作り直す前提の仕組みなので、ファイルをコンテナ内に置きっぱなしにすると消えます。
そこで、settings.json はホスト側に置いて、コンテナに バインドマウントします。

-v "$PWD/settings.json:/app/settings.json"

Tips: マウントは “必要最小限” にする

ホストのパスをマウントした分だけ、コンテナからホストのファイルに触れられます。今回は settings.json だけをマウントし、範囲を最小化します(安全・管理の両面でメリットがあります)。

ビルドと起動(手順まとめ)

今回は単一のBotをdocker runで起動します。複数のサービスをまとめて構成管理する場合は、Docker Composeを使う方法もあります。

1) イメージをビルド

docker build -t discord-chatgpt-bot:1.0 .

permission denied while trying to connect to the docker API が出る場合は、sudo docker ... で実行するか、ユーザーを docker グループに追加します。

2) コンテナ起動(バックグラウンド)

docker run -d --name discord-chatgpt-bot \
  --env-file .env \
  -v "$PWD/settings.json:/app/settings.json" \
  --restart unless-stopped \
  discord-chatgpt-bot:1.0

3) 起動確認とログ

docker ps
docker logs -f discord-chatgpt-bot

ログに ログイン成功 / 全コマンド登録完了 が出れば OK です。

運用:停止・更新・自動起動

停止・再起動

docker stop discord-chatgpt-bot
docker start discord-chatgpt-bot

更新(コードを直したら)

コンテナは「作り直す」運用が基本なので、更新は次の流れになります。

docker build -t discord-chatgpt-bot:1.1 .
docker stop discord-chatgpt-bot
docker rm discord-chatgpt-bot
docker run -d --name discord-chatgpt-bot \
  --env-file .env \
  -v "$PWD/settings.json:/app/settings.json" \
  --restart unless-stopped \
  discord-chatgpt-bot:1.1

自動起動(サーバ再起動対策)

今回の起動コマンドでは --restart unless-stopped を付けています。これにより、サーバ再起動や Docker デーモン再起動後も、明示的に止めていない限り自動で復旧します。

まとめ

ターミナルでの手動起動をやめて Docker 化すると、「起動・停止・ログ・自動復旧」の運用が揃って管理がかなり楽になります。
さらに .env(秘密情報)と settings.json(永続化が必要な設定)をイメージから分離できるので、保守性と安全性が一段上がります。


この記事はReo’s Tech Blogの同名記事にも掲載しています。

Reo’s Tech Blogでは、個人開発や日々の技術的な取り組みを記録しています。興味がありましたら、ほかの記事もご覧ください