跳到主要内容

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. 选择合适的基础镜像

镜像大小适用场景
scratch0 MBGo 静态二进制
alpine~5 MB极简 Linux
distroless~20 MBGoogle 推荐,无 Shell
slim~80 MBDebian 精简版
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 镜像体积?

答案

  1. 使用 alpine/distroless 基础镜像
  2. 多阶段构建:编译和运行分离
  3. 合并 RUN 指令:减少镜像层数
  4. 清理缓存rm -rf /var/lib/apt/lists/*
  5. 使用 .dockerignore:排除不需要的文件
  6. 只安装运行依赖npm ci --production
  7. Go/Rust 使用静态编译 + scratch

Q2: COPY 和 ADD 的区别?

答案

  • COPY:简单的文件/目录复制,推荐使用
  • ADD:除了复制外,还支持自动解压 tar 文件和从 URL 下载

最佳实践:优先使用 COPY,只在需要解压 tar 时才用 ADD。不推荐用 ADD 下载 URL,应使用 RUN curlRUN wget

Q3: Docker 构建缓存机制是怎样的?

答案

Docker 逐条执行 Dockerfile 指令,每条生成一个镜像层。如果指令和上下文没有变化,就复用缓存层。

缓存失效条件:

  1. 指令本身变化(如 RUN 命令修改)
  2. COPY/ADD 的源文件内容变化(通过校验和判断)
  3. 上一层缓存失效后,后续所有层都失效

所以应该把变化频率低的指令放前面(如安装依赖),变化频率高的放后面(如复制源码)。

Q4: 为什么推荐用非 root 用户运行容器?

答案

容器默认以 root 运行。如果容器被攻破,攻击者就获得了 root 权限。虽然有 Namespace 隔离,但内核漏洞可能导致逃逸。

使用非 root 用户可以:

  1. 降低容器逃逸后的危害
  2. 符合最小权限原则
  3. Kubernetes PodSecurityPolicy/Standards 要求
RUN adduser -D -u 1000 appuser
USER appuser

相关链接