Dockerfile 編寫指南

Dockerfile 指令詳解、最佳實踐和實際範例


目錄


Dockerfile 基本概念

Dockerfile 是什麼?

Dockerfile = 建立 Docker Image 的腳本
           = 定義如何組裝 Image 的指令集
           = 可以版本控制的基礎設施代碼

核心特點

  • 純文字檔案,易於版本控制
  • 自動化建立 Image
  • 可重複、可追蹤
  • 標準化環境配置

Dockerfile 基本結構

# 1. 基礎 Image
FROM node:18-alpine

# 2. 維護者資訊(選擇性)
LABEL maintainer="dev@example.com"

# 3. 環境變數
ENV NODE_ENV=production

# 4. 工作目錄
WORKDIR /app

# 5. 複製檔案
COPY package*.json ./
RUN npm install
COPY . .

# 6. 暴露 Port
EXPOSE 3000

# 7. 啟動指令
CMD ["npm", "start"]

建立 Image

# 基本建立
docker build -t myapp .

# 指定 Dockerfile
docker build -f Dockerfile.prod -t myapp:prod .

# 建立時帶參數
docker build --build-arg VERSION=1.0 -t myapp .

# 不使用快取
docker build --no-cache -t myapp .

Dockerfile 指令詳解

FROM - 基礎 Image

# 基本使用
FROM ubuntu:20.04
FROM node:18-alpine
FROM python:3.11-slim

# 多階段建立
FROM node:18 AS builder
FROM nginx:alpine AS runtime

# 從 scratch 開始(完全空白)
FROM scratch

說明

  • 每個 Dockerfile 必須以 FROM 開頭
  • 定義基礎 Image
  • 可以有多個 FROM(多階段建立)
  • scratch 是特殊的空 Image

選擇建議

優先順序:
1. alpine 版本(最小,~5-10MB)
2. slim 版本(較小,~50-100MB)
3. 標準版本(完整,~200-900MB)

範例:
node:18-alpine  → 推薦
node:18-slim    → 需要更多工具時
node:18         → 避免使用(除非必要)

LABEL - 標籤元資料

# 單個標籤
LABEL version="1.0"
LABEL description="My Application"

# 多個標籤
LABEL maintainer="dev@example.com" \
      version="1.0" \
      description="Production ready app"

說明

  • 為 Image 添加元資料
  • 可以用於文件化、版本控制
  • 查看:docker inspect myapp

WORKDIR - 工作目錄

# 設定工作目錄
WORKDIR /app

# 相當於
RUN mkdir -p /app && cd /app

# 可以多次使用
WORKDIR /app
WORKDIR src        # 現在在 /app/src
WORKDIR ../dist    # 現在在 /app/dist

說明

  • 設定之後指令的工作目錄
  • 如果目錄不存在會自動建立
  • 影響 RUN、CMD、ENTRYPOINT、COPY、ADD
  • 建議使用絕對路徑

COPY vs ADD

# COPY - 單純複製(推薦)
COPY package.json .
COPY src/ /app/src/
COPY --chown=1000:1000 app.js .

# ADD - 複製 + 額外功能
ADD archive.tar.gz /app/     # 自動解壓縮
ADD https://example.com/file.txt /app/  # 下載檔案

對比

指令 COPY ADD
複製檔案
複製目錄
自動解壓 ✅ (.tar.gz, .tar.xz)
遠端 URL
推薦使用 特殊情況

最佳實踐

# ✅ 推薦:使用 COPY
COPY requirements.txt .

# ❌ 避免:不必要的 ADD
ADD requirements.txt .

# ✅ 特殊情況:需要解壓縮
ADD archive.tar.gz /app/

RUN - 執行指令

# Shell 形式(/bin/sh -c)
RUN apt-get update
RUN npm install

# Exec 形式(推薦,不經過 shell)
RUN ["npm", "install"]
RUN ["/bin/bash", "-c", "echo hello"]

# 鏈結指令(減少層數)
RUN apt-get update && \
    apt-get install -y \
        curl \
        vim \
        git && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

說明

  • 在建立 Image 時執行
  • 每個 RUN 會建立新的層
  • 合併指令可減少層數
  • Exec 形式不會展開變數

減少層數技巧

# ❌ 不好:4 層
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get clean

# ✅ 好:1 層
RUN apt-get update && \
    apt-get install -y curl vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

CMD vs ENTRYPOINT

CMD - 預設指令

# Shell 形式
CMD npm start

