Appearance
Dockerfile 怎么写更稳:基础指令、分层思路和多阶段构建
会写一个能跑的 Dockerfile 不难,难的是把它写得更小、更稳,也更适合团队长期维护。
如果你后面准备把 Spring Boot、网关、任务服务逐步挂到现在这台腾讯云服务器上,多阶段构建几乎是最值得先养成的习惯。
先说结论
Dockerfile 最容易踩坑的地方通常不是语法,而是构建思路:
- 基础镜像选得过重
- 构建工具和运行环境混在一起
- 不必要文件全进了镜像
- 层太多,构建缓存也用不好
所以更推荐的思路是:
- 构建阶段负责编译和打包
- 运行阶段只保留真正需要的产物
- 依赖下载尽量吃到缓存
- 最终镜像尽量只保留运行时
一、先理解几个基础指令
FROM
指定基础镜像:
dockerfile
FROM eclipse-temurin:21-jdkWORKDIR
设置工作目录:
dockerfile
WORKDIR /appCOPY
把文件复制进镜像:
dockerfile
COPY target/app.jar /app/app.jarRUN
在镜像构建阶段执行命令:
dockerfile
RUN apt-get updateENV
定义环境变量:
dockerfile
ENV TZ=Asia/ShanghaiENTRYPOINT
定义容器启动入口:
dockerfile
ENTRYPOINT ["java", "-jar", "/app/app.jar"]二、为什么分层思路很重要
Docker 镜像不是一个整体文件,而是一层一层叠起来的。
这意味着:
- 指令顺序会影响缓存命中
- 经常变化的内容应尽量放后面
- 大量无关文件进入上下文会拖慢构建
例如 Java 项目如果每次都把整个工程一股脑 COPY . .,那么哪怕只是改了一行业务代码,前面依赖下载那层也可能重新失效。
三、一个单阶段 Dockerfile 的基本写法
dockerfile
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/demo.jar /app/demo.jar
ENTRYPOINT ["java", "-jar", "/app/demo.jar"]这能跑,但有个前提:
- Jar 已经在宿主机提前打好了
如果你希望镜像构建过程自己完成编译,或者想把 CI、测试环境、生产环境构建链路统一起来,就更适合用多阶段构建。
四、多阶段构建为什么更适合 Java
典型写法:
dockerfile
FROM maven:3.9.9-eclipse-temurin-21 AS builder
WORKDIR /workspace
COPY pom.xml .
RUN mvn -B -q -DskipTests dependency:go-offline
COPY src ./src
RUN mvn -B -DskipTests clean package
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /workspace/target/demo.jar /app/demo.jar
ENTRYPOINT ["java", "-jar", "/app/demo.jar"]这个写法的好处很明显:
- Maven 和编译缓存只存在于构建阶段
- 最终运行镜像更小
- 运行环境更纯净
- 安全面和维护成本都更好控制
这里多加一步 dependency:go-offline 的意义是:
pom.xml不变时,依赖层更容易命中缓存- 业务代码变化不会让依赖下载反复执行
- CI 构建时间通常会更稳定
五、放到真实部署链路里,Dockerfile 负责的边界是什么
这点特别容易混。
对你现在这类服务器环境,更推荐把职责拆清楚:
- Dockerfile 负责“镜像怎么构建”
- Compose 或
docker run负责“容器怎么运行” - Nginx 负责“入口、域名、HTTPS、静态资源或反向代理”
- 宿主机目录负责“配置、日志、挂载数据、备份”
也就是说,不要在 Dockerfile 里试图把所有运行期问题都解决掉,比如:
- 写死数据库地址
- 写死环境端口
- 把生产配置直接打进镜像
- 把日志目录写成容器内临时路径却不挂载
Dockerfile 越像“可复用构建说明书”,后面越容易长期维护。
六、Java 项目写 Dockerfile 的几个实用建议
1. 尽量使用运行时镜像
如果只是启动 Jar,运行阶段优先考虑 jre 或更轻量的运行时镜像,而不是完整 jdk。
2. 配合 .dockerignore
把这些目录排除掉通常很有必要:
.gitnode_moduleslogs- 本地临时文件
否则构建上下文会变大,速度也会受影响。
3. 不要把配置硬编码死
例如:
- 端口
- Profile
- 外部服务地址
更适合通过环境变量或挂载配置文件传入。
4. 明确区分构建和运行责任
Dockerfile 负责镜像构建,真正的端口、挂载目录、网络和重启策略,通常应该放到运行层,比如 Compose。
5. 应用最好不要默认以 root 运行
如果镜像要长期在线上跑,更稳的做法是:
- 创建专用用户
- 只给应用目录必要权限
例如:
dockerfile
RUN useradd -r -s /sbin/nologin appuser
USER appuser对于纯个人项目这不是强制项,但如果后面会继续接更多服务,这一步很值得尽早养成。
6. 用固定版本,不要长期依赖模糊标签
像下面这种写法虽然方便:
dockerfile
FROM maven:latest但它会让构建结果变得不稳定。更推荐固定大版本甚至小版本,至少要让团队知道“这次构建到底基于什么环境”。
七、一个更贴近 Spring Boot 的多阶段示例
dockerfile
FROM maven:3.9.9-eclipse-temurin-21 AS builder
WORKDIR /workspace
COPY pom.xml .
RUN mvn -B -q -DskipTests dependency:go-offline
COPY src ./src
RUN mvn -B -DskipTests clean package
FROM eclipse-temurin:21-jre
WORKDIR /app
RUN useradd -r -s /sbin/nologin appuser
COPY --from=builder /workspace/target/*.jar /app/app.jar
RUN chown -R appuser:appuser /app
USER appuser
ENV TZ=Asia/Shanghai
ENTRYPOINT ["java", "-jar", "/app/app.jar"]如果后面你的知识库旁边再挂一个 Java API 服务,这类 Dockerfile 就已经很适合作为起点了。
八、常见误区
1. 把整个 JDK、源码、Maven 仓库都带进最终镜像
这会让镜像明显变大,也没必要。
2. 为了方便直接 COPY . .
短期方便,长期很容易把无关文件也带进去。
3. 把“构建成功”和“运行稳定”混为一谈
镜像能 build 通过,不代表启动时目录、端口、配置和依赖一定都正常。
4. 以为 Dockerfile 越“全能”越好
真正稳定的部署链路,往往是每层只处理自己的职责,而不是把构建、配置、证书、数据都硬塞进一个 Dockerfile 里。
一句话总结
Dockerfile 的目标不只是“打出一个镜像”,而是把构建过程写成一份可维护、可复用、可复现的产物说明。
对 Java 项目来说,多阶段构建几乎是最值得优先掌握的一步,因为它能同时改善镜像体积、清晰度和部署稳定性。