第05章:Dockerfile 深度解析

第05章:Dockerfile 深度解析

本章目标:全面掌握 Dockerfile 的每条指令,理解构建缓存机制,编写企业级的高效 Dockerfile。


5.1 Dockerfile 是什么

Dockerfile 是一个文本文件,包含了一系列指令(Instruction),用于自动化构建 Docker 镜像。每条指令都会在镜像中创建一个新的层。

复制代码
Dockerfile → docker build → Docker Image
                 ↓
         逐行执行指令
         每条指令生成一层
         层层叠加形成最终镜像

5.2 Dockerfile 指令全解析

5.2.1 FROM ------ 指定基础镜像

dockerfile 复制代码
# FROM 指令:每个 Dockerfile 必须以 FROM 开头
FROM <image>[:<tag>] [AS <name>]

# 示例
FROM ubuntu:22.04
FROM python:3.11-slim
FROM node:20-alpine
FROM scratch  # 空白镜像,从零开始构建

# 多阶段构建中使用命名阶段
FROM golang:1.21 AS builder
FROM node:20-alpine AS frontend
FROM nginx:latest AS production

选择基础镜像的原则

基础镜像 大小 适用场景
ubuntu:22.04 ~77MB 需要完整 Ubuntu 工具链
debian:bookworm-slim ~52MB 比 ubuntu 更小的通用选择
alpine:3.19 ~7MB 极致轻量化,注意 musl libc 兼容性
distroless ~20MB Google 的无 shell 镜像,安全性最高
scratch 0MB 静态编译的 Go/Rust 二进制

5.2.2 RUN ------ 执行命令

dockerfile 复制代码
# RUN 两种语法形式

# Shell 形式(默认通过 /bin/sh -c 执行)
RUN apt-get update && apt-get install -y \
    curl \
    wget \
    vim \
    && rm -rf /var/lib/apt/lists/*

# Exec 形式(直接执行,不经过 shell)
RUN ["/usr/bin/python3", "-m", "pip", "install", "flask"]

# ⚠️ 最佳实践:合并多个 RUN 减少层数
# 反面教材(4层):
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN rm -rf /var/lib/apt/lists/*

# 正确做法(1层):
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        curl \
        wget \
    && rm -rf /var/lib/apt/lists/*

5.2.3 COPY ------ 复制文件

dockerfile 复制代码
# COPY 语法
COPY [--chown=<user>:<group>] <src>... <dest>

# 基本用法
COPY requirements.txt /app/
COPY . /app/

# 使用通配符
COPY *.py /app/
COPY html/ /app/html/

# 使用 --chown 设置所有者
COPY --chown=appuser:appuser . /app/

# 使用 --chmod 设置权限(Docker 18.09+)
COPY --chmod=755 entrypoint.sh /usr/local/bin/

5.2.4 ADD ------ 增强版 COPY

dockerfile 复制代码
# ADD 比 COPY 多了两个功能:
# 1. 自动解压 tar 文件
ADD app.tar.gz /app/

# 2. 支持 URL 下载(不推荐,建议用 RUN curl)
ADD https://example.com/file.tar.gz /tmp/

# 最佳实践:大多数情况下使用 COPY 更清晰
# 只在需要自动解压时使用 ADD

COPY vs ADD 对比

特性 COPY ADD
复制文件
自动解压 tar
URL 下载
语义清晰度 ✅ 明确 ⚠️ 有隐式行为
推荐度 ⭐⭐⭐ 推荐 特定场景使用

5.2.5 CMD ------ 容器启动命令

dockerfile 复制代码
# CMD 三种语法形式

# Exec 形式(推荐)
CMD ["python3", "app.py"]

# Shell 形式(进程在 sh -c 中运行,PID 不为1)
CMD python3 app.py

# 作为 ENTRYPOINT 的参数
CMD ["--port", "8080"]

CMD 的关键特性

  • 一个 Dockerfile 中只能有一个 CMD(多个只有最后一个生效)
  • docker run 传入的命令会覆盖 CMD
  • CMD 是容器的默认启动命令

5.2.6 ENTRYPOINT ------ 入口点

dockerfile 复制代码
# ENTRYPOINT 定义容器的主进程
# 与 CMD 的区别:ENTRYPOINT 不会被 docker run 的参数覆盖

# Exec 形式
ENTRYPOINT ["python3", "app.py"]

# Shell 形式
ENTRYPOINT python3 app.py

# 配合 CMD 提供默认参数
ENTRYPOINT ["python3"]
CMD ["app.py"]
# docker run myapp         → python3 app.py
# docker run myapp test.py → python3 test.py

CMD vs ENTRYPOINT 对比

特性 CMD ENTRYPOINT
覆盖方式 docker run 参数覆盖 需要 --entrypoint 才能覆盖
默认命令 可以被覆盖 不会被覆盖
用途 定义默认命令 定义容器的固定入口
多个定义 只有最后一个生效 只有最后一个生效

5.2.7 WORKDIR ------ 工作目录

dockerfile 复制代码
# WORKDIR 设置后续指令的工作目录
WORKDIR /app

# 如果目录不存在会自动创建
WORKDIR /app/src
# 等价于: RUN mkdir -p /app/src && cd /app/src

# 可以使用环境变量
ENV APP_HOME=/app
WORKDIR $APP_HOME

# ⚠️ 不要用 RUN cd /app(切换目录只在当前层有效)
# ✅ 正确做法:WORKDIR /app

5.2.8 ENV ------ 环境变量

dockerfile 复制代码
# 设置环境变量
ENV APP_HOME=/app
ENV APP_VERSION=1.0.0
ENV PYTHONUNBUFFERED=1

# 多行设置
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1

# 环境变量在后续指令中可用
ENV MY_NAME="John Doe"
RUN echo "Hello, $MY_NAME"

# 在容器运行时也可以使用
# docker run -e MY_NAME="Jane" myimage

5.2.9 ARG ------ 构建参数

dockerfile 复制代码
# ARG 在构建时可用,运行时不可用
ARG VERSION=1.0.0
ARG REGISTRY=registry.example.com

# 在 FROM 中使用 ARG
ARG BASE_IMAGE=python:3.11-slim
FROM ${BASE_IMAGE}

# 在 RUN 中使用 ARG
RUN echo "Building version ${VERSION}"

# 通过 --build-arg 传递
# docker build --build-arg VERSION=2.0.0 .

ENV vs ARG 对比

特性 ENV ARG
构建阶段 ✅ 可用 ✅ 可用
运行阶段 ✅ 可用 ❌ 不可用
docker run -e ✅ 可覆盖 ❌ 不可用
缓存影响 变化触发重建 变化触发重建

5.2.10 EXPOSE ------ 声明端口

dockerfile 复制代码
# EXPOSE 声明容器监听的端口(仅文档作用)
EXPOSE 80
EXPOSE 443
EXPOSE 8080/tcp
EXPOSE 5000/udp

# ⚠️ EXPOSE 不会自动发布端口!
# 必须通过 -p 或 -P 参数发布
# docker run -p 8080:80 myimage
# docker run -P myimage  # 自动映射所有 EXPOSE 的端口

5.2.11 VOLUME ------ 声明卷

dockerfile 复制代码
# 声明匿名卷(数据持久化)
VOLUME /data
VOLUME ["/data", "/logs"]

# ⚠️ VOLUME 声明后,对该目录的修改会存储到卷中
# ⚠️ 卷在容器删除后仍然存在

5.2.12 USER ------ 切换用户

dockerfile 复制代码
# 创建应用用户并切换
RUN groupadd -r appuser && useradd -r -g appuser appuser

# 切换到非 root 用户运行
USER appuser

# ⚠️ 安全最佳实践:不要用 root 运行应用!
# USER 之后的所有指令和容器运行时都使用该用户

5.2.13 HEALTHCHECK ------ 健康检查

dockerfile 复制代码
# HEALTHCHECK 定义容器的健康检查策略
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost/ || exit 1

# 参数说明:
# --interval=30s    检查间隔(默认30s)
# --timeout=3s      超时时间(默认30s)
# --start-period=5s 启动等待时间(默认0s)
# --retries=3       失败重试次数(默认3)

# 禁用健康检查
HEALTHCHECK NONE

5.2.14 LABEL ------ 元数据标签

dockerfile 复制代码
# LABEL 为镜像添加元数据
LABEL maintainer="ops@example.com"
LABEL version="1.0"
LABEL description="My Python Web Application"
LABEL org.opencontainers.image.source="https://github.com/example/myapp"

# 多行 LABEL
LABEL maintainer="ops@example.com" \
      version="1.0" \
      description="My Python Web Application"

5.2.15 SHELL ------ 指定 Shell

dockerfile 复制代码
# 更改 RUN 指令使用的默认 Shell
SHELL ["/bin/bash", "-c"]

# 使用 PowerShell(Windows 容器)
SHELL ["powershell", "-Command"]

# 示例:确保 bash 可用
RUN apt-get update && apt-get install -y bash
SHELL ["/bin/bash", "-c"]
RUN echo "Hello from bash"

5.2.16 .dockerignore ------ 排除文件

dockerignore 复制代码
# .dockerignore 排除不需要发送到构建上下文的文件
# 类似于 .gitignore

.git
.gitignore
Dockerfile
docker-compose*.yml
README.md
.env
*.md
.vscode
.idea
__pycache__
*.pyc
node_modules
npm-debug.log
coverage
.nyc_output
test/
tests/
tmp/
*.log

5.3 多阶段构建(Multi-stage Build)

5.3.1 为什么需要多阶段构建

复制代码
问题:一个 Node.js 应用的构建和运行

单阶段构建:
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build        # 生成 dist/ 目录(~50MB)
RUN npm prune --production  # 保留生产依赖
EXPOSE 3000
CMD ["node", "dist/index.js"]

最终镜像大小:~1GB(包含了 Node.js 编译工具链、源码、dev 依赖等)

5.3.2 多阶段构建解决方案

dockerfile 复制代码
# ========== 阶段 1:构建 ==========
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# ========== 阶段 2:运行 ==========
FROM node:20-slim AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

# 最终镜像大小:~200MB(只有运行时需要的文件)

5.3.3 多阶段构建的高级用法

dockerfile 复制代码
# ========== Go 应用多阶段构建 ==========
# 构建阶段
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 静态编译,不依赖任何系统库
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .

# 运行阶段(使用空白镜像)
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

# 最终镜像大小:~10MB

# ========== Python 应用多阶段构建 ==========
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

FROM python:3.11-slim
WORKDIR /app
# 从 builder 阶段复制安装好的依赖
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python3", "app.py"]

5.3.4 从指定阶段复制

dockerfile 复制代码
# 选择性复制特定阶段的产物
FROM node:20 AS frontend
WORKDIR /app
COPY frontend/ .
RUN npm ci && npm run build

FROM node:20 AS backend
WORKDIR /app
COPY backend/ .
RUN npm ci && npm run build

FROM nginx:latest
# 只复制前端构建产物
COPY --from=frontend /app/dist /usr/share/nginx/html
# 也可以复制后端产物
# COPY --from=backend /app/dist /app

5.4 构建缓存机制

5.4.1 缓存的工作原理

复制代码
docker build 执行流程:

指令1: FROM ubuntu:22.04
  → 检查缓存:有!使用缓存层 ✓

指令2: RUN apt-get update && apt-get install -y curl
  → 检查缓存:有!使用缓存层 ✓

指令3: COPY requirements.txt /app/
  → 检查 requirements.txt 的 hash
  → 与上次构建时的 hash 对比
  → 相同!使用缓存层 ✓

指令4: RUN pip install -r requirements.txt
  → 检查缓存:有!使用缓存层 ✓

指令5: COPY . /app/
  → 检查 .dockerignore 排除后的文件 hash
  → 与上次构建时的 hash 对比
  → 不同!❌ 缓存失效,重新执行

指令6: RUN python3 app.py
  → 缓存已失效(指令5变更),重新执行 ❌

优化原则:
1. 变化频率低的指令放前面
2. 变化频率高的指令放后面
3. 利用 COPY 与 RUN 的分离来最大化缓存命中

5.4.2 缓存优化策略

dockerfile 复制代码
# ❌ 反面教材:每次代码修改都会重新安装依赖
COPY . /app/
RUN pip install -r requirements.txt
RUN python3 app.py

# ✅ 正确做法:先复制依赖文件,再复制代码
COPY requirements.txt /app/     # 依赖文件很少变化
RUN pip install -r /app/requirements.txt
COPY . /app/                    # 代码经常变化
RUN python3 app.py

5.5 企业级 Dockerfile 最佳实践

5.5.1 完整的生产级 Dockerfile 示例

dockerfile 复制代码
# ============================================
# 企业级 Python Flask 应用 Dockerfile
# ============================================

# Stage 1: 构建
FROM python:3.11-slim AS builder

# 设置工作目录
WORKDIR /app

# 安装构建依赖
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        gcc \
        libffi-dev \
    && rm -rf /var/lib/apt/lists/*

# 先复制依赖文件(利用缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# Stage 2: 运行
FROM python:3.11-slim AS production

# 设置元数据
LABEL maintainer="ops@example.com"
LABEL version="1.0"
LABEL description="Production Flask Application"

# 设置工作目录
WORKDIR /app

# 安装运行时依赖(极小化)
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        curl \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# 从 builder 阶段复制依赖
COPY --from=builder /install /usr/local

# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser

# 复制应用代码
COPY --chown=appuser:appuser . .

# 切换到非 root 用户
USER appuser

# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    APP_ENV=production \
    APP_PORT=5000

# 声明端口
EXPOSE 5000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD ["python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"]

# 启动命令
ENTRYPOINT ["python3"]
CMD ["app.py"]

5.5.2 最佳实践清单

复制代码
✅ DO(推荐做法):

1. 使用多阶段构建减小镜像体积
2. 使用 alpine 或 slim 基础镜像
3. 合并 RUN 指令减少层数
4. 先复制依赖文件,后复制代码(利用缓存)
5. 使用 .dockerignore 排除无关文件
6. 使用非 root 用户运行应用
7. 添加 HEALTHCHECK 健康检查
8. 使用 LABEL 添加元数据
9. 设置 PYTHONUNBUFFERED 等环境变量
10. 清理包管理器缓存(rm -rf /var/lib/apt/lists/*)

❌ DON'T(避免做法):

1. 不要在生产镜像中包含源代码和构建工具
2. 不要使用 root 用户运行应用
3. 不要在 RUN 中存储密码或敏感信息
4. 不要安装不必要的包(用 --no-install-recommends)
5. 不要使用 :latest 标签(版本不可控)
6. 不要将 Dockerfile 放在 Docker 构建上下文根目录
7. 不要忽略 .dockerignore
8. 不要在一个 RUN 中运行多个不相关的命令

5.6 构建命令详解

5.6.1 docker build 基本用法

bash 复制代码
# 基本构建
docker build -t myapp:v1.0 .

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

# 传入构建参数
docker build -t myapp:v1.0 --build-arg VERSION=1.0.0 .

# 不使用缓存
docker build -t myapp:v1.0 --no-cache .

# 指定目标阶段(多阶段构建)
docker build -t myapp:v1.0 --target production .

# 传递 secret(不缓存到层中)
docker build -t myapp:v1.0 --secret id=npmrc,src=.npmrc .

# 传递 SSH 密钥
docker build -t myapp:v1.0 --ssh default .

5.6.2 BuildKit 构建引擎

bash 复制代码
# 启用 BuildKit(Docker 18.09+ 默认启用)
DOCKER_BUILDKIT=1 docker build -t myapp:v1.0 .

# BuildKit 的优势:
# 1. 并行构建多个阶段
# 2. 更好的缓存管理
# 3. 支持 secret mount(安全传递密钥)
# 4. 支持 SSH mount
# 5. 更高效的层管理

# 在 Dockerfile 中使用 BuildKit 特性
# syntax=docker/dockerfile:1

5.7 动手实验

实验 5.1:编写基础 Dockerfile

bash 复制代码
# 创建实验目录
mkdir -p ~/docker-lab/05-dockerfile
cd ~/docker-lab/05-dockerfile

# 创建一个简单的 Python 应用
cat > app.py << 'EOF'
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"Hello from Docker! Running on {os.environ.get('HOSTNAME', 'unknown')}"

@app.route('/health')
def health():
    return {'status': 'healthy'}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
EOF

cat > requirements.txt << 'EOF'
flask==3.0.0
EOF

# 创建 Dockerfile
cat > Dockerfile << 'EOF'
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python3", "app.py"]
EOF

# 构建并运行
docker build -t myflask:v1.0 .
docker run -d -p 5000:5000 --name flask-test myflask:v1.0

# 测试
curl http://localhost:5000

# 清理
docker stop flask-test
docker rm flask-test

实验 5.2:多阶段构建对比

bash 复制代码
# 创建实验目录
cd ~/docker-lab/05-dockerfile

# 单阶段构建
cat > Dockerfile.single << 'EOF'
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
EOF

# 多阶段构建
cat > Dockerfile.multi << 'EOF'
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
EOF

# 对比镜像大小
docker build -t myapp:single -f Dockerfile.single .
docker build -t myapp:multi -f Dockerfile.multi .

docker images myapp:single myapp:multi
# REPOSITORY   TAG      SIZE
# myapp        single   ~1.2GB
# myapp        multi    ~200MB

5.8 本章小结

指令 作用 注意事项
FROM 指定基础镜像 选择合适的精简基础镜像
RUN 执行命令 合并多条,清理缓存
COPY 复制文件 优先于 ADD
ADD 增强复制 自动解压 tar
CMD 默认命令 可被 docker run 覆盖
ENTRYPOINT 入口点 不会被 docker run 覆盖
WORKDIR 工作目录 自动创建目录
ENV 环境变量 运行时可用
ARG 构建参数 仅构建时可用
EXPOSE 声明端口 仅文档作用
VOLUME 声明卷 数据持久化
USER 切换用户 安全最佳实践
HEALTHCHECK 健康检查 生产必须
LABEL 元数据 添加维护信息

5.9 课后练习

  1. 基础题:为你的 Python/Node.js/Java 项目编写一个优化的 Dockerfile。
  2. 进阶题:使用多阶段构建将镜像体积减少 50% 以上。
  3. 最佳实践:检查你的 Dockerfile 是否符合本章的最佳实践清单。

📖 下一章:Docker 容器生命周期 ------ 掌握容器的创建、运行、停止和删除