# Exec 形式(推薦)
CMD ["npm", "start"]
CMD ["node", "server.js"]

# 作為 ENTRYPOINT 的參數
CMD ["--port", "3000"]

ENTRYPOINT - 入口點

# Exec 形式(推薦)
ENTRYPOINT ["docker-entrypoint.sh"]
ENTRYPOINT ["node"]

# Shell 形式
ENTRYPOINT node server.js

組合使用

# 範例 1:彈性參數
ENTRYPOINT ["node"]
CMD ["app.js"]

# 執行
docker run myapp           # → node app.js
docker run myapp server.js # → node server.js

# 範例 2:固定指令 + 彈性參數
ENTRYPOINT ["mysql"]
CMD ["--default-authentication-plugin=mysql_native_password"]

# 執行
docker run mydb                    # → mysql --default-authentication-plugin=...
docker run mydb --port 3307        # → mysql --port 3307

使用場景

情況 使用 範例
提供預設指令 CMD CMD ["npm", "start"]
定義容器主要功能 ENTRYPOINT ENTRYPOINT ["nginx"]
指令 + 彈性參數 兩者組合 ENTRYPOINT ["node"] + CMD ["app.js"]

ENV - 環境變數

# 單個變數
ENV NODE_ENV=production
ENV PORT=3000

# 多個變數
ENV NODE_ENV=production \
    PORT=3000 \
    DB_HOST=localhost

# 在指令中使用
ENV APP_HOME=/app
WORKDIR $APP_HOME
COPY . $APP_HOME

說明

  • 設定環境變數
  • 在 Container 中可以使用
  • 可以在 docker run 時用 -e 覆蓋
  • 影響所有後續指令

使用時機

# ✅ 好:可能需要修改的配置
ENV NODE_ENV=production
ENV MAX_MEMORY=512M

# ❌ 不好:敏感資訊(使用 secrets)
ENV DB_PASSWORD=secret123  # 不要這樣做!

ARG - 建立參數

# 定義參數
ARG VERSION=1.0
ARG NODE_VERSION=18

# 使用參數
FROM node:${NODE_VERSION}-alpine
RUN echo "Building version ${VERSION}"

# 有預設值
ARG ENVIRONMENT=development

建立時傳入

docker build --build-arg VERSION=2.0 --build-arg NODE_VERSION=20 -t myapp .

ARG vs ENV

特性 ARG ENV
生命週期 建立時 建立時 + 執行時
可見性 Dockerfile Container
用途 建立參數 環境配置

EXPOSE - 聲明 Port

# 聲明 Port
EXPOSE 3000
EXPOSE 8080/tcp
EXPOSE 53/udp

# 多個 Port
EXPOSE 80 443

說明

  • 文件化作用(告知使用者)
  • 不會實際開啟 Port
  • 需要 -p-P 才能映射
# 仍需要 -p 映射
docker run -p 8080:3000 myapp

VOLUME - 掛載點

# 單個 Volume
VOLUME /data

# 多個 Volume
VOLUME ["/var/log", "/var/db"]

# 使用變數
ENV DATA_DIR=/app/data
VOLUME $DATA_DIR

說明

  • 定義掛載點
  • 資料持久化
  • 可以在 docker run 時用 -v 指定

USER - 切換使用者

# 創建使用者
RUN groupadd -r appuser && useradd -r -g appuser appuser

# 切換使用者
USER appuser

# 之後的指令都以 appuser 執行
WORKDIR /app
CMD ["npm", "start"]

安全最佳實踐

# ❌ 不好:使用 root
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["npm", "start"]  # 以 root 執行

# ✅ 好:使用非 root 使用者
FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["npm", "start"]  # 以 appuser 執行

HEALTHCHECK - 健康檢查

# 基本健康檢查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost/ || exit 1

# HTTP 檢查
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:3000/health || exit 1

# 自訂腳本
HEALTHCHECK CMD /app/healthcheck.sh

# 禁用健康檢查
HEALTHCHECK NONE

參數說明

  • --interval=30s:每 30 秒檢查一次
  • --timeout=3s:超過 3 秒視為失敗
  • --retries=3:連續失敗 3 次視為 unhealthy
  • --start-period=60s:啟動後 60 秒內不算失敗

Dockerfile 最佳實踐

1. 多階段建立(Multi-stage Build)

# 第一階段:建立
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 第二階段:執行
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

EXPOSE 3000
CMD ["node", "dist/main.js"]

優點

