Docker & Kubernetes 完全掌握 第3篇: Dockerfile 编写与镜像构建
Docker & Kubernetes Complete Guide Part 3: Dockerfile Writing and Image Build
前言:Dockerfile 的重要性
Docker 系列第3篇将深入介绍创建容器镜像的核心要素 Dockerfile。Dockerfile 是将应用程序容器化的蓝图,良好的编写是实现高效安全容器运营的第一步。
本文将详细介绍 Dockerfile 的基本概念、主要指令、多阶段构建、构建缓存利用以及安全相关的最佳实践。
1. 什么是 Dockerfile?
1.1 Dockerfile 的定义
Dockerfile 是用于构建 Docker 镜像的文本文件。该文件从基础镜像选择到应用程序安装、环境配置、执行命令等,按顺序定义了镜像生成所需的所有指令。
Dockerfile 的主要特点:
- 可重现性:使用相同的 Dockerfile 可以在任何时间任何地点构建相同的镜像
- 版本管理:作为文本文件,可以用 Git 等版本控制系统追踪变更历史
- 自动化:可以在 CI/CD 流水线中自动构建和部署镜像
- 文档化:Dockerfile 本身起到记录应用程序运行环境的作用
1.2 基本结构
一个简单的 Node.js 应用程序 Dockerfile 示例:
# 指定基础镜像
FROM node:20-alpine
# 设置工作目录
WORKDIR /app
# 复制包文件并安装依赖
COPY package*.json ./
RUN npm install
# 复制源代码
COPY . .
# 设置环境变量
ENV NODE_ENV=production
# 暴露端口
EXPOSE 3000
# 执行命令
CMD ["node", "server.js"]
2. Dockerfile 主要指令
2.1 FROM - 指定基础镜像
FROM 是所有 Dockerfile 的第一条指令,指定新镜像的基础镜像。
# 基本格式
FROM <镜像>:<标签>
# 示例
FROM ubuntu:22.04
FROM python:3.11-slim
FROM node:20-alpine
FROM scratch # 从空镜像开始
选择基础镜像时的考虑因素:
- Alpine 镜像:非常小的体积(约5MB),安全性高,可能存在一些兼容性问题
- Slim 镜像:移除不必要包的轻量版本
- 官方镜像:建议使用 Docker Hub 验证的官方镜像
2.2 RUN - 执行命令
RUN 在镜像构建过程中执行命令,并将结果保存为新层。
# Shell 格式
RUN apt-get update && apt-get install -y curl
# Exec 格式
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "curl"]
# 将多个命令合并为一个 RUN(最小化层)
RUN apt-get update \
&& apt-get install -y \
curl \
vim \
git \
&& rm -rf /var/lib/apt/lists/*
2.3 COPY vs ADD - 文件复制
COPY 和 ADD 都可以将文件复制到镜像中,但用途不同。
# COPY - 将本地文件/目录复制到镜像
COPY src/ /app/src/
COPY package.json package-lock.json ./
# ADD - COPY 功能 + 额外功能
# 1. 从 URL 下载文件
ADD https://example.com/file.tar.gz /tmp/
# 2. 自动解压压缩文件 (tar, gzip, bzip2, xz)
ADD archive.tar.gz /app/
建议:大多数情况下使用 COPY。只在需要 URL 下载或自动解压时使用 ADD。
2.4 WORKDIR - 设置工作目录
WORKDIR 设置后续 RUN、CMD、ENTRYPOINT、COPY、ADD 指令执行的工作目录。
# 设置工作目录
WORKDIR /app
# 如果不存在则自动创建
WORKDIR /app/src
# 也可以使用相对路径(基于前一个 WORKDIR)
WORKDIR subdir # /app/src/subdir
2.5 ENV - 设置环境变量
ENV 设置容器运行时使用的环境变量。
# 单个环境变量
ENV NODE_ENV=production
# 多个环境变量(一行)
ENV NODE_ENV=production PORT=3000
# 多个环境变量(多行)
ENV NODE_ENV=production \
PORT=3000 \
DB_HOST=localhost
2.6 EXPOSE - 端口文档化
EXPOSE 记录容器将使用的端口。实际端口绑定通过 docker run -p 进行。
# 单个端口
EXPOSE 3000
# 多个端口
EXPOSE 80 443
# UDP 端口
EXPOSE 53/udp
2.7 CMD vs ENTRYPOINT - 执行命令
CMD 和 ENTRYPOINT 定义容器启动时执行的命令。
# CMD - 默认执行命令(可被 docker run 覆盖)
CMD ["node", "server.js"]
CMD ["npm", "start"]
# ENTRYPOINT - 固定执行命令
ENTRYPOINT ["python", "app.py"]
# CMD 和 ENTRYPOINT 组合
ENTRYPOINT ["python"]
CMD ["app.py"] # 可通过 docker run image other.py 更改
| 指令 | 作用 | 覆盖方式 |
|---|---|---|
| CMD | 默认命令/参数 | 可被 docker run 参数覆盖 |
| ENTRYPOINT | 固定执行文件 | 只能通过 --entrypoint 选项更改 |
2.8 其他有用的指令
# ARG - 构建时变量(不保存在镜像中)
ARG VERSION=1.0
ARG BUILD_DATE
# LABEL - 添加元数据
LABEL maintainer="dev@example.com"
LABEL version="1.0"
LABEL description="My application"
# USER - 更改执行用户
USER node
USER 1001:1001
# VOLUME - 声明卷挂载点
VOLUME /data
VOLUME ["/var/log", "/var/db"]
# HEALTHCHECK - 容器健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
3. 多阶段构建
3.1 什么是多阶段构建?
多阶段构建是在单个 Dockerfile 中定义多个阶段(stage),将构建所需的工具与最终运行所需的文件分离的技术。通过这种方式可以大大减小最终镜像的大小。
3.2 实战示例: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 -o main .
# 运行阶段
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# 只从构建阶段复制二进制文件
COPY --from=builder /app/main .
CMD ["./main"]
3.3 实战示例:Node.js 应用程序
# 依赖安装阶段
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 运行阶段
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# 只复制生产依赖
COPY --from=deps /app/node_modules ./node_modules
# 只复制构建产物
COPY --from=builder /app/dist ./dist
COPY package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
3.4 多阶段构建的优势
- 减小镜像大小:只包含运行所需的文件,不包含构建工具
- 增强安全性:构建时使用的源代码、认证信息等不会包含在最终镜像中
- 利用构建缓存:可以独立缓存每个阶段
4. .dockerignore 文件
4.1 什么是 .dockerignore?
.dockerignore 文件指定要从 Docker 构建上下文中排除的文件和目录。使用与 .gitignore 类似的语法。
4.2 .dockerignore 示例
# Git 相关
.git
.gitignore
# Node.js
node_modules
npm-debug.log
# 构建产物(非多阶段情况)
dist
build
# 环境配置
.env
.env.local
*.env
# IDE
.vscode
.idea
*.swp
# 测试
coverage
__tests__
*.test.js
# 文档
README.md
docs
# Docker 相关
Dockerfile*
docker-compose*
.docker
4.3 .dockerignore 的重要性
- 提高构建速度:不必要的文件不包含在构建上下文中,减少传输时间
- 减小镜像大小:防止 COPY 指令复制不必要的文件
- 安全性:防止
.env、证书等敏感文件包含在镜像中
5. 镜像构建 (docker build)
5.1 基本构建命令
# 使用当前目录的 Dockerfile 构建
docker build -t myapp:1.0 .
# 指定其他 Dockerfile
docker build -f Dockerfile.prod -t myapp:prod .
# 传递构建参数
docker build --build-arg VERSION=2.0 -t myapp:2.0 .
# 忽略构建缓存
docker build --no-cache -t myapp:latest .
# 只构建到特定阶段
docker build --target builder -t myapp:builder .
5.2 构建上下文
构建上下文是传递给 docker build 命令的文件和目录集合。最后一个参数(`.`)指定构建上下文路径。
# 使用当前目录作为构建上下文
docker build -t myapp .
# 使用特定目录作为构建上下文
docker build -t myapp /path/to/context
# 直接从 Git 仓库构建
docker build -t myapp https://github.com/user/repo.git
# 从 stdin 读取 Dockerfile
docker build -t myapp - < Dockerfile
6. 利用构建缓存
6.1 缓存工作原理
Docker 在构建时将每条指令的结果缓存为层。与前一次构建相同的指令会重用缓存的层。
缓存失效的条件:
- 指令发生变化
- COPY/ADD 的源文件发生变化
- 上层的缓存失效
6.2 提高缓存效率的技巧
# 不好的例子:源代码变化时 npm install 也会重新执行
COPY . .
RUN npm install
# 好的例子:package.json 不变则使用缓存
COPY package*.json ./
RUN npm install
COPY . .
将频繁变化的层放在 Dockerfile 下方可以提高缓存效率。
7. 镜像标签和版本管理
7.1 标签策略
# 版本标签
docker build -t myapp:1.0.0 .
docker build -t myapp:1.0 .
docker build -t myapp:1 .
# 环境标签
docker build -t myapp:prod .
docker build -t myapp:staging .
docker build -t myapp:dev .
# Git 提交哈希标签
docker build -t myapp:$(git rev-parse --short HEAD) .
# 日期标签
docker build -t myapp:$(date +%Y%m%d) .
7.2 添加和更改标签
# 为现有镜像添加新标签
docker tag myapp:1.0.0 myapp:latest
docker tag myapp:1.0.0 registry.example.com/myapp:1.0.0
# 推送到仓库
docker push registry.example.com/myapp:1.0.0
8. Dockerfile 最佳实践
8.1 最小化层
# 不好的例子:创建不必要的层
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*
# 好的例子:合并为一个 RUN
RUN apt-get update \
&& apt-get install -y curl vim \
&& rm -rf /var/lib/apt/lists/*
8.2 安全考虑
# 1. 以非 root 用户运行
FROM node:20-alpine
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nodejs -u 1001
USER nodejs
# 2. 使用可信的基础镜像
FROM node:20-alpine@sha256:abc123...
# 3. 禁止安装不必要的包
RUN apt-get install --no-install-recommends -y curl
# 4. 敏感信息在运行时传递而非 ENV
# 不好的例子
ENV DATABASE_PASSWORD=secret123
# 好的例子:使用 docker run -e DATABASE_PASSWORD=xxx
# 5. 只复制特定文件而非 COPY 全部
COPY src/ /app/src/
COPY package.json /app/
8.3 镜像大小优化
- 尽可能使用小的基础镜像(Alpine, distroless)
- 利用多阶段构建
- 删除不必要的文件(缓存、临时文件)
- 积极利用 .dockerignore
8.4 完整的 Dockerfile 示例
# 生产环境 Node.js 应用程序 Dockerfile
# 应用多阶段构建和安全最佳实践
# 1. 依赖安装阶段
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# 2. 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 3. 生产阶段
FROM node:20-alpine AS runner
LABEL maintainer="dev@example.com"
LABEL version="1.0"
WORKDIR /app
ENV NODE_ENV=production
# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nodejs -u 1001
# 只复制必要的文件
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# 更改所有权并切换用户
RUN chown -R nodejs:nodejs /app
USER nodejs
# 设置健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
EXPOSE 3000
CMD ["node", "dist/server.js"]
结论
Dockerfile 编写是容器化的核心技术。本文内容总结如下:
- 理解基本指令:FROM、RUN、COPY、WORKDIR、ENV、EXPOSE、CMD、ENTRYPOINT 的作用和使用方法
- 多阶段构建:分离构建和运行环境以最小化镜像大小
- .dockerignore:从构建中排除不必要的文件
- 构建缓存:优化层顺序以提高构建速度
- 最佳实践:考虑安全性、大小、可维护性的 Dockerfile 编写
下一篇第4篇将介绍使用 Docker Compose 高效管理多个容器的方法。