前言: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 - 文件复制

COPYADD 都可以将文件复制到镜像中,但用途不同。

# 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 - 执行命令

CMDENTRYPOINT 定义容器启动时执行的命令。

# 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 高效管理多个容器的方法。