欢迎你来读这篇博客。
这篇文章专门讲 Dockerfile。
前面的 Docker 基础文章更多关注容器、镜像、网络、数据卷、端口映射、常见部署这些内容;而 Dockerfile 是镜像构建的核心。一个 Dockerfile 写得好不好,会直接影响:
镜像体积
构建速度
构建缓存命中率
线上启动速度
安全风险
部署稳定性
CI/CD 流水线效率
团队后续维护成本
很多人写 Dockerfile 的第一阶段是:
能跑就行。
第二阶段是:
镜像怎么这么大?
第三阶段是:
为什么每次构建都这么慢?
第四阶段是:
为什么线上容器停不掉、日志爆了、密码泄露了、构建缓存又失效了?
这篇文章的目标就是把这些坑一次性讲清楚。
一、Dockerfile 到底是什么? Dockerfile 是一个文本文件,用来描述如何构建 Docker 镜像。
它不是 shell 脚本,但里面包含很多类似 shell 的构建步骤。Docker 会按照 Dockerfile 中的指令,一步一步构建出镜像。
一个最简单的 Java 应用 Dockerfile 可能是这样:
1 2 3 4 5 6 7 8 9 FROM eclipse-temurin:17 -jreWORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
构建命令:
1 docker build -t demo-app:1.0.0 .
运行命令:
1 docker run -d --name demo-app -p 8080:8080 demo-app:1.0.0
从表面看,Dockerfile 就几行内容。但它背后的东西不少:
flowchart LR
A[源码与构建上下文] --> B[docker build]
B --> C[读取 Dockerfile]
C --> D[执行每条指令]
D --> E[生成镜像层 Layer]
E --> F[组合成最终镜像]
F --> G[docker run]
G --> H[容器运行实例]
要写好 Dockerfile,先记住一个核心认知:
Dockerfile 不是部署脚本,而是镜像构建说明书。
也就是说,它的目标不是“在服务器上装一遍环境”,而是“构建一个可复现、可分发、可运行的镜像”。
二、Dockerfile 的核心心智模型 很多 Dockerfile 写得烂,不是因为命令不会,而是因为心智模型错了。
写 Dockerfile 必须理解下面几个概念。
1. 构建上下文 Build Context 当你执行:
1 docker build -t demo-app:1.0.0 .
最后那个 . 表示当前目录会作为 build context 发送给 Docker 构建器。
Dockerfile 中的:
1 COPY target/app.jar app.jar
只能复制 build context 里的文件。
例如目录结构如下:
1 2 3 4 5 demo/ ├── Dockerfile ├── target/ │ └── app.jar └── README.md
在 demo 目录执行:
1 docker build -t demo-app:1.0.0 .
此时 Dockerfile 可以访问 target/app.jar。
但如果你写:
通常是不行的,因为 ../app.jar 不在 build context 里面。
构建上下文越大,构建越慢,缓存越容易失效。所以 .dockerignore 非常重要。
2. 镜像层 Layer Docker 镜像不是一个单独的大文件,而是由多层组成。
一般来说:
FROM 会确定基础镜像层
RUN 会产生新的文件系统层
COPY 和 ADD 会产生新的文件系统层
CMD、ENTRYPOINT、ENV、EXPOSE 等更多是修改镜像配置元数据
示意图:
flowchart TB
L1[基础镜像层: debian / alpine / ubuntu] --> L2[安装运行时: JRE / Node / Nginx]
L2 --> L3[复制依赖文件]
L3 --> L4[复制应用文件]
L4 --> L5[配置环境变量与启动命令]
L5 --> IMG[最终镜像]
镜像层有两个重要特性:
层可以复用
层一旦创建,后续删除不一定真正减少历史层体积
例如:
1 2 3 RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/*
这看起来最后删除了 apt 缓存,但缓存可能已经存在于前面的 layer 中。
更好的写法是:
1 2 3 RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/*
因为安装和清理发生在同一层里。
3. 构建缓存 Build Cache Docker 构建时会尽量复用之前构建过的 layer。
假设 Dockerfile:
1 2 3 4 FROM eclipse-temurin:17 -jreWORKDIR /app COPY target/app.jar app.jar ENTRYPOINT ["java" , "-jar" , "app.jar" ]
如果 target/app.jar 没变,那么 COPY target/app.jar app.jar 这一层可能复用缓存。
如果 target/app.jar 变了,这一层和后续层都会重新构建。
缓存规则可以简化理解为:
前面的层没变,后面的层才可能复用;前面某层变了,后面基本都要重新来。
所以 Dockerfile 的指令顺序非常重要。
4. 构建阶段 Build Stage 多阶段构建允许一个 Dockerfile 里出现多个 FROM。
例如:
1 2 3 4 5 6 7 8 9 FROM maven:3.9 -eclipse-temurin-17 AS builderWORKDIR /build COPY . . RUN mvn clean package -DskipTests FROM eclipse-temurin:17 -jreWORKDIR /app COPY --from=builder /build/target/*.jar app.jar ENTRYPOINT ["java" , "-jar" , "app.jar" ]
第一个阶段负责构建,第二个阶段负责运行。
最终镜像只包含第二个阶段中的内容,不包含 Maven、不包含源码、不包含构建缓存。
三、Dockerfile 指令总览 常见指令如下:
指令
作用
是否常用
优化重点
FROM
指定基础镜像,开始一个构建阶段
是
基础镜像体积、安全、版本固定
RUN
构建时执行命令
是
缓存、层数、清理无用文件
COPY
从构建上下文复制文件
是
精准复制,避免全量复制
ADD
类似 COPY,额外支持 URL 和自动解压
偶尔
默认优先 COPY
WORKDIR
设置工作目录
是
替代大量 cd
ENV
设置环境变量
是
不要存敏感信息
ARG
设置构建参数
是
用于版本、构建开关
EXPOSE
声明容器监听端口
是
只声明,不等于发布端口
CMD
默认命令或参数
是
与 ENTRYPOINT 配合
ENTRYPOINT
容器入口命令
是
推荐 exec 格式
USER
指定运行用户
强烈建议
避免 root 运行
HEALTHCHECK
容器健康检查
建议
提高可观测性
LABEL
镜像元数据
建议
版本、作者、源码地址
VOLUME
声明挂载点
谨慎
生产镜像不建议乱写
SHELL
修改默认 shell
少量
Windows 或特殊 shell 场景
STOPSIGNAL
指定停止信号
偶尔
优雅停机场景
四、FROM:基础镜像选择 FROM 是 Dockerfile 的第一块地基。
1 FROM eclipse-temurin:17 -jre
选基础镜像时要考虑:
体积
安全漏洞数量
运行时依赖是否完整
架构支持,例如 amd64、arm64
维护频率
是否官方或可信发布者
与应用技术栈是否匹配
1. 常见基础镜像类型
镜像类型
示例
优点
缺点
完整操作系统镜像
ubuntu、debian
兼容性好
体积较大
精简系统镜像
debian:bookworm-slim
兼顾兼容性和体积
工具较少
Alpine 镜像
alpine
很小
musl libc 兼容性问题
语言运行时镜像
node、python、eclipse-temurin
使用方便
可能偏大
distroless 镜像
gcr.io/distroless/java17
小、安全面低
调试困难
scratch
scratch
极小
只适合静态二进制等场景
2. 不要盲目追求 Alpine 很多人会觉得 Alpine 小,所以所有镜像都用 Alpine。
这不总是对的。
Alpine 使用 musl libc,而很多常见 Linux 发行版使用 glibc。部分 Java、Python、Node 原生依赖、字体、图像处理、DNS、时区、本地化相关行为可能会出现差异。
建议:
Go 静态二进制:可以考虑 Alpine 或 scratch
Java 服务:优先考虑 eclipse-temurin:17-jre、eclipse-temurin:17-jre-alpine 需充分测试
Python 有 native 依赖:谨慎使用 Alpine
Node 有 native module:谨慎使用 Alpine
生产稳定优先:slim 类镜像通常更稳
3. 不要在生产中长期使用 latest 不推荐:
推荐:
1 FROM node:20 -bookworm-slim
或者:
1 FROM eclipse-temurin:17.0 .11 _9-jre
更严格可以固定 digest:
1 FROM eclipse-temurin:17 -jre@sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
优点:
构建可复现
避免上游 latest 变化导致线上构建不一致
便于安全审计和版本回滚
缺点:
digest 固定后需要主动升级基础镜像
维护成本更高
所以实际可以分层处理:
开发环境:可以用较宽松 tag
测试/预发:固定主版本或小版本
生产/合规场景:固定 digest
五、WORKDIR:别在 RUN 里到处 cd 不推荐:
1 2 3 RUN mkdir -p /app RUN cd /app && echo hello > a.txt COPY target/app.jar /app/app.jar
推荐:
1 2 3 4 WORKDIR /app RUN echo hello > a.txt COPY target/app.jar app.jar
WORKDIR 的好处:
可读性更好
后续 RUN、COPY、CMD、ENTRYPOINT 默认都基于该目录
避免一堆 cd /xxx && ...
如果目录不存在,WORKDIR 会创建目录。
推荐始终显式设置:
不要依赖基础镜像默认工作目录。
六、COPY 和 ADD:优先 COPY,谨慎 ADD 1. COPY COPY 用于把构建上下文中的文件复制到镜像中。
1 COPY target/app.jar app.jar
也可以复制目录:
1 COPY config/ /app/config/
2. ADD ADD 功能更多:
可以从 URL 添加文件
可以自动解压本地 tar 包
例如:
但也正因为它功能多,语义不够直接。大多数情况下推荐使用 COPY。
3. COPY 的优化原则 原则一:不要一上来 COPY 全部 不推荐:
1 2 COPY . . RUN mvn clean package -DskipTests
这样只要项目里任何文件变化,都会导致后续构建缓存失效。
推荐:
1 2 3 4 5 COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests
这样如果只是源码变化,依赖下载层可以复用。
原则二:只复制运行需要的文件 运行阶段不应该复制源码:
运行阶段更应该:
1 COPY --from=builder /build/target/app.jar app.jar
原则三:合理使用 --chown 如果最终使用非 root 用户运行,可以在复制文件时直接设置 owner。
1 COPY --chown =app:app target/app.jar app.jar
这样可以避免额外的:
1 RUN chown -R app:app /app
4. COPY 和缓存 COPY 会参与缓存校验。被复制文件的内容或相关元数据变化,会导致该层缓存失效。
所以:
高频变化的文件尽量放后面复制
低频变化的依赖声明文件放前面复制
大文件和无关文件不要进入 build context
七、RUN:构建时执行命令 RUN 在镜像构建阶段执行。
例如:
1 2 3 RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/*
1. RUN 的常见问题 问题一:拆太多层 不推荐:
1 2 3 RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/*
推荐:
1 2 3 RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/*
问题二:安装推荐包导致镜像膨胀 不推荐:
1 RUN apt-get install -y curl
推荐:
1 2 3 RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/*
问题三:构建依赖进入运行镜像 不推荐:
1 2 3 4 5 6 7 8 FROM node:20 WORKDIR /app COPY . . RUN npm install RUN npm run build CMD ["npm" , "run" , "start" ]
如果只是前端静态资源,最终不应该包含 Node、npm、源码和 node_modules。
推荐多阶段构建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FROM node:20 AS builderWORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:stable-alpineCOPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx" , "-g" , "daemon off;" ]
2. RUN 的 shell 格式和 exec 格式 shell 格式:
等价于:
1 RUN /bin/sh -c "echo hello"
exec 格式:
多数 Linux 镜像中,RUN 常用 shell 格式,因为方便写管道、变量和多命令。
八、ARG 和 ENV:构建参数与运行环境变量 1. ARG ARG 是构建时参数。
1 2 3 ARG APP_VERSION=1.0 .0 RUN echo "build version: ${APP_VERSION} "
构建时传入:
1 docker build --build-arg APP_VERSION=1.2.0 -t demo-app:1.2.0 .
ARG 适合:
版本号
下载地址
构建开关
是否跳过测试
构建环境标识
2. ENV ENV 是镜像环境变量,容器运行时也会存在。
1 ENV JAVA_OPTS="-Xms256m -Xmx512m"
运行时可以覆盖:
1 docker run -e JAVA_OPTS="-Xms512m -Xmx1024m" demo-app:1.0.0
3. ARG 和 ENV 的区别
对比项
ARG
ENV
生效阶段
构建阶段
构建阶段和运行阶段
容器运行时是否存在
通常不存在
存在
是否适合敏感信息
不适合
不适合
典型用途
构建版本、构建开关
应用运行参数
注意:
不要把密码、Token、私钥写进 ARG 或 ENV。
因为它们可能出现在镜像历史、构建日志或运行环境中。
错误示例:
1 2 ARG GIT_TOKEN=xxxxxENV DB_PASSWORD=123456
正确思路:
构建阶段需要密钥:使用 BuildKit secret mount
运行阶段需要密钥:运行时注入、配置中心、密钥管理系统
九、EXPOSE:只是声明端口,不是端口映射 Dockerfile 中:
只是说明这个镜像内的应用会监听 8080 端口。
它不会自动让宿主机访问容器端口。
真正端口映射要在运行时做:
1 docker run -p 8080:8080 demo-app:1.0.0
可以这么理解:
flowchart LR
A[Dockerfile EXPOSE 8080] --> B[镜像元数据声明]
C[docker run -p 8080:8080] --> D[真正发布端口]
推荐写 EXPOSE,因为它是镜像文档的一部分。但不要误以为写了它就能访问服务。
十、CMD 和 ENTRYPOINT:容器启动命令 这两个指令很容易混。
1. CMD CMD 表示容器默认命令。
1 CMD ["java" , "-jar" , "app.jar" ]
运行时可以覆盖:
1 docker run demo-app:1.0.0 echo hello
这时 echo hello 会替代原来的 CMD。
2. ENTRYPOINT ENTRYPOINT 表示容器入口程序。
1 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
运行时追加的参数会接在 ENTRYPOINT 后面。
3. 推荐使用 exec 格式 推荐:
1 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
不推荐:
1 ENTRYPOINT java -jar app.jar
原因是 shell 格式会启动一个 shell 作为 PID 1,信号转发可能不如 exec 格式直接,影响容器优雅停止。
4. ENTRYPOINT + CMD 组合 可以把 ENTRYPOINT 当固定命令,把 CMD 当默认参数。
1 2 ENTRYPOINT ["java" , "-jar" , "app.jar" ] CMD ["--spring.profiles.active=prod" ]
运行时覆盖参数:
1 docker run demo-app:1.0.0 --spring.profiles.active=test
5. Java 应用支持 JAVA_OPTS 的写法 如果想用环境变量控制 JVM 参数,经常会写:
1 ENTRYPOINT ["sh" , "-c" , "java $JAVA_OPTS -jar app.jar" ]
这确实方便,但它用了 shell。
更强一点的做法是写一个入口脚本,并在脚本最后使用 exec:
1 2 3 4 #!/usr/bin/env sh set -eexec java ${JAVA_OPTS} -jar /app/app.jar "$@ "
Dockerfile:
1 2 3 4 5 COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh ENTRYPOINT ["docker-entrypoint.sh" ] CMD []
这样既支持环境变量,又能让 Java 进程接收信号。
十一、USER:不要用 root 跑应用 默认情况下,很多镜像里容器进程是 root 用户。
这不推荐。
更好的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 FROM eclipse-temurin:17 -jreRUN groupadd -r app && useradd -r -g app app WORKDIR /app COPY --chown =app:app target/app.jar app.jar USER appEXPOSE 8080 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
如果是 Alpine:
1 RUN addgroup -S app && adduser -S app -G app
为什么要用非 root?
降低容器逃逸后的破坏面
降低误删系统文件的风险
更符合最小权限原则
生产安全扫描更容易通过
注意:如果应用需要写目录,必须确保目录权限正确。
1 2 RUN mkdir -p /app/logs /app/tmp \ && chown -R app:app /app
十二、HEALTHCHECK:让容器状态更可观测 没有健康检查时,容器只要主进程没退出,Docker 就认为它还活着。
但这不代表服务真的可用。
比如:
Java 进程在,但数据库连接池挂了
Nginx 进程在,但配置错误
应用启动了,但健康接口返回 500
服务死锁了,但进程还没退出
可以加健康检查:
1 2 HEALTHCHECK --interval=30s --timeout =3s --retries=3 --start-period=20s \ CMD curl -f http://localhost:8080/actuator/health || exit 1
查看:
1 2 docker ps docker inspect demo-app | grep -A 30 Health
问题是:很多精简镜像里没有 curl。
可以选择:
安装 curl/wget
应用自身提供轻量健康检查命令
在编排层做健康检查
使用 shell 的 /dev/tcp,但可移植性一般
如果安装 curl:
1 2 3 RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/*
但这会增加镜像体积。生产上要权衡。
十三、LABEL:镜像元数据 LABEL 可以给镜像添加元数据。
1 2 3 4 5 LABEL org.opencontainers.image.title="demo-app" \ org.opencontainers.image.description="Spring Boot demo application" \ org.opencontainers.image.version="1.0.0" \ org.opencontainers.image.source="https://git.example.com/backend/demo-app" \ org.opencontainers.image.authors="team@example.com"
常见用途:
标记版本
标记源码仓库
标记构建时间
标记维护者
配合镜像扫描、SBOM、制品治理系统
可以查看:
1 docker inspect demo-app:1.0.0
十四、VOLUME:为什么生产镜像里要谨慎写 Dockerfile 可以写:
它表示该路径会作为挂载点。
但生产镜像里不建议随便写 VOLUME。
原因:
它可能创建匿名卷
匿名卷不容易管理
后续在 Dockerfile 中对该目录写入可能不符合预期
数据持久化应该由运行部署层明确声明
比如数据库官方镜像会声明数据目录,这是合理的。 但普通业务应用镜像里,除非非常明确,否则可以不写 VOLUME。
更推荐在运行时显式挂载:
1 docker run -v app-data:/app/data demo-app:1.0.0
十五、STOPSIGNAL:优雅停机相关 容器停止时,Docker 默认会给容器主进程发送 SIGTERM,等待一段时间后再发送 SIGKILL。
可以通过 Dockerfile 指定停止信号:
大多数 Linux 服务默认 SIGTERM 即可。
更关键的是:
应用进程必须是容器的主进程
ENTRYPOINT 推荐 exec 格式
Java/Spring Boot 要正确处理优雅停机
不要让 shell 吞掉信号
Spring Boot 可以配置:
1 2 3 4 5 6 server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s
Dockerfile 层面负责信号可达,应用层面负责优雅退出。
十六、.dockerignore:被严重低估的优化点 .dockerignore 用来排除不需要发送到构建上下文的文件。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .git .idea .vscode *.log *.tmp .DS_Store target/classes target/test-classes target/surefire-reports node_modules dist coverage .env .env.* *.pem *.key
1. 为什么它重要? 因为执行:
1 docker build -t demo-app:1.0.0 .
时,当前目录作为 build context 参与构建。
如果你不写 .dockerignore,可能会把这些东西都发给构建器:
.git
IDE 配置
本地日志
测试报告
node_modules
临时文件
密钥文件
大型数据文件
结果就是:
构建变慢
缓存更容易失效
镜像可能泄露敏感信息
CI/CD 流水线浪费时间
2. 常见错误 错误示例:
然后项目中没有 .dockerignore。
这等于把整个项目目录一股脑塞进去。 如果目录里有 .env、私钥、日志、测试数据,恭喜你,镜像变成了“赛博垃圾桶”。
3. Java 项目 .dockerignore 推荐 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .git .idea .vscode *.iml *.log .DS_Store target/classes target/generated-sources target/generated-test-sources target/maven-status target/surefire-reports target/test-classes .env .env.* *.pem *.key
如果是先在宿主机构建 jar,再 Docker build,可以保留:
4. Node 项目 .dockerignore 推荐 1 2 3 4 5 6 7 8 9 10 11 12 13 14 .git .idea .vscode *.log .DS_Store node_modules dist coverage .env .env.* *.pem *.key
十七、Docker 构建缓存深度优化 Dockerfile 优化里,缓存是核心。
1. 基本规则 Docker 构建缓存可以简化理解为:
flowchart TB
A[FROM 是否命中缓存] --> B[第一条指令是否命中缓存]
B --> C[第二条指令是否命中缓存]
C --> D[第三条指令是否命中缓存]
D --> E[后续层继续判断]
C -->|某层失效| F[该层之后全部重建]
一旦某一层缓存失效,后续层通常都要重新构建。
2. 指令顺序原则 核心原则:
低频变化的内容放前面,高频变化的内容放后面。
例如 Java Maven 项目:
不推荐:
1 2 COPY . . RUN mvn clean package -DskipTests
推荐:
1 2 3 4 5 COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests
因为:
pom.xml 变化频率低
src 变化频率高
依赖下载很耗时
源码变化不应该导致依赖重新下载
3. Node 项目缓存优化 不推荐:
1 2 3 COPY . . RUN npm install RUN npm run build
推荐:
1 2 3 4 5 COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build
只要 package-lock.json 不变,npm ci 层可以复用。
4. Python 项目缓存优化 不推荐:
1 2 COPY . . RUN pip install -r requirements.txt
推荐:
1 2 3 4 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . .
5. 缓存失效的常见来源
来源
说明
COPY . .
项目任意文件变化都可能导致缓存失效
未写 .dockerignore
日志、临时文件变化导致缓存失效
依赖和源码一起复制
源码变化导致依赖层重建
RUN apt-get update 单独一层
可能复用旧缓存,导致包索引不符合预期
构建参数变化
ARG 被使用后会影响后续缓存
基础镜像变化
FROM 变化导致整体重建
十八、BuildKit:现代 Dockerfile 优化能力 BuildKit 是 Docker 现代构建后端,提供更强的缓存、并行构建、secret、ssh mount、多平台构建等能力。
建议 Dockerfile 顶部加:
这表示使用 Dockerfile frontend 的语法版本。
1. 启用 BuildKit 较新 Docker 版本一般默认启用。也可以显式:
1 DOCKER_BUILDKIT=1 docker build -t demo-app:1.0.0 .
使用 buildx:
1 docker buildx build -t demo-app:1.0.0 .
2. cache mount:依赖缓存优化神器 传统写法里,依赖下载可能被 layer cache 影响。BuildKit 的 cache mount 可以提供一个跨构建复用的缓存目录。
Maven 缓存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FROM maven:3.9 -eclipse-temurin-17 AS builderWORKDIR /build COPY pom.xml . RUN --mount=type =cache,target=/root/.m2 \ mvn -B dependency:go-offline COPY src ./src RUN --mount=type =cache,target=/root/.m2 \ mvn -B clean package -DskipTests
这样 Maven 本地仓库 /root/.m2 可以在多次构建间复用。
npm 缓存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 FROM node:20 AS builderWORKDIR /app COPY package.json package-lock.json ./ RUN --mount=type =cache,target=/root/.npm \ npm ci COPY . . RUN npm run build
apt 缓存 1 2 3 4 5 6 RUN --mount=type =cache,target=/var/cache/apt \ --mount=type =cache,target=/var/lib/apt \ apt-get update \ && apt-get install -y --no-install-recommends curl
注意:不同发行版、不同包管理器的缓存目录不一样,使用前要确认。
3. secret mount:不要把密钥写进镜像 错误写法:
1 2 ARG TOKENRUN curl -H "Authorization: Bearer ${TOKEN} " https://example.com/private.tar.gz
这个 token 可能进入构建历史或日志。
BuildKit 推荐:
构建:
1 2 3 docker build \ --secret id =api_token,src=./api_token.txt \ -t demo-app:1.0.0 .
Dockerfile:
1 2 3 4 5 RUN --mount=type =secret,id =api_token \ TOKEN="$(cat /run/secrets/api_token) " \ && curl -H "Authorization: Bearer ${TOKEN} " https://example.com/private.tar.gz -o private.tar.gz
secret mount 的特点:
只在该 RUN 指令执行期间可见
不会持久化到镜像层
不应该出现在最终镜像里
4. ssh mount:拉取私有 Git 仓库 构建:
1 2 3 docker buildx build \ --ssh default \ -t demo-app:1.0.0 .
Dockerfile:
1 2 3 4 RUN --mount=type =ssh \ git clone git@github.com:example/private-repo.git
5. bind mount:构建时临时挂载源码 BuildKit 支持构建时 bind mount,适合某些只需要参与构建但不想进入镜像层和缓存的内容。
1 2 3 4 RUN --mount=type =bind ,target=/src \ cp /src/some-file /app/some-file
多数业务场景用 COPY 足够,bind mount 属于高级优化。
6. external cache:CI/CD 加速 CI/CD 机器经常是临时环境,本地缓存没法长期保留。可以使用 registry cache。
1 2 3 4 5 docker buildx build \ --cache-from=type =registry,ref=registry.example.com/demo-app:buildcache \ --cache-to=type =registry,ref=registry.example.com/demo-app:buildcache,mode=max \ -t registry.example.com/demo-app:1.0.0 \ --push .
这样不同 CI 构建任务之间可以复用远程缓存。
十九、多阶段构建深入 多阶段构建不是“高级技巧”,而是生产 Dockerfile 的基本功。
1. 为什么要多阶段? 以 Java 项目为例,构建需要:
运行只需要:
JRE
app.jar
配置
必要证书/字体/时区
如果把构建环境也放进最终镜像,会导致:
镜像体积大
安全面变大
源码泄露
构建依赖污染运行环境
2. 标准多阶段结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 FROM maven:3.9 -eclipse-temurin-17 AS depsWORKDIR /build COPY pom.xml . RUN --mount=type =cache,target=/root/.m2 \ mvn -B dependency:go-offline FROM deps AS builderWORKDIR /build COPY src ./src RUN --mount=type =cache,target=/root/.m2 \ mvn -B clean package -DskipTests FROM eclipse-temurin:17 -jre AS runtimeWORKDIR /app COPY --from=builder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
这里分了三个阶段:
阶段
作用
deps
下载依赖
builder
编译打包
runtime
最终运行
3. 命名阶段 推荐:
1 FROM maven:3.9 -eclipse-temurin-17 AS builder
然后:
1 COPY --from=builder /build/target/*.jar app.jar
不要用:
1 COPY --from=0 /build/target/*.jar app.jar
因为阶段顺序一改,数字就可能错。
4. 只构建某个阶段 调试时可以:
1 docker build --target builder -t demo-app:builder .
这对排查构建阶段问题非常有用。
5. debug stage 与 production stage 可以在一个 Dockerfile 里提供 debug 和 production 两种目标。
1 2 3 4 5 6 7 8 9 10 11 12 13 FROM eclipse-temurin:17 -jre AS baseWORKDIR /app COPY --from=builder /build/target/*.jar app.jar FROM base AS debugRUN apt-get update \ && apt-get install -y --no-install-recommends curl vim procps \ && rm -rf /var/lib/apt/lists/* ENTRYPOINT ["java" , "-jar" , "app.jar" ] FROM base AS productionUSER appENTRYPOINT ["java" , "-jar" , "app.jar" ]
构建 debug 镜像:
1 docker build --target debug -t demo-app:debug .
构建生产镜像:
1 docker build --target production -t demo-app:1.0.0 .
二十、Java / Spring Boot Dockerfile 深度优化 Java 是企业后端里非常常见的 Docker 化对象。这里重点讲 Spring Boot。
1. 最粗糙写法 1 2 3 4 5 6 7 8 9 FROM openjdk:17 WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 CMD java -jar app.jar
问题:
基础镜像可能偏大
没有固定版本
使用 shell 格式 CMD
没有非 root 用户
没有 JVM 参数入口
没有健康检查
没有镜像元数据
没有利用 Spring Boot 分层 jar
没有优雅停机考虑
2. 基础生产写法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FROM eclipse-temurin:17 -jreRUN groupadd -r app && useradd -r -g app app WORKDIR /app COPY --chown =app:app target/app.jar app.jar USER appEXPOSE 8080 ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0" ENTRYPOINT ["sh" , "-c" , "exec java $JAVA_OPTS -jar app.jar" ]
这里已经做了几件事:
使用 JRE 而不是完整 JDK
非 root 用户运行
支持 JVM 参数
使用 exec 转发信号
声明端口
3. Maven 多阶段构建版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 FROM maven:3.9 -eclipse-temurin-17 AS builderWORKDIR /build COPY pom.xml . RUN --mount=type =cache,target=/root/.m2 \ mvn -B dependency:go-offline COPY src ./src RUN --mount=type =cache,target=/root/.m2 \ mvn -B clean package -DskipTests FROM eclipse-temurin:17 -jreRUN groupadd -r app && useradd -r -g app app WORKDIR /app COPY --from=builder --chown =app:app /build/target/*.jar app.jar USER appEXPOSE 8080 ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC" ENTRYPOINT ["sh" , "-c" , "exec java $JAVA_OPTS -jar app.jar" ]
4. Spring Boot Layered Jar 优化 Spring Boot 支持 layered jar,可以把 jar 拆成不同层:
dependencies
spring-boot-loader
snapshot-dependencies
application
这样应用代码变化时,不必让依赖层全部失效。
Maven 插件配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <configuration > <layers > <enabled > true</enabled > </layers > </configuration > </plugin > </plugins > </build >
Dockerfile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 FROM maven:3.9 -eclipse-temurin-17 AS builderWORKDIR /build COPY pom.xml . RUN --mount=type =cache,target=/root/.m2 \ mvn -B dependency:go-offline COPY src ./src RUN --mount=type =cache,target=/root/.m2 \ mvn -B clean package -DskipTests RUN mkdir -p target/extracted \ && java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted FROM eclipse-temurin:17 -jreRUN groupadd -r app && useradd -r -g app app WORKDIR /app COPY --from=builder --chown =app:app /build/target/extracted/dependencies/ ./ COPY --from=builder --chown =app:app /build/target/extracted/spring-boot-loader/ ./ COPY --from=builder --chown =app:app /build/target/extracted/snapshot-dependencies/ ./ COPY --from=builder --chown =app:app /build/target/extracted/application/ ./ USER appEXPOSE 8080 ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC" ENTRYPOINT ["sh" , "-c" , "exec java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher" ]
优化点:
flowchart TB
A[dependencies 第三方依赖] --> B[变化频率低,可长期缓存]
C[snapshot-dependencies 快照依赖] --> D[变化频率中]
E[spring-boot-loader] --> F[变化频率低]
G[application 业务代码] --> H[变化频率高,放最后]
这样当你只改业务代码时,依赖层大概率可以复用。
5. Java 容器内存参数建议 不要简单写死:
如果容器运行时限制内存 512MB,JVM 堆也设置 512MB,就容易 OOM,因为还有:
metaspace
direct memory
thread stack
code cache
native memory
更容器友好的方式:
1 -XX:MaxRAMPercentage=75.0
例如:
1 ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
运行时再按环境覆盖:
1 2 3 docker run \ -e JAVA_OPTS="-XX:MaxRAMPercentage=70.0 -XX:+UseG1GC" \ demo-app:1.0.0
6. Spring Boot 优雅停机与 Dockerfile Dockerfile:
1 ENTRYPOINT ["sh" , "-c" , "exec java $JAVA_OPTS -jar app.jar" ]
Spring 配置:
1 2 3 4 5 6 server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s
运行时:
1 docker stop -t 35 demo-app
含义:
Docker 发送 SIGTERM
Java 进程收到信号
Spring Boot 执行优雅停机
35 秒后仍未退出才强杀
二十一、Node / 前端项目 Dockerfile 优化 1. 错误写法 1 2 3 4 5 6 7 8 9 10 11 FROM node:20 WORKDIR /app COPY . . RUN npm install RUN npm run build CMD ["npm" , "run" , "start" ]
问题:
COPY . . 导致缓存容易失效
npm install 不如 npm ci 可复现
最终镜像包含源码和 node_modules
前端静态资源不需要 Node 运行时
镜像大
2. 优化写法:构建后交给 Nginx 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 FROM node:20 -bookworm-slim AS builderWORKDIR /app COPY package.json package-lock.json ./ RUN --mount=type =cache,target=/root/.npm \ npm ci COPY . . RUN npm run build FROM nginx:stable-alpine AS runtimeCOPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx" , "-g" , "daemon off;" ]
3. 自定义 Nginx 配置 1 2 3 4 5 6 7 8 FROM nginx:stable-alpineCOPY nginx.conf /etc/nginx/conf.d/default.conf COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx" , "-g" , "daemon off;" ]
nginx.conf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server { listen 80 ; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri / /index.html; } location /assets/ { expires 7d ; add_header Cache-Control "public" ; } }
适合 Vue、React、Vite 等单页应用。
二十二、Go 项目 Dockerfile 优化 Go 项目非常适合多阶段构建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 FROM golang:1.22 AS builderWORKDIR /src COPY go.mod go.sum ./ RUN --mount=type =cache,target=/go/pkg/mod \ go mod download COPY . . RUN --mount=type =cache,target=/go/pkg/mod \ --mount=type =cache,target=/root/.cache/go-build \ CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/app FROM scratchCOPY --from=builder /out/app /app EXPOSE 8080 ENTRYPOINT ["/app" ]
如果需要 CA 证书:
1 2 3 4 5 6 7 FROM alpine:3.20 AS certsRUN apk --no-cache add ca-certificates FROM scratchCOPY --from=builder /out/app /app COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ ENTRYPOINT ["/app" ]
二十三、Python 项目 Dockerfile 优化 Python 镜像优化重点:
固定 Python 版本
使用 slim 镜像
单独复制 requirements
使用 venv 或安装到系统路径
避免 pip 缓存进入镜像
多阶段处理编译依赖
基础写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 FROM python:3.12 -slimWORKDIR /app ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["python" , "app.py" ]
带构建依赖的多阶段写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 FROM python:3.12 -slim AS builderWORKDIR /app RUN apt-get update \ && apt-get install -y --no-install-recommends build-essential gcc \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN --mount=type =cache,target=/root/.cache/pip \ pip wheel --wheel-dir /wheels -r requirements.txt FROM python:3.12 -slimWORKDIR /app ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 COPY --from=builder /wheels /wheels COPY requirements.txt . RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt \ && rm -rf /wheels COPY . . EXPOSE 8000 CMD ["python" , "app.py" ]
二十四、镜像体积优化:不是越小越好,而是刚好够用 1. 查看镜像大小
查看层信息:
1 docker history demo-app:1.0.0
更好用的工具:
dive 可以分析每层文件变化和浪费空间。
2. 常见瘦身手段
手段
效果
使用更小基础镜像
减少初始体积
多阶段构建
去掉构建工具和源码
清理包管理器缓存
减少无用文件
精准 COPY
避免复制无关文件
.dockerignore
减少上下文和泄露
JRE 替代 JDK
Java 运行镜像更小
distroless
进一步减少系统组件
删除测试文件和文档
减少产物体积
3. 瘦身反模式 反模式一:为了小盲目 Alpine 小不等于稳定。兼容性问题排查起来很贵。
反模式二:运行镜像里保留构建工具 例如最终镜像里还有:
Maven
npm
gcc
make
git
源码目录
这基本说明没做好多阶段构建。
反模式三:先复制再删除 1 2 COPY . . RUN rm -rf test docs .git
如果 .git 已经在上一层复制进镜像,后面删掉不一定能减少历史层体积。应该用 .dockerignore 从源头排除。
二十五、Dockerfile 安全优化 1. 不要把密钥写进镜像 错误:
1 2 3 ENV DB_PASSWORD=123456 ARG GITHUB_TOKEN=xxxCOPY id_rsa /root/.ssh/id_rsa
风险:
docker history 可能看到
镜像被推送后泄露
CI 日志可能泄露
镜像扫描可能记录
正确:
构建期使用 BuildKit secret
运行期使用环境变量或密钥管理
私钥不要进入镜像层
2. 非 root 用户 1 2 RUN groupadd -r app && useradd -r -g app app USER app
3. 最小化安装包 1 2 3 RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl \ && rm -rf /var/lib/apt/lists/*
4. 使用可信基础镜像 优先:
Docker Official Images
Verified Publisher 镜像
企业内部维护的基础镜像
定期扫描和更新的镜像
5. 镜像扫描 常见工具:
1 2 docker scout quickview demo-app:1.0.0 docker scout cves demo-app:1.0.0
也可以使用:
1 trivy image demo-app:1.0.0
6. 生成 SBOM 1 docker sbom demo-app:1.0.0
或使用 Syft:
7. 减少运行时能力 有些安全项不在 Dockerfile 里,而是在运行时配置:
1 2 3 4 5 6 docker run \ --read-only \ --cap-drop=ALL \ --security-opt=no-new-privileges \ --tmpfs /tmp \ demo-app:1.0.0
Dockerfile 可以配合:
但运行时安全策略仍要在部署层配置。
二十六、可复现构建优化 可复现构建的目标:
同一份源码、同一份依赖、同一份 Dockerfile,在不同机器构建出尽可能一致的镜像。
1. 固定基础镜像版本 不推荐:
推荐:
1 FROM node:20 -bookworm-slim
更严格:
1 FROM node:20 -bookworm-slim@sha256:...
2. 使用 lock 文件 Node:
1 2 COPY package.json package-lock.json ./ RUN npm ci
不要:
Java:
固定 Maven 插件版本
固定依赖版本
使用内部 Maven 仓库代理
避免动态版本,例如 1.0.+
Python:
1 2 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt
更严格可以使用 hash pin。
3. 构建时写入版本信息 1 2 3 4 5 6 7 ARG APP_VERSIONARG GIT_COMMITARG BUILD_TIMELABEL org.opencontainers.image.version="${APP_VERSION} " \ org.opencontainers.image.revision="${GIT_COMMIT} " \ org.opencontainers.image.created="${BUILD_TIME} "
构建:
1 2 3 4 5 docker build \ --build-arg APP_VERSION=1.0.0 \ --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \ --build-arg BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ" ) \ -t demo-app:1.0.0 .
二十七、Dockerfile 可维护性优化 好的 Dockerfile 应该让别人一眼看懂:
用什么基础镜像
哪个阶段负责构建
哪个阶段负责测试
哪个阶段负责运行
复制了什么文件
启动命令是什么
端口是什么
是否非 root
如何传参数
1. 推荐结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ARG JAVA_VERSION=17 FROM maven:3.9 -eclipse-temurin-${JAVA_VERSION} AS depsWORKDIR /build COPY pom.xml . RUN --mount=type =cache,target=/root/.m2 \ mvn -B dependency:go-offline FROM deps AS builderCOPY src ./src RUN --mount=type =cache,target=/root/.m2 \ mvn -B clean package -DskipTests FROM eclipse-temurin:${JAVA_VERSION}-jre AS runtimeWORKDIR /app COPY --from=builder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
2. 不要过度炫技 不要为了“高级”而把 Dockerfile 写成谜语。
比如过度使用:
复杂 shell
动态下载
大量 ARG 拼接
多层嵌套脚本
构建时访问不稳定外部资源
Dockerfile 是基础设施代码,稳定和可读优先。
二十八、生产级 Spring Boot Dockerfile 完整模板 下面给一个较完整的 Spring Boot 模板。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 ARG JAVA_VERSION=17 ARG APP_NAME=demo-appFROM maven:3.9 -eclipse-temurin-${JAVA_VERSION} AS depsWORKDIR /build COPY pom.xml . RUN --mount=type =cache,target=/root/.m2 \ mvn -B dependency:go-offline FROM deps AS builderWORKDIR /build COPY src ./src RUN --mount=type =cache,target=/root/.m2 \ mvn -B clean package -DskipTests RUN mkdir -p target/extracted \ && java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted FROM eclipse-temurin:${JAVA_VERSION}-jre AS runtimeARG APP_NAMEARG APP_VERSION=unknownARG GIT_COMMIT=unknownARG BUILD_TIME=unknownLABEL org.opencontainers.image.title="${APP_NAME} " \ org.opencontainers.image.version="${APP_VERSION} " \ org.opencontainers.image.revision="${GIT_COMMIT} " \ org.opencontainers.image.created="${BUILD_TIME} " RUN groupadd -r app && useradd -r -g app app WORKDIR /app COPY --from=builder --chown =app:app /build/target/extracted/dependencies/ ./ COPY --from=builder --chown =app:app /build/target/extracted/spring-boot-loader/ ./ COPY --from=builder --chown =app:app /build/target/extracted/snapshot-dependencies/ ./ COPY --from=builder --chown =app:app /build/target/extracted/application/ ./ USER appEXPOSE 8080 ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC" \ TZ="Asia/Shanghai" ENTRYPOINT ["sh" , "-c" , "exec java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher" ]
构建脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/usr/bin/env bash set -e APP_NAME="demo-app" APP_VERSION="${1:-1.0.0} " GIT_COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo unknown) " BUILD_TIME="$(date -u +"%Y-%m-%dT%H:%M:%SZ" ) " docker buildx build \ --build-arg APP_NAME="${APP_NAME} " \ --build-arg APP_VERSION="${APP_VERSION} " \ --build-arg GIT_COMMIT="${GIT_COMMIT} " \ --build-arg BUILD_TIME="${BUILD_TIME} " \ -t "${APP_NAME} :${APP_VERSION} " \ .
运行:
1 2 3 4 5 docker run -d \ --name demo-app \ -p 8080:8080 \ -e JAVA_OPTS="-XX:MaxRAMPercentage=70.0 -XX:+UseG1GC" \ demo-app:1.0.0
二十九、常见反模式总结 反模式一:COPY . . 一把梭
如果没有 .dockerignore,这是事故源头。
反模式二:生产使用 latest
构建不可控。
反模式三:把密码写进镜像
这是把密码做成“预制菜”,谁拉镜像谁吃到。
反模式四:构建和运行不分离 1 2 3 4 FROM maven:3.9 COPY . . RUN mvn package CMD ["java" , "-jar" , "target/app.jar" ]
最终镜像里带 Maven 和源码。
反模式五:容器里用 root 跑服务 1 2 3 FROM ubuntuCOPY app /app CMD ["/app" ]
没有 USER。
反模式六:shell 格式 ENTRYPOINT 1 ENTRYPOINT java -jar app.jar
信号处理可能不理想。
反模式七:安装包不清理缓存 1 RUN apt-get update && apt-get install -y curl
会留下包索引缓存。
反模式八:镜像里装太多调试工具 1 RUN apt-get install -y vim net-tools telnet curl wget git procps
生产镜像不是瑞士军刀,越多越危险。
反模式九:使用 VOLUME 制造匿名卷
业务镜像里随手写可能导致数据位置难追踪。
三十、Dockerfile 优化检查清单 1. 构建速度
2. 镜像体积
3. 安全
4. 可维护性
5. 运行可靠性
三十一、排查 Dockerfile 构建问题的命令 1. 构建时显示详细日志 1 docker build --progress=plain -t demo-app:1.0.0 .
2. 不使用缓存构建 1 docker build --no-cache -t demo-app:1.0.0 .
3. 只构建某个阶段 1 docker build --target builder -t demo-app:builder .
4. 进入中间阶段调试 1 docker run -it --rm demo-app:builder sh
5. 查看镜像层 1 docker history demo-app:1.0.0
6. 查看镜像配置 1 docker inspect demo-app:1.0.0
7. 查看最终容器文件 1 docker run --rm -it demo-app:1.0.0 sh
如果运行镜像没有 shell,比如 distroless,就不能这样调试。这也是 distroless 的权衡。
三十二、完整示例:从差 Dockerfile 到好 Dockerfile 1. 初始版本 1 2 3 4 5 6 7 8 9 10 11 FROM maven:3.9 -eclipse-temurin-17 WORKDIR /app COPY . . RUN mvn clean package -DskipTests EXPOSE 8080 CMD java -jar target/demo.jar
问题:
运行镜像带 Maven
运行镜像带源码
COPY . . 导致缓存差
shell 格式 CMD
root 用户
没有 JVM 参数入口
没有构建缓存优化
没有元数据
没有 .dockerignore
2. 中级版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 FROM maven:3.9 -eclipse-temurin-17 AS builderWORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM eclipse-temurin:17 -jreWORKDIR /app COPY --from=builder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
已经解决:
多阶段构建
依赖缓存更好
运行镜像不带 Maven
ENTRYPOINT exec 格式
但还可以继续优化:
非 root
BuildKit cache mount
Spring Boot layered jar
JVM 参数
LABEL
优雅停机
3. 生产优化版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 ARG JAVA_VERSION=17 FROM maven:3.9 -eclipse-temurin-${JAVA_VERSION} AS depsWORKDIR /build COPY pom.xml . RUN --mount=type =cache,target=/root/.m2 \ mvn -B dependency:go-offline FROM deps AS builderWORKDIR /build COPY src ./src RUN --mount=type =cache,target=/root/.m2 \ mvn -B clean package -DskipTests RUN mkdir -p target/extracted \ && java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted FROM eclipse-temurin:${JAVA_VERSION}-jre AS runtimeARG APP_VERSION=unknownARG GIT_COMMIT=unknownARG BUILD_TIME=unknownLABEL org.opencontainers.image.title="demo-app" \ org.opencontainers.image.version="${APP_VERSION} " \ org.opencontainers.image.revision="${GIT_COMMIT} " \ org.opencontainers.image.created="${BUILD_TIME} " RUN groupadd -r app && useradd -r -g app app WORKDIR /app COPY --from=builder --chown =app:app /build/target/extracted/dependencies/ ./ COPY --from=builder --chown =app:app /build/target/extracted/spring-boot-loader/ ./ COPY --from=builder --chown =app:app /build/target/extracted/snapshot-dependencies/ ./ COPY --from=builder --chown =app:app /build/target/extracted/application/ ./ USER appEXPOSE 8080 ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC" ENTRYPOINT ["sh" , "-c" , "exec java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher" ]
三十三、Dockerfile 优化的本质 Dockerfile 优化不是简单地“少几行”或“镜像小一点”。
它本质上是在平衡六件事:
mindmap
root((Dockerfile 优化))
构建速度
缓存命中
BuildKit
.dockerignore
remote cache
镜像体积
多阶段构建
精简基础镜像
清理缓存
只保留运行产物
安全性
非 root
不写密钥
漏洞扫描
最小权限
可复现
固定版本
lock 文件
构建参数
元数据
可维护
阶段命名
清晰结构
少炫技
可调试
运行可靠
exec ENTRYPOINT
优雅停机
健康检查
容器内存参数
工程上真正好的 Dockerfile,通常有这些特征:
构建阶段和运行阶段分离。
构建缓存命中率高。
镜像只包含运行所需文件。
使用非 root 用户运行。
不包含密码、私钥、Token。
基础镜像版本明确。
支持版本信息追踪。
支持 CI/CD 快速构建。
支持容器优雅停止。
出问题时能够快速定位。
总结 Dockerfile 是容器化工程的核心之一。
刚开始学 Dockerfile,可以先记住指令:
FROM
WORKDIR
COPY
RUN
EXPOSE
CMD
ENTRYPOINT
但真正写好 Dockerfile,要进一步理解:
构建上下文
镜像分层
构建缓存
多阶段构建
BuildKit
.dockerignore
非 root 用户
密钥管理
基础镜像选择
Java/Node/Python 等语言的构建特性
一份差的 Dockerfile 也许能跑,但会慢、胖、不安全、不可复现,还会在 CI/CD 和生产环境里给你上强度。
一份好的 Dockerfile 应该是:
构建快、镜像小、安全、稳定、可复现、可维护。
最后给一个最实用的判断标准:
如果你修改一行业务代码,Docker 却重新下载全部依赖,那 Dockerfile 还有优化空间。 如果你最终镜像里还有 Maven、npm、gcc、源码和私钥,那已经不是优化空间了,那是事故现场。
参考资料