昨天,我們已經能夠用 docker-compose.prod.yml
模擬生產環境。
今天要更進一步 —— 學會讓映像更小、更快、更穩定,
並加入 Healthcheck(健康檢查) 來確保服務運作正常。
隨著專案越來越完整,我開始在意三件事:
今天的目標就是讓 Docker 容器更像「一台上線中的伺服器」——
精簡、穩定、可被監控。
假設我目前的映像大小如下:
REPOSITORY TAG SIZE
ts-todo-api prod 350MB
如果我每次都要 push 到 Docker Hub 或部署到雲端,
這個大小會讓整個流程變得又慢又浪費頻寬。
Docker Image 的最佳化策略大概分為三種:
類別 | 說明 |
---|---|
減少層數 | 合併多個 RUN 指令 |
使用輕量基底 | 例如 node:alpine 取代 node:latest |
多階段建置 | 將 build 階段與執行階段拆開(昨天已做) |
docker history ts-todo-api:prod
可以看到每一層的指令、大小,例如:
IMAGE CREATED CREATED BY SIZE
8e912f9c2f7a 2 hours ago CMD ["node" "dist/index.js"] 0B
<missing> 2 hours ago RUN npm ci --only=production 105MB
<missing> 2 hours ago COPY --from=builder /app/dist ./dist 3MB
<missing> 2 hours ago FROM node:18-alpine 46MB
🔍 發現 npm ci
層通常是最肥的。
如果你的專案只用到部分套件,可以考慮切分 monorepo 或用 PNPM 壓縮依賴。
在 Dockerfile 中,有時我們這樣寫:
RUN apk add --no-cache bash
RUN npm ci
這會建立兩層。
可以改成這樣合併:
RUN apk add --no-cache bash && npm ci
減少層數 = 減少 image 大小。
node:18-slim
或 alpine
映像 | 大小 | 備註 |
---|---|---|
node:18 |
約 900MB | 含完整工具 |
node:18-slim |
約 300MB | 較輕、仍支援常見套件 |
node:18-alpine |
約 180MB | 最輕,但部分 native 模組需額外安裝編譯工具 |
如果你的專案沒有使用原生 C 模組(例如 bcrypt、sharp),node:18-alpine
幾乎是完美選擇。
# ========= Build stage =========
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
# ========= Run stage =========
FROM node:18-alpine AS runner
WORKDIR /app
# 安全性:刪除不必要權限
RUN addgroup -S app && adduser -S app -G app
USER app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
USER app
:避免使用 root 權限執行容器HEALTHCHECK
:檢查 /health
是否回應--omit=dev
只安裝生產依賴在你的 Express 主程式中(例如 src/index.ts
)加入:
import express from "express";
const app = express();
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" });
});
app.listen(3000, () => console.log("🚀 Server running on port 3000"));
Docker 就會透過 /health
檢查容器是否存活。
啟動容器後:
docker ps
會看到新的欄位:
CONTAINER ID IMAGE STATUS PORTS
8e1b7d... ts-todo-api:prod Up 3 minutes (healthy) 0.0.0.0:3000->3000/tcp
如果 /health
回傳非 200、或無法連線,
Docker 會標記為 (unhealthy)
。
試著把 /health
改成回傳 500:
app.get("/health", (req, res) => res.status(500).send("fail"));
重新 build + run,
幾分鐘後你會看到:
STATUS: Up 2 minutes (unhealthy)
這代表健康檢查機制成功偵測異常 👏
開發過程中會產生很多 intermediate layers,
可以用以下方式清理:
docker system prune -a
如果要只清除暫存 build cache:
docker builder prune
今天的主題讓我開始理解 Docker「真正的生產觀念」。
不是只有「能跑就好」,而是要穩、快、輕、可監控。
✨ 今日關鍵收穫:
現在的容器就像一台「乾淨又聰明的伺服器」🧠
出錯會自己標記、重新啟動也不怕。