ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Day53-Docker:Dockerfile最佳实践 + 多阶段构建 + docker-compose编排

Day53-Docker:Dockerfile最佳实践 + 多阶段构建 + docker-compose编排 为什么你的 Spring Boot 镜像又大、又慢、还跑不起来把 Spring Boot 应用部署上线的老大难往往不是代码 bug而是在我机器上能跑到你环境起不来——JDK 版本不一致、内网依赖拉不到、glibc 差异让 native 库直接崩。Docker 解决的就是环境一致性把操作系统、JDK、依赖、配置、运行时打包成一个不可变制品交付的是确定能跑的镜像而非一堆源码。本篇不堆概念直接上生产级实操带你解决三个核心问题Dockerfile 怎么写才不踩安全与体积坑、多阶段构建怎么把镜像从 700MB 压到 180MB、docker-compose 怎么一条命令拉起 Spring Boot MySQL Redis RabbitMQ 开发环境。文中代码均可直接跑踩过的坑都已标注。一、先看一个反面教材的Dockerfile很多团队第一次写Spring Boot的Dockerfile长这样# ❌ 反面教材能跑但全是坑 FROM openjdk:17 COPY target/app.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar]能跑吗能。能上生产吗不能。问题一堆问题后果openjdk:17基础镜像 ~640MB镜像体积虚胖拉取/推送慢浪费存储用root用户运行容器逃逸风险安全审计不过关没有时区/语言设置日志时间错8小时中文乱码jar包直接COPY每次代码改动整个jar层缓存失效构建巨慢没有健康检查K8s/Docker不知道应用是否真就绪没有JVM参数OOM了才知道默认堆太小或太大下面我们一步步把它改对。二、生产级Dockerfile逐行解读1. 选择合适的基础镜像openjdk:17这种全家桶镜像是历史包袱。现在官方推荐用 Eclipse Temurin Adoptium的 JRE 镜像体积小一半# ✅ 基础镜像Eclipse Temurin 17 JRE基于Ubuntu约260MB FROM eclipse-temurin:17-jre ​ # 如果要极致瘦身用 alpine 版本约180MB但注意musl libc兼容性 # FROM eclipse-temurin:17-jre-alpine版本说明JDK 17 是 LTSSpring Boot 3.x 最低要求就是 JDK 17。如果你还在用 JDK 8那是另一个故事了——建议尽快升级Spring Boot 3.2 的虚拟线程、GraalVM AOT 都是17才能享受的红利。2. 非root用户运行# ✅ 创建非root用户容器内最小权限原则 RUN groupadd -r appuser useradd -r -g appuser appuser USER appuser这步看着不起眼但安全扫描工具Trivy、Snyk看到USER root就会给你一个高危告警。生产环境的安全合规这行不能省。3. 时区与语言环境# ✅ 设置时区为东八区解决日志时间偏移问题 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone ​ # ✅ 语言环境防止中文日志/文件名乱码 ENV LANGC.UTF-84. 分层COPY利用构建缓存这是很多人忽略的优化。jar包里BOOT-INF/lib下的第三方依赖很少变而BOOT-INF/classes下的业务代码天天变。如果整个jar一起COPY每次改一行代码整个jar层都缓存失效。Spring Boot从2.3开始支持分层jar我们利用这一点!-- pom.xml开启分层jarSpring Boot 2.33.x默认开启 -- plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration layers enabledtrue/enabled /layers /configuration /plugin然后Dockerfile这样写# ✅ 分层提取 分层COPY最大化缓存命中率 WORKDIR /app COPY --frombuild target/app.jar app.jar RUN java -Djarmodelayertools -jar app.jar extract ​ # 先COPY依赖层几乎不变缓存命中率99% COPY --fromextract dependencies/ ./ COPY --fromextract spring-boot-loader/ ./ COPY --fromextract snapshot-dependencies/ ./ # 最后COPY业务代码层频繁变化 COPY --fromextract application/ ./实测改一行业务代码Docker构建从重新传整个70MB的jar变成只传2MB的classes层构建时间从40秒降到8秒。5. 健康检查 JVM参数# ✅ 健康检查Docker原生HEALTHCHECKK8s有自己的探针这里二选一 HEALTHCHECK --interval30s --timeout3s --retries3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 ​ # ✅ JVM参数通过环境变量传入不硬编码 ENV JAVA_OPTS-XX:MaxRAMPercentage75.0 -XX:UseG1GC -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/app/heapdump.hprof ​ ENTRYPOINT [sh, -c, java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher]关键点-XX:MaxRAMPercentage75.0而不是-Xmx2g。容器里内存是动态的用百分比让JVM自动适配容器内存上限比写死数字灵活得多。这是JDK 10容器感知特性。完整的生产级Dockerfile把上面的拼起来再加上多阶段构建下一节细讲完整版如下# 第一阶段构建 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build # 先COPY pom.xml利用缓存下载依赖 COPY pom.xml . RUN mvn dependency:go-offline -B # 再COPY源码 COPY src ./src RUN mvn clean package -DskipTests -B ​ # 第二阶段运行 FROM eclipse-temurin:17-jre ​ # 非root用户 RUN groupadd -r appuser useradd -r -g appuser appuser ​ # 时区 语言 ENV TZAsia/Shanghai ENV LANGC.UTF-8 RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone ​ WORKDIR /app ​ # 从构建阶段COPY jar COPY --frombuilder /build/target/*.jar app.jar ​ # 分层提取 RUN java -Djarmodelayertools -jar app.jar extract \ rm app.jar ​ # 分层COPY COPY --fromextract dependencies/ ./ COPY --fromextract spring-boot-loader/ ./ COPY --fromextract application/ ./ ​ USER appuser ​ EXPOSE 8080 ​ HEALTHCHECK --interval30s --timeout3s --start-period40s --retries3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 ​ ENV JAVA_OPTS-XX:MaxRAMPercentage75.0 -XX:UseG1GC -XX:HeapDumpOnOutOfMemoryError ​ ENTRYPOINT [sh, -c, java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher]三、多阶段构建镜像从700MB压到180MB多阶段构建Multi-stage build是Docker 17.05的核心特性。核心思想构建环境和生产环境分离。为什么需要它看这个对比Maven、JDK编译器、源代码——这些东西在运行时根本用不到凭什么让它们占着镜像体积多阶段构建就是让builder阶段干完活后只把产物jarCOPY到runtime阶段builder阶段整个丢弃。再进一步用eclipse-temurin:17-jre-alpine基于Alpine Linux用musl libc替代glibc能压到180MB左右FROM eclipse-temurin:17-jre-alpine # Alpine 注意事项 # 1. musl libc 可能在某些native库上有兼容问题如Netty的native epoll # 2. 需要安装 curl 用于健康检查 RUN apk add --no-cache curl tzdata ENV TZAsia/Shanghai COPY --frombuilder /build/target/*.jar app.jar体积对比实测方案镜像体积拉取时间(首次)适用场景openjdk:17 单阶段712MB28s仅本地调试temurin:17-jre 多阶段268MB11s生产推荐temurin:17-jre-alpine 多阶段182MB7s极致瘦身注意兼容性GraalVM Native Image86MB3sServerless/冷启动敏感提醒Alpine虽好但musl libc的坑不少。Netty、SQLite-JDBC、某些JNI库在Alpine上可能出问题。如果你不确定依赖里有没有native组件先用标准jre镜像别上来就alpine。出了问题排查musl兼容性比省下的那80MB代价大得多。四、docker-compose编排开发环境生产环境用K8s但开发环境用K8s太重了。docker-compose是开发环境的最佳选择——一条命令拉起Spring Boot MySQL Redis RabbitMQ新人clone代码5分钟就能跑起来。# docker-compose.yml一键编排开发环境 version: 3.9 ​ services: # 应用服务 app: build: context: . dockerfile: Dockerfile container_name: myapp ports: - 8080:8080 environment: - SPRING_PROFILES_ACTIVEdev - SPRING_DATASOURCE_URLjdbc:mysql://mysql:3306/myapp?useSSLfalseserverTimezoneAsia/Shanghai - SPRING_DATASOURCE_USERNAMEroot - SPRING_DATASOURCE_PASSWORDroot123 - SPRING_REDIS_HOSTredis - SPRING_REDIS_PORT6379 - SPRING_RABBITMQ_HOSTrabbitmq - JAVA_OPTS-XX:MaxRAMPercentage75.0 -Xms256m -Xmx512m depends_on: mysql: condition: service_healthy redis: condition: service_started rabbitmq: condition: service_healthy restart: unless-stopped networks: - app-network ​ # MySQL mysql: image: mysql:8.0 container_name: myapp-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: myapp TZ: Asia/Shanghai ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql - ./sql/init:/docker-entrypoint-initdb.d # 自动执行初始化SQL command: --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -uroot, -proot123] interval: 10s timeout: 5s retries: 5 networks: - app-network ​ # Redis redis: image: redis:7-alpine container_name: myapp-redis ports: - 6379:6379 volumes: - redis-data:/data command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru networks: - app-network ​ # RabbitMQ含管理界面 rabbitmq: image: rabbitmq:3.13-management container_name: myapp-rabbitmq environment: RABBITMQ_DEFAULT_USER: admin RABBITMQ_DEFAULT_PASS: admin123 ports: - 5672:5672 # AMQP端口 - 15672:15672 # 管理界面 volumes: - rabbitmq-data:/var/lib/rabbitmq healthcheck: test: [CMD, rabbitmq-diagnostics, ping] interval: 15s timeout: 10s retries: 5 networks: - app-network ​ # 数据卷 volumes: mysql-data: redis-data: rabbitmq-data: ​ # 网络 networks: app-network: driver: bridge几个关键设计点depends_oncondition: service_healthy确保MySQL和RabbitMQ健康检查通过后app才启动。否则app先起来连不上数据库直接崩溃重启依赖顺序问题在compose里非常常见。docker-entrypoint-initdb.dMySQL官方镜像的约定容器首次启动时自动执行这个目录下的.sql/.sh文件。把建表语句放进去新人拉起环境就有表结构。数据卷持久化mysql-data、redis-data这些volume保证容器删除重建后数据不丢。开发环境不怕丢数据但每次重建环境重新造测试数据也烦人。网络隔离app-networkbridge网络让四个容器在同一个网络里用服务名互相访问mysql:3306、redis:6379不用暴露所有端口到宿主机。常用命令# 启动全部服务后台运行 docker-compose up -d ​ # 只启动基础设施app用本地IDE跑开发常用 docker-compose up -d mysql redis rabbitmq ​ # 查看日志 docker-compose logs -f app ​ # 重建app镜像改了代码后 docker-compose up -d --build app ​ # 销毁全部保留数据卷 docker-compose down ​ # 销毁并删除数据慎用开发环境重置用 docker-compose down -v五、.dockerignore90%的人漏掉的配置跟.gitignore一个道理.dockerignore决定哪些文件不进构建上下文。没有这个文件docker build会把整个项目目录发给Docker daemon包括target/、.git/、node_modules/构建上下文动辄几百MB。# .dockerignore # 构建产物 target/ build/ out/ ​ # 版本控制 .git/ .gitignore ​ # IDE .idea/ *.iml .vscode/ ​ # 日志 *.log logs/ ​ # 文档 *.md docs/ ​ # 测试 test-results/ coverage/ ​ # 本地配置敏感信息不入镜像 application-local.yml application-prod.yml ​ # Docker自身文件 Dockerfile docker-compose.yml .dockerignore实测一个中型Spring Boot项目不加.dockerignore构建上下文约280MB加上后约45MB。构建速度提升明显尤其是CI/CD环境。六、建议1. 镜像版本必须锁定禁止用latestFROM eclipse-temurin:17-jre是可以的隐式锁定到17的patch版本但FROM eclipse-temurin:latest是灾难——某天Temurin推了JDK 21的latest标签你的镜像突然构建出JDK 21的环境Spring Boot 3.x在JDK 21上虚拟线程行为变化可能导致一堆隐性bug。生产镜像必须用完整版本号eclipse-temurin:17.0.12_7-jre或者至少锁定大版本。2. 开发环境用compose生产环境别用docker-compose适合本地开发和单机部署小项目、内部工具。一旦你的服务超过3个、需要弹性伸缩、需要滚动更新——直接上K8s。compose的scale参数是个半成品网络模型也不适合多节点。我见过有人拿docker-compose Swarm上生产一个节点挂了服务全灭别走这条路。3. 把JVM参数做成环境变量配合K8s ConfigMap动态调整Dockerfile里JAVA_OPTS用环境变量传入不要硬编码。这样在K8s里可以通过ConfigMap/环境变量动态调整GC参数、堆大小不用重新构建镜像。线上GC调优、OOM排查时改个ConfigMap重启Pod就行而不是改Dockerfile→构建→推镜像→滚动更新这个链路太长了。容器化的本质不是把jar塞进Docker而是把环境、依赖、配置、运行时打包成一个不可变制品。你交付的不再是代码而是一个确定能跑的制品。这个思维转变比学会写Dockerfile重要一百倍。下篇预告Day 54《企业级CI/CD流水线设计Jenkins GitLab Docker》——代码push到main分支自动构建→测试→SonarQube质量门禁→Docker镜像推送→部署到测试环境全流程Pipeline拆解我们明天见。
返回列表