一句话总结
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。