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 - 複製檔案
COPY 和 ADD 都用於將檔案複製到映像檔中,但有些微差異:
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"]
|
多階段建置的優點
- 減少映像檔大小:最終映像檔只包含運行所需的檔案
- 提高安全性:建置工具和原始碼不會出現在最終映像檔中
- 簡化建置流程:在單一 Dockerfile 中完成所有建置步驟
- 分離關注點:建置環境和運行環境可以獨立設定
總結
撰寫好的 Dockerfile 需要考慮以下重點:
- 選擇適當的基礎映像檔
- 善用建置快取,將不常變動的指令放在前面
- 合併相關的 RUN 指令以減少層數
- 使用 .dockerignore 排除不必要的檔案
- 以非 root 使用者執行應用程式
- 善用多階段建置減少最終映像檔大小
- 添加健康檢查確保容器正常運行
透過遵循這些最佳實務,你可以建立出安全、精簡且易於維護的 Docker 映像檔。