第05章:Dockerfile 深度解析
本章目标:全面掌握 Dockerfile 的每条指令,理解构建缓存机制,编写企业级的高效 Dockerfile。
5.1 Dockerfile 是什么
Dockerfile 是一个文本文件,包含了一系列指令(Instruction),用于自动化构建 Docker 镜像。每条指令都会在镜像中创建一个新的层。
Dockerfile → docker build → Docker Image ↓ 逐行执行指令 每条指令生成一层 层层叠加形成最终镜像5.2 Dockerfile 指令全解析
5.2.1 FROM —— 指定基础镜像
# 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 —— 执行命令
# 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 —— 复制文件
# 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
# ADD 比 COPY 多了两个功能: # 1. 自动解压 tar 文件 ADD app.tar.gz /app/ # 2. 支持 URL 下载(不推荐,建议用 RUN curl) ADD https://example.com/file.tar.gz /tmp/ # 最佳实践:大多数情况下使用 COPY 更清晰 # 只在需要自动解压时使用 ADDCOPY vs ADD 对比:
| 特性 | COPY | ADD |
|---|---|---|
| 复制文件 | ✅ | ✅ |
| 自动解压 tar | ❌ | ✅ |
| URL 下载 | ❌ | ✅ |
| 语义清晰度 | ✅ 明确 | ⚠️ 有隐式行为 |
| 推荐度 | ⭐⭐⭐ 推荐 | 特定场景使用 |
5.2.5 CMD —— 容器启动命令
# 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 —— 入口点
# 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.pyCMD vs ENTRYPOINT 对比:
| 特性 | CMD | ENTRYPOINT |
|---|---|---|
| 覆盖方式 | docker run 参数覆盖 | 需要 --entrypoint 才能覆盖 |
| 默认命令 | 可以被覆盖 | 不会被覆盖 |
| 用途 | 定义默认命令 | 定义容器的固定入口 |
| 多个定义 | 只有最后一个生效 | 只有最后一个生效 |
5.2.7 WORKDIR —— 工作目录
# 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 /app5.2.8 ENV —— 环境变量
# 设置环境变量 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" myimage5.2.9 ARG —— 构建参数
# 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 —— 声明端口
# 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 —— 声明卷
# 声明匿名卷(数据持久化) VOLUME /data VOLUME ["/data", "/logs"] # ⚠️ VOLUME 声明后,对该目录的修改会存储到卷中 # ⚠️ 卷在容器删除后仍然存在5.2.12 USER —— 切换用户
# 创建应用用户并切换 RUN groupadd -r appuser && useradd -r -g appuser appuser # 切换到非 root 用户运行 USER appuser # ⚠️ 安全最佳实践:不要用 root 运行应用! # USER 之后的所有指令和容器运行时都使用该用户5.2.13 HEALTHCHECK —— 健康检查
# 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 NONE5.2.14 LABEL —— 元数据标签
# 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
# 更改 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 排除不需要发送到构建上下文的文件 # 类似于 .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/ *.log5.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 多阶段构建解决方案
# ========== 阶段 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 多阶段构建的高级用法
# ========== 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 从指定阶段复制
# 选择性复制特定阶段的产物 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 /app5.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 缓存优化策略
# ❌ 反面教材:每次代码修改都会重新安装依赖 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.py5.5 企业级 Dockerfile 最佳实践
5.5.1 完整的生产级 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 基本用法
# 基本构建dockerbuild-tmyapp:v1.0.# 指定 Dockerfiledockerbuild-tmyapp:v1.0-fDockerfile.prod.# 传入构建参数dockerbuild-tmyapp:v1.0 --build-argVERSION=1.0.0.# 不使用缓存dockerbuild-tmyapp:v1.0 --no-cache.# 指定目标阶段(多阶段构建)dockerbuild-tmyapp:v1.0--targetproduction.# 传递 secret(不缓存到层中)dockerbuild-tmyapp:v1.0--secretid=npmrc,src=.npmrc.# 传递 SSH 密钥dockerbuild-tmyapp:v1.0--sshdefault.5.6.2 BuildKit 构建引擎
# 启用 BuildKit(Docker 18.09+ 默认启用)DOCKER_BUILDKIT=1dockerbuild-tmyapp:v1.0.# BuildKit 的优势:# 1. 并行构建多个阶段# 2. 更好的缓存管理# 3. 支持 secret mount(安全传递密钥)# 4. 支持 SSH mount# 5. 更高效的层管理# 在 Dockerfile 中使用 BuildKit 特性# syntax=docker/dockerfile:15.7 动手实验
实验 5.1:编写基础 Dockerfile
# 创建实验目录mkdir-p~/docker-lab/05-dockerfilecd~/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) EOFcat>requirements.txt<<'EOF' flask==3.0.0 EOF# 创建 Dockerfilecat>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# 构建并运行dockerbuild-tmyflask:v1.0.dockerrun-d-p5000:5000--nameflask-test myflask:v1.0# 测试curlhttp://localhost:5000# 清理dockerstop flask-testdockerrmflask-test实验 5.2:多阶段构建对比
# 创建实验目录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# 对比镜像大小dockerbuild-tmyapp:single-fDockerfile.single.dockerbuild-tmyapp:multi-fDockerfile.multi.dockerimages myapp:single myapp:multi# REPOSITORY TAG SIZE# myapp single ~1.2GB# myapp multi ~200MB5.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 容器生命周期 —— 掌握容器的创建、运行、停止和删除