Docker 映像檔建立與 Dockerfile 撰寫

A comprehensive guide to building Docker images and writing Dockerfiles with best practices

Docker 映像檔(Image)是容器運行的基礎,而 Dockerfile 則是用來定義映像檔建立流程的腳本。本文將深入介紹 Dockerfile 的各項指令、映像檔建立方式、最佳實務以及多階段建置技術。

Dockerfile 指令詳解

Dockerfile 是一個純文字檔案,包含一系列指令來告訴 Docker 如何建立映像檔。以下是最常用的指令說明:

FROM - 指定基礎映像檔

FROM 指令用於指定建立映像檔時所使用的基礎映像檔,這是每個 Dockerfile 必須具備的第一個指令。

1
2
3
4
5
6
7
8
# 使用官方 Python 3.11 映像檔作為基礎
FROM python:3.11-slim

# 使用特定版本的 Node.js
FROM node:18-alpine

# 從空白映像檔開始(適用於靜態二進位檔案)
FROM scratch

建議使用特定版本標籤而非 latest,以確保建置的可重現性。

RUN - 執行指令

RUN 指令用於在映像檔建立過程中執行命令,常用於安裝套件、設定環境等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 安裝系統套件
RUN apt-get update && apt-get install -y \
    curl \
    git \
    vim \
    && rm -rf /var/lib/apt/lists/*

# 安裝 Python 套件
RUN pip install --no-cache-dir flask gunicorn

# 建立目錄
RUN mkdir -p /app/data

最佳實務:將多個相關的 RUN 指令合併為一個,以減少映像檔層數。

COPY 與 ADD - 複製檔案

COPYADD 都用於將檔案複製到映像檔中,但有些微差異:

1
2
3
4
5
6
7
8
9
# COPY:單純複製檔案或目錄
COPY requirements.txt /app/
COPY src/ /app/src/

# 使用 --chown 設定擁有者
COPY --chown=appuser:appgroup app.py /app/

# ADD:支援解壓縮和遠端 URL(較少使用)
ADD archive.tar.gz /app/

建議:優先使用 COPY,因為它的行為更明確可預測。僅在需要自動解壓縮時使用 ADD

WORKDIR - 設定工作目錄

WORKDIR 指令用於設定後續指令的工作目錄。

1
2
3
4
5
WORKDIR /app

# 相對路徑會基於 WORKDIR
COPY . .
RUN npm install

ENV - 設定環境變數

ENV 指令用於設定環境變數,這些變數在建置期間和容器運行時都有效。

1
2
3
ENV NODE_ENV=production
ENV APP_HOME=/app
ENV PATH="${APP_HOME}/bin:${PATH}"

ARG - 建置時參數

ARG 指令定義建置時可傳入的參數,只在建置階段有效。

1
2
3
4
5
ARG VERSION=1.0.0
ARG BASE_IMAGE=python:3.11-slim

FROM ${BASE_IMAGE}
LABEL version="${VERSION}"

使用方式:

1
docker build --build-arg VERSION=2.0.0 -t myapp .

EXPOSE - 宣告埠號

EXPOSE 指令用於宣告容器將監聽的埠號,這是一個文件性質的宣告。

1
2
3
4
EXPOSE 80
EXPOSE 443
EXPOSE 8080/tcp
EXPOSE 8081/udp

注意EXPOSE 不會自動發布埠號,執行時仍需使用 -p 參數。

CMD 與 ENTRYPOINT - 啟動指令

這兩個指令定義容器啟動時要執行的命令,但用途略有不同:

CMD - 預設命令

1
2
3
4
5
6
7
8
# exec 格式(建議)
CMD ["python", "app.py"]

# shell 格式
CMD python app.py

# 作為 ENTRYPOINT 的預設參數
CMD ["--help"]

ENTRYPOINT - 入口點

1
2
3
4
5
6
# exec 格式(建議)
ENTRYPOINT ["python", "app.py"]

# 搭配 CMD 使用
ENTRYPOINT ["python"]
CMD ["app.py"]

差異說明

  • CMD 的內容可以在 docker run 時被覆蓋
  • ENTRYPOINT 定義的命令不會被覆蓋(除非使用 --entrypoint
  • 兩者搭配使用時,CMD 提供預設參數
1
2
3
4
5
6
# 範例:彈性的 Python 應用程式
ENTRYPOINT ["python"]
CMD ["app.py"]

# docker run myapp          -> 執行 python app.py
# docker run myapp test.py  -> 執行 python test.py

USER - 設定執行使用者

1
2
3
4
5
6
7
# 建立非 root 使用者
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

# 切換使用者
USER appuser

# 後續指令將以 appuser 身份執行

VOLUME - 宣告掛載點

1
2
VOLUME /data
VOLUME ["/var/log", "/var/data"]

LABEL - 添加元資料

1
2
3
LABEL maintainer="team@example.com"
LABEL version="1.0.0"
LABEL description="My application image"

建立映像檔

基本建置指令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 基本建置(在 Dockerfile 所在目錄)
docker build -t myapp:1.0.0 .

# 指定 Dockerfile 路徑
docker build -f path/to/Dockerfile -t myapp:1.0.0 .

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

# 傳入建置參數
docker build --build-arg VERSION=1.0.0 -t myapp:1.0.0 .

完整範例

建立一個 Python Flask 應用程式的 Dockerfile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 使用官方 Python 映像檔
FROM python:3.11-slim

# 設定環境變數
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# 設定工作目錄
WORKDIR /app

# 安裝系統依賴
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# 複製並安裝 Python 依賴
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 建立非 root 使用者
RUN useradd -r -s /bin/false appuser

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

# 切換到非 root 使用者
USER appuser

# 宣告埠號
EXPOSE 5000

# 啟動應用程式
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

最佳實務

1. 使用適當的基礎映像檔

1
2
3
4
5
6
# 使用 alpine 或 slim 版本減少映像檔大小
FROM python:3.11-alpine
FROM node:18-slim

# 使用特定版本而非 latest
FROM nginx:1.24-alpine

2. 善用建置快取

將不常變動的指令放在前面:

1
2
3
4
5
6
7
8
# 好的做法:依賴檔案先複製
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# 不好的做法:每次程式碼變動都要重新安裝依賴
COPY . .
RUN pip install -r requirements.txt

3. 減少映像檔層數

1
2
3
4
5
6
7
8
9
# 好的做法:合併指令
RUN apt-get update && \
    apt-get install -y package1 package2 && \
    rm -rf /var/lib/apt/lists/*

# 不好的做法:分開執行
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2

4. 使用 .dockerignore

建立 .dockerignore 檔案排除不需要的檔案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.git
.gitignore
README.md
Dockerfile
.dockerignore
node_modules
__pycache__
*.pyc
.env
.venv

5. 不要以 root 身份執行

1
2
RUN useradd -r -s /bin/false appuser
USER appuser

6. 使用健康檢查

1
2
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:5000/health || exit 1

多階段建置(Multi-stage Build)

多階段建置可以大幅減少最終映像檔的大小,特別適用於需要編譯的應用程式。

基本概念

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 建置階段
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 運行階段
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Go 應用程式範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 建置階段
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 -o main .

# 運行階段:使用最小映像檔
FROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]

Java 應用程式範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 建置階段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

# 運行階段
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

多階段建置的優點

  1. 減少映像檔大小:最終映像檔只包含運行所需的檔案
  2. 提高安全性:建置工具和原始碼不會出現在最終映像檔中
  3. 簡化建置流程:在單一 Dockerfile 中完成所有建置步驟
  4. 分離關注點:建置環境和運行環境可以獨立設定

總結

撰寫好的 Dockerfile 需要考慮以下重點:

  • 選擇適當的基礎映像檔
  • 善用建置快取,將不常變動的指令放在前面
  • 合併相關的 RUN 指令以減少層數
  • 使用 .dockerignore 排除不必要的檔案
  • 以非 root 使用者執行應用程式
  • 善用多階段建置減少最終映像檔大小
  • 添加健康檢查確保容器正常運行

透過遵循這些最佳實務,你可以建立出安全、精簡且易於維護的 Docker 映像檔。

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy