第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 课后练习
- 基础题:为你的 Python/Node.js/Java 项目编写一个优化的 Dockerfile。
- 进阶题:使用多阶段构建将镜像体积减少 50% 以上。
- 最佳实践:检查你的 Dockerfile 是否符合本章的最佳实践清单。
📖 下一章:Docker 容器生命周期 ------ 掌握容器的创建、运行、停止和删除