Dockerfile 最佳实践
Dockerfile 基础
常用指令
| 指令 | 说明 | 示例 |
|---|---|---|
FROM | 基础镜像 | FROM node:20-alpine |
WORKDIR | 工作目录 | WORKDIR /app |
COPY | 复制文件 | COPY package.json . |
ADD | 复制文件(支持解压、URL) | ADD app.tar.gz /app/ |
RUN | 执行命令(构建时) | RUN npm install |
CMD | 容器启动命令 | CMD ["node", "server.js"] |
ENTRYPOINT | 容器入口点 | ENTRYPOINT ["nginx"] |
ENV | 环境变量 | ENV NODE_ENV=production |
ARG | 构建参数 | ARG VERSION=latest |
EXPOSE | 声明端口(文档用途) | EXPOSE 3000 |
VOLUME | 声明挂载点 | VOLUME ["/data"] |
USER | 运行用户 | USER node |
HEALTHCHECK | 健康检查 | HEALTHCHECK CMD curl -f http://localhost/ |
CMD vs ENTRYPOINT
# CMD:定义默认命令,可被 docker run 参数覆盖
CMD ["nginx", "-g", "daemon off;"]
# docker run myimage → 执行 nginx
# docker run myimage /bin/sh → 执行 /bin/sh(覆盖 CMD)
# ENTRYPOINT:定义入口点,docker run 参数作为追加参数
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
# docker run myimage → nginx -g daemon off;
# docker run myimage -t → nginx -t(测试配置)
ENTRYPOINT + CMD 最佳组合
ENTRYPOINT 定义可执行程序,CMD 定义默认参数。这样既有默认行为,又允许用户自定义参数。
多阶段构建
多阶段构建是镜像瘦身的最佳实践,将编译环境和运行环境分离:
Dockerfile - Go 应用
# ===== 构建阶段 =====
FROM golang:1.22-alpine 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 alpine:3.19
# 安装证书(HTTPS 请求需要)
RUN apk --no-cache add ca-certificates tzdata
# 非 root 用户运行
RUN adduser -D -u 1000 appuser
USER appuser
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
ENTRYPOINT ["./server"]
Dockerfile - Node.js 应用
# ===== 构建阶段 =====
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# ===== 生产依赖 =====
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --prod --frozen-lockfile
# ===== 运行阶段 =====
FROM node:20-alpine
RUN adduser -D -u 1000 appuser
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json .
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/main.js"]
Dockerfile - Java Spring Boot
# ===== 构建阶段 =====
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B
# ===== 运行阶段 =====
FROM eclipse-temurin:21-jre-alpine
RUN adduser -D -u 1000 appuser
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
镜像优化技巧
1. 选择合适的基础镜像
| 镜像 | 大小 | 适用场景 |
|---|---|---|
scratch | 0 MB | Go 静态二进制 |
alpine | ~5 MB | 极简 Linux |
distroless | ~20 MB | Google 推荐,无 Shell |
slim | ~80 MB | Debian 精简版 |
ubuntu | ~78 MB | 需要 apt 包 |
| 完整版 | ~300+ MB | 开发环境 |
2. 合并 RUN 指令减少层数
# ❌ 多层(每个 RUN 都是一层)
RUN apt-get update
RUN apt-get install -y nginx
RUN rm -rf /var/lib/apt/lists/*
# ✅ 单层
RUN apt-get update && \
apt-get install -y --no-install-recommends nginx && \
rm -rf /var/lib/apt/lists/*
3. 利用构建缓存
# ✅ 先复制依赖文件,再复制源码
# 依赖不变时,npm install 这层可以命中缓存
COPY package.json package-lock.json ./
RUN npm ci --production
COPY . .
# ❌ 直接复制所有文件
# 任何源码变化都会导致 npm install 重新执行
COPY . .
RUN npm ci --production
4. .dockerignore
.dockerignore
node_modules
.git
.gitignore
Dockerfile
docker-compose.yml
.env
*.md
.vscode
coverage
dist
.next
5. 镜像大小对比
# 查看镜像大小
docker images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"
# 分析镜像层
docker history myimage:latest
# 使用 dive 工具分析镜像层(推荐)
dive myimage:latest
HEALTHCHECK 健康检查
# HTTP 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# TCP 端口检查(无 curl 时)
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
# 自定义脚本
HEALTHCHECK --interval=30s --timeout=5s \
CMD /app/healthcheck.sh || exit 1
# 查看健康状态
docker inspect --format='{{json .State.Health}}' container_name | jq
构建命令
# 基础构建
docker build -t myapp:v1 .
# 指定 Dockerfile
docker build -f Dockerfile.prod -t myapp:prod .
# 传递构建参数
docker build --build-arg VERSION=1.2.3 -t myapp:1.2.3 .
# 不使用缓存
docker build --no-cache -t myapp:v1 .
# 多平台构建(ARM + AMD64)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:v1 --push .
常见面试问题
Q1: 如何优化 Docker 镜像体积?
答案:
- 使用 alpine/distroless 基础镜像
- 多阶段构建:编译和运行分离
- 合并 RUN 指令:减少镜像层数
- 清理缓存:
rm -rf /var/lib/apt/lists/* - 使用 .dockerignore:排除不需要的文件
- 只安装运行依赖:
npm ci --production - Go/Rust 使用静态编译 + scratch
Q2: COPY 和 ADD 的区别?
答案:
COPY:简单的文件/目录复制,推荐使用ADD:除了复制外,还支持自动解压 tar 文件和从 URL 下载
最佳实践:优先使用 COPY,只在需要解压 tar 时才用 ADD。不推荐用 ADD 下载 URL,应使用 RUN curl 或 RUN wget。
Q3: Docker 构建缓存机制是怎样的?
答案:
Docker 逐条执行 Dockerfile 指令,每条生成一个镜像层。如果指令和上下文没有变化,就复用缓存层。
缓存失效条件:
- 指令本身变化(如 RUN 命令修改)
- COPY/ADD 的源文件内容变化(通过校验和判断)
- 上一层缓存失效后,后续所有层都失效
所以应该把变化频率低的指令放前面(如安装依赖),变化频率高的放后面(如复制源码)。
Q4: 为什么推荐用非 root 用户运行容器?
答案:
容器默认以 root 运行。如果容器被攻破,攻击者就获得了 root 权限。虽然有 Namespace 隔离,但内核漏洞可能导致逃逸。
使用非 root 用户可以:
- 降低容器逃逸后的危害
- 符合最小权限原则
- Kubernetes PodSecurityPolicy/Standards 要求
RUN adduser -D -u 1000 appuser
USER appuser