← 返回部落格
·10 min 閱讀·DevOps

Docker 容器化部署踩坑紀錄:從開發到生產的實戰經驗

記錄我們在將多個 Node.js 和 Python 專案容器化部署到生產環境時踩過的坑,包含映像檔優化、多階段建置、健康檢查與日誌管理。

Docker部署容器化DevOps
GP Wang
GP Wang
GWP4 STUDIO 創辦人 · 軟體 / 資料工程師

前言

Docker 讓「在我機器上可以跑」變成了「在任何地方都可以跑」。但從開發環境的 docker-compose up 到生產環境的穩定運作,中間有很多需要注意的細節。

這篇文章記錄了我們在多個專案中容器化部署的經驗——特別是那些文件上不會告訴你,只有踩過才知道的坑。

映像檔優化:從 1.2GB 到 89MB

問題

我們第一次 build 一個 Next.js 專案的 Docker image,結果大小是 1.2GB。推送到 Container Registry 要好幾分鐘,部署的冷啟動也很慢。

原因分析

原始的 Dockerfile:

FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]

問題:

  1. 使用了完整的 node:20 映像(包含一堆不需要的工具)
  2. node_modules(開發依賴)和原始碼都包進去了
  3. 沒有利用多階段建置

優化後的 Dockerfile

# Stage 1: 安裝依賴
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Stage 2: 建置
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: 最終映像
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# 只複製需要的檔案
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]

配合 Next.js 的 output: 'standalone' 設定:

// next.config.ts
const nextConfig = {
  output: 'standalone',
};

結果:1.2GB → 89MB(減少 93%)。

關鍵優化技巧

1. 使用 Alpine 基底映像

node:20-alpinenode:20 小 10 倍以上。大多數 Node.js 應用都不需要完整的 Linux 發行版。

2. 多階段建置

  • Stage 1:只安裝生產依賴
  • Stage 2:安裝所有依賴(包含 devDependencies)並建置
  • Stage 3:只取需要的產物

3. 善用 .dockerignore

node_modules
.git
.next
*.md
.env*

避免不必要的檔案進入 build context,加快 build 速度。

4. 利用層快取

把不常變動的步驟放前面(如 COPY package.json),常變動的放後面(如 COPY . .)。這樣當只有程式碼變動時,npm install 的層可以直接使用快取。

Python 專案的容器化

Python 專案(如我們的爬蟲程式)有不同的考量:

FROM python:3.12-slim AS base
WORKDIR /app

# 安裝系統依賴(某些 Python 套件需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "-m", "app.main"]

踩坑:Playwright 的容器化

我們的爬蟲使用 Playwright 來處理需要 JavaScript 渲染的頁面。在 Docker 中跑 Playwright 需要安裝瀏覽器和它的系統依賴:

FROM python:3.12-slim

# Playwright 需要的系統依賴
RUN apt-get update && apt-get install -y --no-install-recommends \
    libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \
    libcups2 libdrm2 libxkbcommon0 libxcomposite1 \
    libxdamage1 libxfixes3 libxrandr2 libgbm1 \
    libpango-1.0-0 libcairo2 libasound2 \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN playwright install chromium

COPY . .
CMD ["python", "-m", "app.main"]

第一次嘗試時,我們忘了安裝系統依賴,container 啟動後 Playwright 直接 crash 但錯誤訊息很不明確。花了兩個小時才定位到是缺少 shared libraries。

教訓:如果 Python 套件依賴 C library,在 Docker 中一定要測試,不要假設 slim/alpine 映像有包含。

健康檢查

容器啟動不代表應用就緒。特別是需要連接資料庫或其他服務的應用,啟動到真正能服務請求之間有一段時間差。

Docker 的 HEALTHCHECK

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1

應用端的健康檢查 endpoint

// app/api/health/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  try {
    // 確認資料庫連線正常
    await prisma.$queryRaw`SELECT 1`;

    return NextResponse.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    return NextResponse.json(
      { status: 'unhealthy', error: 'Database connection failed' },
      { status: 503 }
    );
  }
}

為什麼健康檢查重要?

在使用 Cloud Run 或 Kubernetes 時,平台需要知道 container 是否真的準備好接收流量。沒有健康檢查的話:

  • 新版本部署時,流量可能被導到還沒連上資料庫的 container
  • 應用掛了(但 process 還活著),平台不會自動重啟

