第05章:Dockerfile 深度解析
第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~20MBGoogle 的无 shell 镜像安全性最高scratch0MB静态编译的 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 [--chownuser:group] src... dest # 基本用法 COPY requirements.txt /app/ COPY . /app/ # 使用通配符 COPY *.py /app/ COPY html/ /app/html/ # 使用 --chown 设置所有者 COPY --chownappuser:appuser . /app/ # 使用 --chmod 设置权限Docker 18.09 COPY --chmod755 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 对比特性COPYADD复制文件✅✅自动解压 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 传入的命令会覆盖 CMDCMD 是容器的默认启动命令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 对比特性CMDENTRYPOINT覆盖方式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_VERSION1.0.0 ENV PYTHONUNBUFFERED1 # 多行设置 ENV PYTHONUNBUFFERED1 \ PYTHONDONTWRITEBYTECODE1 \ PIP_NO_CACHE_DIR1 # 环境变量在后续指令中可用 ENV MY_NAMEJohn Doe RUN echo Hello, $MY_NAME # 在容器运行时也可以使用 # docker run -e MY_NAMEJane myimage5.2.9 ARG —— 构建参数# ARG 在构建时可用运行时不可用 ARG VERSION1.0.0 ARG REGISTRYregistry.example.com # 在 FROM 中使用 ARG ARG BASE_IMAGEpython:3.11-slim FROM ${BASE_IMAGE} # 在 RUN 中使用 ARG RUN echo Building version ${VERSION} # 通过 --build-arg 传递 # docker build --build-arg VERSION2.0.0 .ENV vs ARG 对比特性ENVARG构建阶段✅ 可用✅ 可用运行阶段✅ 可用❌ 不可用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 --interval30s --timeout3s --start-period5s --retries3 \ CMD curl -f http://localhost/ || exit 1 # 参数说明 # --interval30s 检查间隔默认30s # --timeout3s 超时时间默认30s # --start-period5s 启动等待时间默认0s # --retries3 失败重试次数默认3 # 禁用健康检查 HEALTHCHECK NONE5.2.14 LABEL —— 元数据标签# LABEL 为镜像添加元数据 LABEL maintaineropsexample.com LABEL version1.0 LABEL descriptionMy Python Web Application LABEL org.opencontainers.image.sourcehttps://github.com/example/myapp # 多行 LABEL LABEL maintaineropsexample.com \ version1.0 \ descriptionMy Python Web Application5.2.15 SHELL —— 指定 Shell# 更改 RUN 指令使用的默认 Shell SHELL [/bin/bash, -c] # 使用 PowerShellWindows 容器 SHELL [powershell, -Command] # 示例确保 bash 可用 RUN apt-get update apt-get install -y bash SHELL [/bin/bash, -c] RUN echo Hello from bash5.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 Build5.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 --frombuilder /app/dist ./dist COPY --frombuilder /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_ENABLED0 GOOSlinux go build -ldflags-s -w -o /app/server . # 运行阶段使用空白镜像 FROM scratch COPY --frombuilder /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 --frombuilder /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 --fromfrontend /app/dist /usr/share/nginx/html # 也可以复制后端产物 # COPY --frombackend /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 maintaineropsexample.com LABEL version1.0 LABEL descriptionProduction 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 --frombuilder /install /usr/local # 创建非 root 用户 RUN groupadd -r appuser useradd -r -g appuser -d /app -s /sbin/nologin appuser # 复制应用代码 COPY --chownappuser:appuser . . # 切换到非 root 用户 USER appuser # 设置环境变量 ENV PYTHONUNBUFFERED1 \ PYTHONDONTWRITEBYTECODE1 \ APP_ENVproduction \ APP_PORT5000 # 声明端口 EXPOSE 5000 # 健康检查 HEALTHCHECK --interval30s --timeout5s --start-period10s --retries3 \ 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/* ❌ DONT避免做法 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-argVERSION1.0.0.# 不使用缓存dockerbuild-tmyapp:v1.0 --no-cache.# 指定目标阶段多阶段构建dockerbuild-tmyapp:v1.0--targetproduction.# 传递 secret不缓存到层中dockerbuild-tmyapp:v1.0--secretidnpmrc,src.npmrc.# 传递 SSH 密钥dockerbuild-tmyapp:v1.0--sshdefault.5.6.2 BuildKit 构建引擎# 启用 BuildKitDocker 18.09 默认启用DOCKER_BUILDKIT1dockerbuild-tmyapp:v1.0.# BuildKit 的优势# 1. 并行构建多个阶段# 2. 更好的缓存管理# 3. 支持 secret mount安全传递密钥# 4. 支持 SSH mount# 5. 更高效的层管理# 在 Dockerfile 中使用 BuildKit 特性# syntaxdocker/dockerfile:15.7 动手实验实验 5.1编写基础 Dockerfile# 创建实验目录mkdir-p~/docker-lab/05-dockerfilecd~/docker-lab/05-dockerfile# 创建一个简单的 Python 应用catapp.pyEOF from flask import Flask import os app Flask(__name__) app.route(/) def hello(): return fHello from Docker! Running on {os.environ.get(HOSTNAME, unknown)} app.route(/health) def health(): return {status: healthy} if __name__ __main__: app.run(host0.0.0.0, port5000) EOFcatrequirements.txtEOF flask3.0.0 EOF# 创建 DockerfilecatDockerfileEOF 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# 单阶段构建catDockerfile.singleEOF FROM node:20 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build EXPOSE 3000 CMD [node, dist/index.js] EOF# 多阶段构建catDockerfile.multiEOF 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 --frombuilder /app/dist ./dist COPY --frombuilder /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复制文件优先于 ADDADD增强复制自动解压 tarCMD默认命令可被 docker run 覆盖ENTRYPOINT入口点不会被 docker run 覆盖WORKDIR工作目录自动创建目录ENV环境变量运行时可用ARG构建参数仅构建时可用EXPOSE声明端口仅文档作用VOLUME声明卷数据持久化USER切换用户安全最佳实践HEALTHCHECK健康检查生产必须LABEL元数据添加维护信息5.9 课后练习基础题为你的 Python/Node.js/Java 项目编写一个优化的 Dockerfile。进阶题使用多阶段构建将镜像体积减少 50% 以上。最佳实践检查你的 Dockerfile 是否符合本章的最佳实践清单。 下一章Docker 容器生命周期 —— 掌握容器的创建、运行、停止和删除