✅ 最終 Image 更小(只包含執行時需要的檔案)
✅ 不包含建立工具和中間檔案
✅ 更安全(減少攻擊面)
✅ 建立環境和執行環境分離

實際效果對比

單階段:1.2GB
多階段:150MB
減少:87.5%

2. 利用快取機制

# ❌ 不好:每次程式碼改變都重裝依賴
COPY . .
RUN npm install

# ✅ 好:利用快取
COPY package*.json ./
RUN npm install          # ← 只有 package.json 改變才重新執行
COPY . .                 # ← 程式碼改變不影響上面的快取

快取優化策略

# 1. 先複製依賴檔案
COPY package.json package-lock.json ./

# 2. 安裝依賴(快取)
RUN npm ci

# 3. 複製其他檔案
COPY src/ ./src/
COPY public/ ./public/
COPY tsconfig.json ./

# 4. 建立
RUN npm run build

3. 減少層數

# ❌ 不好:太多層(4 層)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get clean

# ✅ 好:合併指令(1 層)
RUN apt-get update && \
    apt-get install -y \
        curl \
        vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

層數優化原則

  • 合併相關的 RUN 指令
  • 每個 RUN 結束時清理暫存檔案
  • 保持可讀性和可維護性的平衡

4. 使用 .dockerignore

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
*.md
*.log
dist
build
coverage
.vscode
.idea

# 保留特定檔案
!README.production.md

作用

✅ 減少建立 context 大小
✅ 加快建立速度
✅ 避免複製不需要的檔案
✅ 避免將敏感資訊複製到 Image

5. 選擇適當的基礎 Image

# 大小對比
FROM node:18           # ~900MB
FROM node:18-slim      # ~200MB
FROM node:18-alpine    # ~100MB

# 建議選擇
FROM node:18-alpine    # ✅ 優先使用

# 需要特定工具時
FROM node:18-slim      # ✅ 次選

# 避免
FROM node:18           # ❌ 除非必要

Alpine 注意事項

# Alpine 使用 musl libc,可能與 glibc 不相容
# 需要編譯原生模組時可能遇到問題

# 解決方案 1:安裝建立工具
RUN apk add --no-cache python3 make g++

# 解決方案 2:使用 slim 版本
FROM node:18-slim

6. 最小化層的大小

# ❌ 錯誤:分開刪除不會減少大小
RUN wget https://example.com/bigfile.tar.gz  # +500MB
RUN tar xzf bigfile.tar.gz                   # +50MB
RUN rm bigfile.tar.gz                        # +0MB (bigfile 仍在上一層)
# 總大小:550MB

# ✅ 正確:在同一層刪除
RUN wget https://example.com/bigfile.tar.gz && \
    tar xzf bigfile.tar.gz && \
    rm bigfile.tar.gz
# 總大小:50MB

7. 使用非 root 使用者

# ✅ 最佳實踐
FROM node:18-alpine

# 創建使用者
RUN addgroup -S appgroup && \
    adduser -S appuser -G appgroup

# 設定權限
WORKDIR /app
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci && npm cache clean --force
COPY --chown=appuser:appgroup . .

# 切換使用者
USER appuser

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

8. 標籤和版本管理

# ❌ 不好:使用 latest
docker build -t myapp:latest .

# ✅ 好:使用語義化版本
docker build -t myapp:1.2.3 .
docker build -t myapp:1.2 .
docker build -t myapp:1 .

# ✅ 使用 Git 提交
docker build -t myapp:$(git rev-parse --short HEAD) .

# ✅ 使用時間戳
docker build -t myapp:$(date +%Y%m%d-%H%M%S) .

多階段建立

Node.js 應用範例

# ============================================
# Stage 1: 依賴安裝
# ============================================
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && \
    npm cache clean --force

# ============================================
# Stage 2: 建立
# ============================================
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# ============================================
# Stage 3: 執行
# ============================================
FROM node:18-alpine AS runtime

# 安全性:創建非 root 使用者
RUN addgroup -S appgroup && \
    adduser -S appuser -G appgroup

WORKDIR /app

# 只複製必要的檔案
COPY --from=dependencies --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --chown=appuser:appgroup package*.json ./

# 切換使用者
USER appuser

# 健康檢查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD node healthcheck.js || exit 1

EXPOSE 3000

CMD ["node", "dist/main.js"]

大小對比

所有階段:~1.5GB
最終 Image:~150MB
減少:90%

Go 應用範例

