Dockerfile 最佳实践?

2025年 阅读约 12 分钟 面试指南 · Linux/Docker

深入解析Dockerfile最佳实践:多阶段构建(分离构建与运行)、指令优化(COPY vs ADD、CMD vs ENTRYPOINT)、镜像瘦身技巧、安全实践、Spring Boot应用镜像示例,附面试模拟问答。

一句话总结

Dockerfile 最佳实践:多阶段构建(分离构建环境和运行环境,最终镜像不含编译工具)、选择最小基础镜像(alpine/slim 替代完整版)、合并 RUN 指令(减少镜像层,用 && 连接)、COPY 优于 ADD(ADD 会自动解压 tar 和远程下载,非必要不用)、CMD 定义默认命令,ENTRYPOINT 定义容器入口.dockerignore 排除无关文件非 root 用户运行(USER 指令)。核心目标:镜像越小越安全,构建越快越可靠

初级理解

Dockerfile 核心指令

# Dockerfile 核心指令速查: FROM openjdk:11-jre-slim # 基础镜像 LABEL maintainer="dev@example.com" # 标签(元数据) WORKDIR /app # 工作目录(后续指令基于此) COPY target/app.jar app.jar # 复制文件(推荐) ADD target/app.tar.gz /app/ # 复制+自动解压(谨慎使用) RUN apt update && apt install -y curl # 构建时执行命令 ENV JAVA_OPTS="-Xmx512m" # 环境变量 EXPOSE 8080 # 声明端口(文档作用) VOLUME /data # 声明数据卷 USER 1000 # 切换用户 CMD ["java", "-jar", "app.jar"] # 默认启动命令 ENTRYPOINT ["java", "-jar", "app.jar"] # 入口点 # CMD vs ENTRYPOINT: # CMD:默认命令,docker run 可覆盖 # ENTRYPOINT:固定入口,docker run 参数作为追加 # 组合使用:ENTRYPOINT 定义程序,CMD 定义默认参数 ENTRYPOINT ["java", "-jar", "app.jar"] CMD ["--server.port=8080"] # docker run myapp --server.port=8081 # 实际执行:java -jar app.jar --server.port=8081

简单 Spring Boot Dockerfile

# 基础版(不推荐,镜像大) FROM openjdk:11 COPY target/app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] # 优化版(推荐) FROM openjdk:11-jre-slim # 用 JRE 而非 JDK,slim 更小 WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 # JVM 参数通过环境变量传入 ENV JAVA_OPTS="-Xms256m -Xmx512m" ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

中级深入

多阶段构建(Multi-stage Build)

# 多阶段构建:分离构建环境和运行环境 # 最终镜像只包含运行需要的文件,不含 Maven/JDK 等构建工具 # === 阶段1:构建阶段 === FROM maven:3.8-openjdk-11 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline # 缓存依赖(利用 Docker 缓存) COPY src ./src RUN mvn package -DskipTests # === 阶段2:运行阶段 === FROM openjdk:11-jre-slim WORKDIR /app # 从构建阶段复制 JAR 包 COPY --from=builder /build/target/app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] # 多阶段构建的好处: # 1. 最终镜像不含 Maven/JDK(从 600MB → 200MB) # 2. 构建缓存更高效(依赖层单独 COPY) # 3. 安全性更高(减少攻击面)

镜像瘦身技巧

# 1. 选择最小基础镜像 # openjdk:11 → 600MB+ # openjdk:11-jre → 300MB # openjdk:11-jre-slim → 200MB # openjdk:11-jre-alpine → 100MB(但需注意 glibc 兼容性) # 2. 合并 RUN 指令(减少层数) # 不推荐(每行一个层): RUN apt update RUN apt install -y curl RUN apt clean RUN rm -rf /var/lib/apt/lists/* # 推荐(合并为一个层): RUN apt update && \ apt install -y curl && \ apt clean && \ rm -rf /var/lib/apt/lists/* # 3. .dockerignore 排除无关文件 # .dockerignore 内容: target/ # 构建产物(通过 COPY 精确复制) .git/ *.md *.log # 4. 清理包管理器缓存 # apt: rm -rf /var/lib/apt/lists/* # yum: yum clean all # apk: rm -rf /var/cache/apk/*

高级进阶

Dockerfile 安全实践

# 1. 非 root 用户运行 # 不推荐(root 运行): FROM openjdk:11-jre-slim COPY app.jar /app.jar ENTRYPOINT ["java", "-jar", "/app.jar"] # 推荐(非 root 用户): FROM openjdk:11-jre-slim RUN groupadd -r appuser && useradd -r -g appuser appuser USER appuser COPY --chown=appuser:appuser app.jar /app.jar ENTRYPOINT ["java", "-jar", "/app.jar"] # 2. 使用特定版本标签,不用 latest # 不推荐: FROM openjdk:latest # 推荐: FROM openjdk:11.0.16-jre-slim # 3. 最小权限原则 # 只安装运行必需的包 # 不安装调试工具(curl/vim/telnet)到生产镜像 # 4. 敏感信息不要写在 Dockerfile 中 # 不推荐: ENV DB_PASSWORD=123456 # 推荐:运行时传入 # docker run -e DB_PASSWORD=123456 myapp # 或使用 Docker Secrets(Swarm/K8s)

构建缓存优化

# Docker 构建缓存原理: # 每行指令生成一个层,如果层没变则使用缓存 # 优化顺序:变化频率低的放前面 # 不推荐(每次改代码都重装依赖): COPY . /app RUN mvn package # 推荐(依赖单独一层,代码变化不影响依赖缓存): COPY pom.xml /app/ RUN mvn dependency:go-offline # 依赖缓存层 COPY src /app/src RUN mvn package # 只有代码变化才重新编译 # 缓存失效规则: # 1. 某层变化 → 该层及之后所有层重建 # 2. COPY 的文件内容变化 → 缓存失效 # 3. RUN 命令文本变化 → 缓存失效 # 4. --no-cache 强制不使用缓存

实战场景

# 场景1:Spring Boot 生产级 Dockerfile FROM maven:3.8-openjdk-11 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM openjdk:11-jre-slim RUN groupadd -r appuser && useradd -r -g appuser appuser WORKDIR /app COPY --from=builder --chown=appuser:appuser \ /build/target/app.jar app.jar EXPOSE 8080 USER appuser ENV JAVA_OPTS="-Xms256m -Xmx512m \ -XX:+UseG1GC \ -Djava.security.egd=file:/dev/./urandom" ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] # 场景2:前端应用 Dockerfile(Nginx) FROM node:16-alpine AS builder WORKDIR /build COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /build/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

面试模拟

面试官:什么是多阶段构建?有什么好处?

你:多阶段构建在一个 Dockerfile 中定义多个 FROM,分离构建环境和运行环境。好处:1)最终镜像不含编译工具(Maven/JDK),体积大幅减小;2)构建缓存更高效(依赖层单独缓存);3)安全性更高(减少攻击面)。从构建阶段 COPY --from=builder 复制产物到运行阶段。

面试官:CMD 和 ENTRYPOINT 有什么区别?

你:CMD 定义默认命令,docker run 可覆盖。ENTRYPOINT 定义容器入口,docker run 参数作为追加。组合使用:ENTRYPOINT 定义程序,CMD 定义默认参数。如 ENTRYPOINT ["java", "-jar", "app.jar"] + CMD ["--port=8080"],docker run myapp --port=8081 实际执行 java -jar app.jar --port=8081。