Docker 完全指南 / 06 - Dockerfile 详解
06 - Dockerfile 详解
全面掌握 Dockerfile 指令、多阶段构建、缓存优化与镜像构建最佳实践。
6.1 Dockerfile 基础
Dockerfile 是一个文本文件,包含一系列指令,用于自动化构建 Docker 镜像。
基本结构
# 注释
INSTRUCTION arguments
最小 Dockerfile 示例
FROM alpine:3.19
RUN echo "Hello, Docker!" > /hello.txt
CMD ["cat", "/hello.txt"]
# 构建镜像
docker build -t my-hello .
# 运行
docker run --rm my-hello
# 输出: Hello, Docker!
6.2 核心指令详解
6.2.1 FROM — 基础镜像
# 指定基础镜像(必须是第一条非注释指令)
FROM ubuntu:22.04
# 使用最小基础镜像
FROM alpine:3.19
# 使用 scratch(空镜像,用于静态二进制)
FROM scratch
# 多架构构建中使用 $TARGETPLATFORM
FROM --platform=$BUILDPLATFORM node:20-alpine
| 基础镜像 | 大小 | 包管理器 | 适用场景 |
|---|---|---|---|
alpine:3.19 | ~7MB | apk | 最小镜像,Go/Rust 静态二进制 |
debian:bookworm-slim | ~75MB | apt | 需要 glibc 的应用 |
ubuntu:22.04 | ~77MB | apt | 通用场景 |
python:3.11-slim | ~125MB | apt | Python 应用 |
node:20-alpine | ~130MB | apk | Node.js 应用 |
scratch | 0MB | 无 | 静态编译的 Go 二进制 |
6.2.2 RUN — 执行命令
# Shell 格式(通过 /bin/sh -c 执行)
RUN apt-get update && apt-get install -y curl
# Exec 格式(直接执行,不经过 shell)
RUN ["apt-get", "install", "-y", "curl"]
# 多条命令合并为一层(减少镜像层数)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
wget \
ca-certificates && \
rm -rf /var/lib/apt/lists/*
最佳实践: 每个 RUN 指令创建一个镜像层。将相关命令合并到一个 RUN 中,并在最后清理缓存。
6.2.3 COPY 与 ADD
# COPY: 复制本地文件到镜像
COPY requirements.txt /app/
COPY src/ /app/src/
COPY --chown=appuser:appgroup config.yml /app/
# ADD: 类似 COPY,但有额外功能
ADD app.tar.gz /app/ # 自动解压 tar 文件
ADD https://example.com/file /app/ # 支持 URL 下载(不推荐)
| 特性 | COPY | ADD |
|---|---|---|
| 复制本地文件 | ✅ | ✅ |
| 自动解压 tar | ❌ | ✅ |
| 支持 URL | ❌ | ✅(不推荐) |
| 推荐使用 | ✅ 首选 | 仅在需要自动解压时 |
建议: 优先使用
COPY,语义更清晰。仅在需要自动解压.tar.gz时使用ADD。
6.2.4 CMD 与 ENTRYPOINT
# CMD: 容器启动时的默认命令(可被 docker run 参数覆盖)
CMD ["nginx", "-g", "daemon off;"]
# ENTRYPOINT: 容器的入口点(不会被轻易覆盖)
ENTRYPOINT ["python", "app.py"]
# 组合使用: ENTRYPOINT + CMD
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run my-app → python app.py
# docker run my-app test.py → python test.py
| 指令 | 可被覆盖 | 用途 |
|---|---|---|
| CMD | ✅ docker run 参数覆盖 | 默认命令/参数 |
| ENTRYPOINT | 仅 --entrypoint 覆盖 | 固定入口程序 |
6.2.5 ENV — 环境变量
# 设置环境变量(构建时和运行时都生效)
ENV NODE_ENV=production
ENV APP_HOME=/app \
APP_USER=appuser
# 构建时使用,运行时不生效
ARG VERSION=1.0
ARG BUILD_DATE
| 指令 | 构建时 | 运行时 | 覆盖方式 |
|---|---|---|---|
| ENV | ✅ | ✅ | docker run -e |
| ARG | ✅ | ❌ | docker build --build-arg |
6.2.6 EXPOSE — 声明端口
# 声明容器监听的端口(仅文档作用,不实际发布端口)
EXPOSE 80
EXPOSE 443
EXPOSE 8080/tcp
EXPOSE 9090/udp
注意:
EXPOSE不会自动发布端口。运行时仍需-p选项映射端口。
6.2.7 WORKDIR — 工作目录
# 设置工作目录(不存在会自动创建)
WORKDIR /app
# 后续的 RUN, CMD, COPY 等指令都基于此目录
避免: 不要使用
RUN cd /app,因为每条 RUN 是独立层,cd 不会持续。
6.2.8 USER — 指定用户
# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# 切换到非 root 用户
USER appuser
# 之后的命令都以 appuser 身份运行
6.2.9 VOLUME — 声明挂载点
# 声明数据卷挂载点
VOLUME ["/data", "/var/log"]
注意: VOLUME 指令后的层对挂载目录的修改不会持久化。尽量在 Dockerfile 最后使用。
6.2.10 ARG — 构建参数
ARG NODE_VERSION=20
ARG APP_VERSION
FROM node:${NODE_VERSION}-alpine
# 注意: ARG 在 FROM 之后需要重新声明
ARG APP_VERSION
RUN echo "Building version $APP_VERSION"
# 使用 ARG 设置默认 ENV
ARG DEFAULT_PORT=3000
ENV PORT=$DEFAULT_PORT
# 传递构建参数
docker build --build-arg NODE_VERSION=18 --build-arg APP_VERSION=1.0.0 -t my-app .
6.3 完整 Dockerfile 示例
Python Flask 应用
# ---- 基础镜像 ----
FROM python:3.11-slim AS base
# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
# ---- 依赖安装(利用缓存) ----
FROM base AS deps
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ---- 生产镜像 ----
FROM base AS production
# 从 deps 阶段复制已安装的依赖
COPY --from=deps /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=deps /usr/local/bin /usr/local/bin
# 创建非 root 用户
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
# 复制应用代码
COPY --chown=appuser:appgroup . .
# 切换用户
USER appuser
EXPOSE 5000
# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]
Node.js 应用
FROM node:20-alpine
# 设置工作目录
WORKDIR /app
# 复制依赖文件(利用缓存层)
COPY package.json package-lock.json ./
# 安装依赖
RUN npm ci --only=production
# 复制应用代码
COPY . .
# 创建非 root 用户
RUN chown -R node:node /app
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Go 应用(静态二进制)
# ---- 构建阶段 ----
FROM golang:1.22-alpine AS builder
WORKDIR /build
# 下载依赖(缓存层)
COPY go.mod go.sum ./
RUN go mod download
# 编译应用
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app ./cmd/server
# ---- 运行阶段 ----
FROM scratch
COPY --from=builder /app /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/app"]
6.4 多阶段构建(Multi-Stage Build)
多阶段构建是减小镜像大小的关键技术。
基本语法
# 阶段 1: 构建
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o server .
# 阶段 2: 运行
FROM alpine:3.19
COPY --from=builder /app/server /usr/local/bin/
CMD ["server"]
多阶段构建的优势
| 对比 | 单阶段构建 | 多阶段构建 |
|---|---|---|
| 镜像大小 | 包含编译器和源码 (1GB+) | 仅包含运行时 (10-50MB) |
| 安全性 | 包含构建工具,攻击面大 | 最小化攻击面 |
| 复杂度 | 简单 | 稍复杂 |
| 推荐场景 | 简单脚本 | 生产环境 |
命名阶段与选择性复制
# 阶段 1: 依赖
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# 阶段 2: 构建
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# 阶段 3: 生产
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package*.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
# 阶段 4: 测试(仅在构建测试镜像时使用)
FROM node:20-alpine AS test
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
CMD ["npm", "test"]
# 构建生产镜像(默认最后一个阶段)
docker build -t my-app:prod .
# 构建测试镜像
docker build --target test -t my-app:test .
# 构建到特定阶段
docker build --target build -t my-app:build .
从外部镜像复制
# 从其他镜像复制文件(无需安装)
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=busybox:latest /bin/sh /bin/sh
6.5 缓存优化策略
Docker 构建缓存原理
构建过程:
指令 1: FROM ubuntu:22.04 → 使用缓存 ✅
指令 2: RUN apt-get update → 检查缓存...
└─ 缓存命中 → 跳过执行 ✅
└─ 缓存未命中 → 执行命令 ❌,此后的所有层都重新构建
缓存优化规则
# ❌ 错误: COPY . . 会破坏缓存
COPY . .
RUN pip install -r requirements.txt
# ✅ 正确: 先复制依赖文件,再复制代码
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
优化排序原则
# 按变化频率从低到高排序
FROM python:3.11-slim
# 1. 系统依赖(很少变化)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev && \
rm -rf /var/lib/apt/lists/*
# 2. 应用依赖(偶尔变化)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 3. 应用代码(频繁变化)
COPY . .
CMD ["python", "app.py"]
BuildKit 缓存挂载
# 使用 --mount=type=cache 缓存包管理器目录
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && apt-get install -y libpq-dev
# 缓存 pip 下载目录
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# 缓存 npm 目录
RUN --mount=type=cache,target=/root/.npm \
npm ci
# 缓存 Go 模块
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /app .
6.6 .dockerignore 文件
# .dockerignore 示例
# Git
.git
.gitignore
# IDE
.vscode
.idea
*.swp
*.swo
# 依赖目录
node_modules
__pycache__
*.pyc
.venv
# 构建产物
dist
build
target
# 文档
README.md
docs
*.md
# Docker 相关
Dockerfile*
docker-compose*.yml
.dockerignore
# 系统文件
.DS_Store
Thumbs.db
# 测试
tests
*.test
coverage
重要:
.dockerignore必须放在构建上下文的根目录(与 Dockerfile 同级)。
6.7 健康检查(HEALTHCHECK)
# 基本健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 使用 wget(Alpine 没有 curl)
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
# 禁用基础镜像的健康检查
HEALTHCHECK NONE
| 参数 | 默认值 | 说明 |
|---|---|---|
--interval | 30s | 检查间隔 |
--timeout | 30s | 超时时间 |
--start-period | 0s | 启动等待时间 |
--retries | 3 | 失败重试次数 |
# 查看健康状态
docker inspect --format '{{.State.Health.Status}}' my-container
# 查看健康检查日志
docker inspect --format '{{json .State.Health.Log}}' my-container | jq .
6.8 入口点脚本(Entrypoint Script)
典型入口点脚本
# Dockerfile
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
#!/bin/sh
# docker-entrypoint.sh
set -e
# 等待数据库就绪
if [ "$DB_HOST" ]; then
echo "Waiting for database at $DB_HOST..."
while ! nc -z "$DB_HOST" "${DB_PORT:-5432}"; do
sleep 1
done
echo "Database is ready!"
fi
# 执行数据库迁移
if [ "$RUN_MIGRATIONS" = "true" ]; then
echo "Running migrations..."
python manage.py migrate
fi
# 执行用户传入的命令(CMD)
exec "$@"
6.9 构建命令详解
# 基本构建
docker build -t my-app:v1.0 .
# 指定 Dockerfile 路径
docker build -f docker/Dockerfile.prod -t my-app:prod .
# 传递构建参数
docker build --build-arg VERSION=1.0 --build-arg ENV=prod -t my-app .
# 不使用缓存
docker build --no-cache -t my-app .
# 指定目标阶段
docker build --target production -t my-app:prod .
# 使用 BuildKit(默认启用)
DOCKER_BUILDKIT=1 docker build -t my-app .
# 构建并输出到本地(Buildx)
docker buildx build --load -t my-app .
# 构建并推送到仓库(Buildx)
docker buildx build --push -t my-user/my-app:v1.0 .
6.10 镜像标签策略
# 语义化版本
docker build -t my-app:1.0.0 -t my-app:1.0 -t my-app:1 -t my-app:latest .
# Git commit hash
GIT_SHA=$(git rev-parse --short HEAD)
docker build -t my-app:$GIT_SHA -t my-app:latest .
# 日期标签
DATE=$(date +%Y%m%d)
docker build -t my-app:$DATE .
| 标签策略 | 示例 | 适用场景 |
|---|---|---|
| 语义版本 | 1.0.0, 1.0 | 正式发布 |
| Git SHA | a1b2c3d | CI/CD 流水线 |
| 日期 | 20240101 | 每日构建 |
| 分支名 | feature-login | 开发分支 |
| latest | latest | 仅用于开发,不用于生产 |
要点回顾
| 要点 | 核心内容 |
|---|---|
| 指令顺序 | 按变化频率排序:系统依赖 → 应用依赖 → 应用代码 |
| 多阶段构建 | 分离构建环境和运行环境,大幅减小镜像大小 |
| 缓存优化 | .dockerignore + 合理 COPY 顺序 + BuildKit 缓存挂载 |
| 安全 | 使用非 root 用户、最小基础镜像、健康检查 |
| CMD vs ENTRYPOINT | CMD 可覆盖,ENTRYPOINT 不可覆盖 |
注意事项
不要在镜像中存储密钥: 不要在 Dockerfile 中硬编码密码、API Key 等敏感信息。使用构建密钥 (
--secret) 或运行时环境变量。
合并 RUN 层: 每个 RUN 指令创建一层。将相关命令合并,减少层数和镜像大小。
清理缓存: 在同一 RUN 中安装包后清理缓存(如
rm -rf /var/lib/apt/lists/*),避免增大镜像。
下一步
→ 07 - 网络模型:学习 Docker 的网络模式和端口映射。