# ============================================
# Stage 1: 建立
# ============================================
FROM golang:1.21-alpine AS builder

WORKDIR /app

# 複製依賴檔案
COPY go.mod go.sum ./
RUN go mod download

# 複製程式碼
COPY . .

# 建立(靜態編譯)
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# ============================================
# Stage 2: 執行
# ============================================
FROM scratch

# 複製 CA 憑證(HTTPS 需要)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 複製執行檔
COPY --from=builder /app/main /main

EXPOSE 8080

CMD ["/main"]

大小對比

使用 golang 基礎:~800MB
使用 scratch:~10MB
減少:98.75%

Java 應用範例

# ============================================
# Stage 1: 建立
# ============================================
FROM gradle:8-jdk17 AS builder

WORKDIR /app

# 複製依賴檔案(利用快取)
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
RUN gradle dependencies --no-daemon

# 複製程式碼並建立
COPY src ./src
RUN gradle build --no-daemon

# ============================================
# Stage 2: 執行
# ============================================
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

# 創建非 root 使用者
RUN addgroup -S appgroup && \
    adduser -S appuser -G appgroup

# 複製 JAR 檔案
COPY --from=builder --chown=appuser:appgroup /app/build/libs/*.jar app.jar

USER appuser

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]


實戰範例

範例 1:Python Flask 應用

FROM python:3.11-slim

# 安全性:使用非 root 使用者
RUN useradd -m -u 1000 appuser

WORKDIR /app

# 複製依賴檔案
COPY requirements.txt .

# 安裝依賴
RUN pip install --no-cache-dir -r requirements.txt

# 複製應用程式
COPY --chown=appuser:appuser . .

# 切換使用者
USER appuser

EXPOSE 5000

# 健康檢查
HEALTHCHECK --interval=30s --timeout=3s \
  CMD python healthcheck.py || exit 1

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

範例 2:Nginx 靜態網站

# ============================================
# Stage 1: 建立
# ============================================
FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# ============================================
# Stage 2: 執行
# ============================================
FROM nginx:alpine

# 複製建立的靜態檔案
COPY --from=builder /app/dist /usr/share/nginx/html

# 複製 Nginx 配置
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

常見問題

問題 1:Build Context 太大

# 錯誤訊息
Sending build context to Docker daemon  2.5GB

# 解決方案:使用 .dockerignore
echo "node_modules" >> .dockerignore
echo ".git" >> .dockerignore
echo "*.log" >> .dockerignore

問題 2:快取沒有生效

# ❌ 問題:COPY . . 會讓快取失效
COPY . .
RUN npm install

# ✅ 解決:先複製 package.json
COPY package*.json ./
RUN npm install
COPY . .

問題 3:Image 太大

# 檢查 Image 大小
docker images myapp

# 查看各層大小
docker history myapp

# 優化方案
# 1. 使用 alpine 基礎
# 2. 多階段建立
# 3. 清理暫存檔案
# 4. 合併 RUN 指令

進階技巧

使用 BuildKit

# 啟用 BuildKit(更快、更好的快取)
export DOCKER_BUILDKIT=1
docker build -t myapp .

# 或在指令前加上
DOCKER_BUILDKIT=1 docker build -t myapp .

條件性 COPY

# 使用 ARG 控制
ARG ENVIRONMENT=production

COPY package*.json ./
RUN npm ci

# 只在開發環境複製測試檔案
COPY . .
RUN if [ "$ENVIRONMENT" = "development" ]; then \
      npm install --only=dev; \
    fi

平行建立

# 使用多個 FROM 平行建立
FROM base AS deps
RUN install-deps

FROM base AS test
RUN run-tests

FROM base AS build
COPY --from=deps /deps .
RUN build-app

總結

核心要點

  • Dockerfile 是建立 Docker Image 的腳本,可版本控制
  • 使用多階段建構可大幅減少最終 Image 大小
  • 利用快取機制加速建構:先複製依賴檔案
  • 合併 RUN 指令、在同一層清理暫存檔案減少層數
  • 使用 alpine 基礎 Image、非 root 使用者提高安全性

快速參考

指令 作用 最佳實踐
FROM 基礎 Image 優先 alpine
RUN 執行指令 合併指令
COPY 複製檔案 先複製依賴
CMD 預設指令 用 Exec 形式
EXPOSE 聲明 Port 文件化
USER 切換使用者 避免 root

建立日期:2025-11-10 最後更新:2025-11-18

🔗相關文章