docker-compose 的生產使用注意事項

在開發環境中,docker-compose.yml 很好用。但要部署到生產時,有幾個需要調整的地方:

開發 vs 生產的設定分離

# docker-compose.yml (共用基礎)
services:
  app:
    build: .
    environment:
      - NODE_ENV=production

# docker-compose.override.yml (開發專用,自動載入)
services:
  app:
    build:
      target: deps  # 開發時用包含 devDependencies 的 stage
    volumes:
      - .:/app     # 掛載原始碼,支援 hot reload
      - /app/node_modules  # 避免覆蓋 container 中的 node_modules
    environment:
      - NODE_ENV=development
    ports:
      - "3000:3000"

# docker-compose.prod.yml (生產專用)
services:
  app:
    restart: always
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'

資料持久化

生產環境中,資料庫的資料不能存在 container 裡(container 重建就沒了):

services:
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

volumes:
  pgdata:
    driver: local

secrets:
  db_password:
    file: ./secrets/db_password.txt

踩坑:Volume 權限問題

在 macOS 上開發時一切正常,部署到 Linux 伺服器後,container 內的應用無法寫入 volume。

原因:macOS 的 Docker Desktop 會自動處理檔案權限映射,但 Linux 上不會。Container 內的 process 通常以非 root 用戶執行,而 volume 的 owner 可能是 root。

解法:在 Dockerfile 中設定正確的使用者和權限:

RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
RUN chown -R appuser:appgroup /app
USER appuser

日誌管理

問題:日誌寫到哪裡?

Container 最佳實踐是把日誌寫到 stdout/stderr,讓 Docker 的 logging driver 來處理。但很多框架預設會寫檔案。

# 不好:寫入檔案(container 重建就不見了)
logging.basicConfig(filename='app.log')

# 好:寫入 stdout
logging.basicConfig(
    stream=sys.stdout,
    format='%(asctime)s %(levelname)s %(message)s'
)

結構化日誌

在生產環境中,結構化(JSON 格式)的日誌比純文字更容易搜尋和分析:

import json
import logging

class JsonFormatter(logging.Formatter):
    def format(self, record):
        return json.dumps({
            'timestamp': self.formatTime(record),
            'level': record.levelname,
            'message': record.getMessage(),
            'module': record.module,
        })

安全性考量

不要在映像中包含機密

# 錯誤:把 .env 或 API key 寫進映像
COPY .env .
ENV API_KEY=sk-xxxxx

# 正確:在執行時注入
# docker run -e API_KEY=$API_KEY my-app

即使你後來 RUN rm .env,機密仍然存在於先前的層中,可以被 docker history 看到。

使用非 root 用戶

# 建立非 root 用戶
RUN addgroup --system app && adduser --system --ingroup app app
USER app

如果 container 被入侵,非 root 用戶可以限制損害範圍。

定期更新基底映像

基底映像可能包含安全漏洞。定期 rebuild 確保使用最新的安全修補:

# 檢查映像的漏洞
docker scout cves my-app:latest

常見問題排查清單

當 container 行為異常時,依序檢查:

  1. Container 有啟動嗎? docker ps -a 看狀態
  2. 啟動後立刻退出? docker logs <container> 看錯誤訊息
  3. 應用有在監聽正確的 port 嗎? 確認 EXPOSE-p 映射
  4. 環境變數有正確注入嗎? docker exec <container> env
  5. DNS/網路有通嗎? docker exec <container> ping <service>
  6. 記憶體夠嗎? docker stats 看即時資源使用

結語

容器化部署不是「寫一個 Dockerfile 就結束」的事情。從映像檔大小、安全性、日誌管理到健康檢查,每一個環節都值得花時間處理好。

投資在這些基礎設施上的時間,會在之後每一次部署、每一次 debugging 中回報給你。一個設計良好的容器化架構,能讓你在凌晨三點收到告警時,快速定位問題而不是對著黑盒子發呆。

關於作者

GP Wang
GP Wang

GWP4 STUDIO 創辦人,超過 8 年軟體開發經驗,專精資料工程、全端開發與系統架構。 持續在部落格分享專案實作經驗與技術心得。

了解更多 →