喂!我是 Wei

Front-End Engineer

Be a Problem Solver.

⌘K

導覽

所有文章緣起互動小功能

文章分類

目錄
問題:Node.js 一遇到未捕獲的例外就直接掛掉解法:restartBot() + MAX_RESTART_ATTEMPTSdev/prod 雙環境以 NODE_ENV 分離Docker 部署Dockerfile.dockerignore整體啟動到穩定運行的完整路徑

相關文章

Discord Bot 排程任務:node-cron 做每日公告與資料重置

2026年3月31日

Discord Bot 監控與告警:Bot 掛掉時自動發通知到頻道

2026年5月18日

Discord Bot 串接 Groq:打造高速 AI 對話助理

2026年4月1日

最新文章
全部 →
前端 CI/CD 與正式環境除錯:從 Pull Request 到事故排查
2026-06-24
即時資料怎麼選?Polling、SSE、WebSocket 比較
2026-06-23
前端系統設計:如何拆元件、資料流與大型專案架構?
2026-06-22
無障礙不是加 ARIA:語意化 HTML、鍵盤操作與焦點管理
2026-06-21
CSS 與 RWD 面試整理:Flexbox、Grid、定位與層疊脈絡
2026-06-19
← 返回文章列表

從本機到伺服器:Discord Bot 的 Docker 部署完整流程

2026年3月28日·約 5 分鐘閱讀·
Discord.jsBotDockerNode.js

上一篇 把 Google Sheets 資料層建立完了。這篇處理一個更實際的問題:Bot 運行在 24 小時不關機的伺服器上,崩潰怎麼辦?

還有就是本機開發和正式環境完全不一樣這件事,要怎麼用 Docker 把這個問題解決。


問題:Node.js 一遇到未捕獲的例外就直接掛掉

Node.js 有兩種「沒人處理的錯誤」情境:

  • unhandledRejection — Promise 被 reject 了,但沒有 .catch() 或 try/catch 處理
  • uncaughtException — throw 出去的 Error 在呼叫鏈上都沒被攔截

預設行為是直接 crash。對一般 API server 來說,PM2 或 Docker 的 --restart unless-stopped 可以應付。但 discord.js 的 Bot 有狀態(已載入的快取、設定值等),單純讓 process 重啟還不夠,需要重新初始化 Discord Client 並重新登入。


解法:restartBot() + MAX_RESTART_ATTEMPTS

index.js
let restartAttempts = 0;
const MAX_RESTART_ATTEMPTS = 5;
 
function restartBot() {
  if (restartAttempts >= MAX_RESTART_ATTEMPTS) {
    console.error("已達到最大重啟次數,退出程式...");
    process.exit(1); // 讓 Docker restart policy 接手
  }
 
  restartAttempts++;
  console.log(`嘗試重新啟動 Bot...(第 ${restartAttempts} 次)`);
 
  client.destroy().then(() => {
    setTimeout(() => {
      initBot();
      client.login(token).catch((error) => {
        console.error("重新連線失敗:", error);
        process.exit(1);
      });
    }, 5000); // 等 5 秒再重試,避免 Discord rate limit
  });
}

設計邏輯:

  1. 每次呼叫 restartBot() 計數 +1
  2. 達到 5 次上限後,改讓 process.exit(1) 結束進程,交給 Docker --restart unless-stopped 的機制做外部重啟
  3. client.destroy() 先把現有連線關乾淨,等 5 秒後重新執行 initBot() 和 client.login()

攔截點在 initBot() 最開頭:

index.js — initBot()
function initBot() {
  process.on("unhandledRejection", (reason, promise) => {
    console.error("Unhandled Rejection at:", promise, "reason:", reason);
    restartBot();
  });
 
  process.on("uncaughtException", (err) => {
    console.error("Uncaught Exception thrown:", err);
    restartBot();
  });
 
  client = new Client({ ... });
  // ...
}

process.on 放在 initBot() 裡意味著每次重新初始化時都會重新掛上監聽器。這在某些情況會造成 listener 重複疊加,生產環境要注意。更穩健的做法是在 process.on 之前先 process.removeAllListeners()。


dev/prod 雙環境以 NODE_ENV 分離

本機開發和正式環境通常有幾個明顯差異:

本機開發正式環境
Discord Bot TokenDEV_TOKENPROD_TOKEN
啟動時讀取外部設定❌✅
啟動排程任務❌✅

這些差異全部靠一個布林值控制:

index.js
const isProduction = process.env.NODE_ENV === "production";
const token = isProduction ? process.env.PROD_TOKEN : process.env.DEV_TOKEN;

在 client.once("ready") 裡:

index.js — ready 事件(節錄)
if (isProduction) {
  await loadExternalConfig();     // 從外部來源讀取設定
  await registerSlashCommands();
  scheduleRecurringTasks(client); // 啟動排程任務
} else {
  // 開發模式只註冊基本指令,不做其他事
  await registerSlashCommands();
}

開發時只需要 node index.js,不用設 NODE_ENV,預設就走開發路徑,不會觸發外部 API call、不會啟動排程,速度快也不會影響正式資料。


Docker 部署

Dockerfile

Dockerfile
FROM node:20
 
WORKDIR /app
 
COPY package.json package-lock.json ./
RUN npm ci
 
ENV TZ=Asia/Taipei
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
 
COPY . .
 
CMD ["node", "index.js"]

時區是一個容易踩的坑。如果 Bot 有排程任務(例如每天早上 8 點發公告),或需要判斷「今天」的日期,container 預設是 UTC 時區,所有時間計算都會偏移 8 小時。

ENV TZ=Asia/Taipei
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

這兩行確保 container 裡的 new Date() 以台灣時間為基準。

.dockerignore

node_modules
key/
.env
.env.*
docker-compose.yml
README.md

key/ 是放 GCP Service Account JSON 的目錄,絕對不能進 image。它被 .dockerignore 排除,改用 -v ./key:/app/key volume mount 掛進 container:

package.json scripts
"build":    "docker build -t discord-bot .",
"deploy":   "npm run build && docker run -d \
              --name discordbot \
              --restart unless-stopped \
              --env-file .env \
              -e NODE_ENV=production \
              -v ./key:/app/key \
              discord-bot",
"redeploy": "docker rm -f discordbot & npm run deploy"

部署三步驟:

  1. npm run build — build image
  2. npm run deploy — run container,掛 .env 和 key/ volume
  3. 要更新時:npm run redeploy — 強制移除舊 container 再重新 build + run

整體啟動到穩定運行的完整路徑

docker run
  └── node index.js
        └── initBot()
              ├── process.on("unhandledRejection") → restartBot()
              ├── process.on("uncaughtException")  → restartBot()
              ├── new Client({ intents: [...] })
              └── client.login(token)
                    └── client.once("ready")
                          ├── [production] 讀取外部設定
                          ├── guild.commands.set(COMMANDS)
                          └── [production] 啟動排程任務

崩潰時:

unhandledRejection / uncaughtException
  └── restartBot()
        ├── restartAttempts < MAX(5) → client.destroy() → setTimeout 5s → initBot()
        └── restartAttempts >= MAX(5) → process.exit(1) → Docker restart policy

這七篇把 Discord Bot 的核心都講完了:從申請設定、Slash Commands、按鈕、下拉選單、Embed 美化、Google Sheets 資料層,到最後的 Docker 部署與自動重啟機制。

分享:XLinkedIn
← 上一篇Google Sheets 當後端?Discord Bot 的輕量資料層
下一篇 →Discord Bot Modal:用彈出表單做完整回饋流程