一、一个"反面教材"
很多团队的第一个 Dockerfile 长这样:
``dockerfile
FROM openjdk:17
COPY target/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
`
这个镜像打包后约 500MB,每次构建都需要重新传输,部署缓慢。接下来我们一步步优化。
二、分层构建 —— 把依赖和业务代码分开
Spring Boot 的 fat jar 由三部分组成:
- 内嵌容器(Tomcat / Undertow)—— 极少变化
- 三方依赖(Spring、Jackson 等)—— 很少变化
- 业务代码 —— 频繁变化
利用这个特性做分层:
`xml
`
`dockerfile
Dockerfile —— 多阶段构建 + 分层
FROM eclipse-temurin:21-jre-alpine AS builder
WORKDIR /app
COPY target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:21-jre-alpine WORKDIR /app
按变化频率倒序 COPY —— 越不容易变的越靠前
COPY --from=builder /app/dependencies/ ./ COPY --from=builder /app/spring-boot-loader/ ./ COPY --from=builder /app/snapshot-dependencies/ ./ COPY --from=builder /app/application/ ./创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser优雅关闭
STOPSIGNAL SIGTERMEXPOSE 8080
ENTRYPOINT ["java", "-XX:+UseG1GC", "-XX:MaxRAMPercentage=75.0", "-Djava.security.egd=file:/dev/./urandom", "org.springframework.boot.loader.launch.JarLauncher"]
`
为什么这样写?
| 容器场景下自动适配内存限制 三、优雅关闭与健康检查
Spring Boot 优雅关闭
`yaml
`application.yml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
Docker 健康检查
`dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=40s CMD wget -qO- http://localhost:8080/actuator/health || exit 1
`
四、Docker Compose 编排
`yaml
version: '3.8'
services: app: build: . image: myapp:${APP_VERSION:-latest} container_name: myapp ports: - "8080:8080" environment: - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/myapp - SPRING_REDIS_HOST=redis - SERVER_TOMCAT_THREADS_MAX=200 depends_on: mysql: condition: service_healthy redis: condition: service_started healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] interval: 30s timeout: 5s retries: 3 start_period: 60s deploy: resources: limits: cpus: '2' memory: 2G reservations: cpus: '0.5' memory: 512M restart: unless-stopped logging: driver: "json-file" options: max-size: "50m" max-file: "3"
mysql: image: mysql:8.0 container_name: mysql environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} MYSQL_DATABASE: myapp volumes: - mysql_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5
redis: image: redis:7-alpine container_name: redis volumes: - redis_data:/data
volumes:
mysql_data:
redis_data:
`
五、JVM 容器感知 —— 别被 OOM Killer 杀掉
JDK 10+ 引入了 UseContainerSupport(默认开启),JVM 能感知容器的 cgroup 限制。
`dockerfile
✅ 推荐:用百分比,跟随容器限制自动适配
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
❌ 避免:固定值,容器 2G 限制时堆 4G → OOM Kill
ENTRYPOINT ["java", "-Xmx4g", "-jar", "app.jar"] `内存计算
容器限制 | MaxRAMPercentage | 堆大小 | 剩余给 OS/Metaspace 512M | 75% | 384M | 128M(偏紧)
1G | 75% | 768M | 256M
2G | 75% | 1.5G | 512M
4G | 75% | 3G | 1G 经验法则:
MaxRAMPercentage 设为 75%,留 25% 给 Metaspace、线程栈和 OS 缓存。六、CI/CD 构建流水线
`yaml
.github/workflows/deploy.yml
name: Build and Deployon:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B clean package -DskipTests
- name: Build Docker image
run: |
docker build -t $REGISTRY/$IMAGE_NAME:${{ github.sha }} .
docker tag $REGISTRY/$IMAGE_NAME:${{ github.sha }} $REGISTRY/$IMAGE_NAME:latest
- name: Push to registry
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login $REGISTRY -u ${{ github.actor }} --password-stdin
docker push $REGISTRY/$IMAGE_NAME:${{ github.sha }}
docker push $REGISTRY/$IMAGE_NAME:latest
- name: Deploy to server
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/myapp
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
`七、镜像瘦身终极方案
如果你对镜像大小有极致追求,可以尝试 jlink + 自定义 JRE:
`dockerfile
第一步:用 jlink 生成最小化 JRE
FROM eclipse-temurin:21-jdk-alpine AS jre-builder
RUN jlink --add-modules java.base,java.logging,java.sql,java.naming,java.management,java.net.http,jdk.unsupported --strip-debug --no-man-pages --no-header-files --compress=zip-6 --output /custom-jre第二步:最终镜像
FROM alpine:3.20
COPY --from=jre-builder /custom-jre /opt/jre
... 其他层
ENV PATH="/opt/jre/bin:$PATH"
`这样做能把基础镜像压缩到 40MB 以内,整个应用镜像 ~80MB。
八、总结
层次 | 最佳实践 基础镜像 | JRE + Alpine,不用 JDK
分层 | 开启 Spring Boot layers,依赖与代码分离
安全 | 非 root 用户运行
资源 | MaxRAMPercentage 而非 -Xmx 固定值
关闭 | server.shutdown=graceful + STOPSIGNAL SIGTERM
健康 | HEALTHCHECK + /actuator/health`
日志 | Docker json-file + 大小轮转
CI/CD | 多阶段构建 + 镜像缓存 + 自动部署 一个好的 Docker 镜像就像一段好代码——简洁、内聚、安全、易于维护。你的 Dockerfile 本身也应该被 Code Review。