速读#
这篇把 Java、Docker、Nginx 串成一条上线链路,重点不是部署清单,而是补上那次“能跑不懂”的理解债。多阶段构建、Docker 层缓存、端口只绑回环,是这篇最该留下的三个动作。
多阶段构建:我借过、后来还过的债#
直接把 JDK 和源码塞进镜像会很臃肿。多阶段:第一阶段编译,第二阶段只拷贝产物。
FROM maven:3.9-eclipse-temurin-21 AS buildWORKDIR /appCOPY pom.xml .RUN mvn dependency:go-offlineCOPY src ./srcRUN mvn clean package -DskipTests
FROM eclipse-temurin:21-jreWORKDIR /appCOPY --from=build /app/target/*.jar app.jarENTRYPOINT ["java", "-jar", "app.jar"]拆开看收益很实在:第一阶段带着 Maven 和 JDK,第二阶段只剩 JRE + 一个 jar。构建工具链不进最终镜像,体积从 GB 级掉到几百 MB,攻击面也跟着小——镜像里没有的东西,不用担心被利用。
顺带交代 -DskipTests:它跳过的只是「构建镜像」这一步里的测试,前提是测试在别处跑过——CI 里有独立的测试环节,这行才写得心安;否则等于把「能构建出镜像」当成了「能上线」。
这段 Dockerfile 当初是 AI 生成的,能跑,我直接用了。两天后想加缓存优化,发现自己完全不理解 COPY --from=build,也不知道为什么 pom.xml 单独先拷能加速依赖缓存。
回头从 Docker 层缓存重学,花的时间比一开始自己写还多。这笔「能跑不懂」的债,后来成了我用 AI 生成代码时反复对照的原发场景。
省下的不是时间,是把时间从「现在写」挪到「以后重学」,还附赠「以为会其实不会」的错觉。之后对 AI 生成的构建 / 配置文件,一律「逐行能解释」才算完成。
pom.xml 先拷#
Docker 每条指令一层,层有缓存。
| 顺序 | 指令 | 为何这样排 |
|---|---|---|
| 先 | COPY pom.xml + 下依赖 | pom.xml 少变,缓存易命中 |
| 后 | COPY src + 打包 | 源码天天变,只让源码层失效 |
变化频率低的放前面,高的放后面,缓存命中率才高。 这是层缓存的取舍,不是背下来的咒语。缓存也不是永不失效——真改了 pom.xml,依赖层照样重新下载,这是应该的;要防的只是天天变的源码,连坐不常变的依赖层。
端口只绑回环#
services: api: ports: - "127.0.0.1:8080:8080" # 只绑本地,由 Nginx 对外| 写法 | 效果 |
|---|---|
8080:8080 | 默认 0.0.0.0,可能直接公网暴露 |
127.0.0.1:8080:8080 | 仅本机,外网必须走反代 |
这和 Cloudflare Tunnel「零入站」是同一防护思路的不同粒度:Tunnel 是连缝都不开;绑回环是开了缝但只开给本机。共同点是默认不对公网暴露,暴露必须是显式决定。
多说一句为什么不靠防火墙兜底:Docker 发布端口时直接写 iptables 规则,ufw 的拒绝策略未必拦得住 0.0.0.0 的发布。与其赌防火墙和 Docker 谁的规则先命中,不如让端口从一开始就只存在于回环上。
docker compose up -d --builddocker compose logs -f api带走两个判断:
- 多阶段构建的层缓存搞懂了,才不欠债
- 端口绑回环,是防护面的最小动作
