springboot

Docker 容器化与生产部署

By AI-Writer 13 min read

Docker 容器化与生产部署

将 Spring Boot 应用容器化是现代云原生部署的标准方式。Spring Boot 内置的 fat jar 机制和分层镜像构建能力,让 Docker 镜像既小又快。本文覆盖从镜像构建到 CI/CD 流水线的完整流程。

Spring Boot fat jar 原理

Spring Boot 应用打包后是一个可直接运行的 fat jar(也称 uber-jar),其内部结构如下:

plaintext
boot-app.jar
├── META-INF/
│   └── MANIFEST.MF
│       Main-Class: org.springframework.boot.loader.JarLauncher
├── BOOT-INF/
│   ├── classes/              # 项目编译后的 class 文件和资源
│   │   └── com/example/demo/
│   │       └── DemoApplication.class
│   └── lib/                  # 所有依赖 JAR
│       ├── spring-boot-3.4.0.jar
│       ├── spring-core-6.x.jar
│       ├── HikariCP-5.x.jar
│       └── ...

关键点Main-Class 不是你的启动类,而是 JarLauncher。它使用自定义类加载器,按以下顺序加载:

  1. BOOT-INF/classes/ — 项目代码
  2. BOOT-INF/lib/*.jar — 依赖 JAR

这样同一个依赖 JAR 在 fat jar 中只能存在一份,同时与用户代码分离,为分层镜像提供了基础。

分层 Docker 镜像构建(Layered JAR)

Spring Boot 3.x 支持分层 jar(layered jar),将 fat jar 的内容分成多个逻辑层,从而实现增量构建,大幅缩短镜像构建和推送时间。

启用分层

xml
<!-- pom.xml -->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <layers>
                    <enabled>true</enabled>
                </layers>
            </configuration>
        </plugin>
    </plugins>
</build>

查看分层结构:

bash
./mvnw spring-boot:layered-jar
java -Djarmode=layertools -jar target/demo-0.0.1.jar list

输出:

plaintext
dependencies
spring-boot-loader
snapshot-dependencies
application

Dockerfile(分层构建)

dockerfile
# multi-stage build:先构建 jar,再分 layer 打包
FROM eclipse-temurin:21-jdk-alpine AS builder

WORKDIR /app
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN chmod +x mvnw
RUN ./mvnw dependency:go-offline -B

COPY src ./src
RUN ./mvnw package -DskipTests -B

# 从分层 jar 中提取各层
FROM eclipse-temurin:21-jre-alpine AS runtime

# 分离各层,提高构建缓存命中率
COPY --from=builder /app/target/demo-0.0.1-SNAPSHOT.jar /app/app.jar
RUN java -Djarmode=layertools -jar /app/app.jar extract

# 顺序:dependencies(变动最少)→ snapshot-dependencies → spring-boot-loader → application
FROM eclipse-temurin:21-jre-alpine

ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC"

# 按顺序 COPY,利用 Docker 缓存
COPY --from=runtime app/dependencies/ ./
COPY --from=runtime app/spring-boot-loader/ ./
COPY --from=runtime app/snapshot-dependencies/ ./
COPY --from=runtime app/application/ ./

ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

构建优化效果

方式镜像大小(典型)首次构建依赖变更后重建
普通 fat jar~350 MB整个镜像重传
分层镜像~220 MB(基础 JRE 层复用)仅 application 层重传
多阶段 + Alpine JRE~180 MB仅 application 层重传

docker-compose 多容器编排

典型微服务依赖:MySQL + Redis + App

yaml
# docker-compose.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/blog?useSSL=false&allowPublicKeyRetrieval=true
      - SPRING_DATASOURCE_USERNAME=blog
      - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
      - SPRING_REDIS_HOST=redis
      - SPRING_REDIS_PORT=6379
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  mysql:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
      - MYSQL_DATABASE=blog
      - MYSQL_USER=blog
      - MYSQL_PASSWORD=${DB_PASSWORD}
    volumes:
      - mysql-data:/var/lib/mysql
      - ./docker/mysql/init:/docker-entrypoint-initdb.d  # SQL 初始化脚本
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD}"]
      interval: 10s
      timeout: 5s
      retries: 5
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes

volumes:
  mysql-data:
  redis-data:

启动命令

bash
# 启动所有服务(后台运行)
docker-compose up -d --build

# 查看日志
docker-compose logs -f app

# 停止并清理
docker-compose down -v

生产环境配置

生产环境 application-prod.yml

yaml
# application-prod.yml
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/blog?useSSL=true&requireSSL=true
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

  jpa:
    hibernate:
      ddl-auto: validate  # 生产环境用 validate,仅校验不修改表结构
    open-in-view: false    # 生产环境关闭 OSIV,防止长事务
    properties:
      hibernate:
        generate_statistics: false

  data:
    redis:
      host: ${REDIS_HOST}
      port: 6379
      password: ${REDIS_PASSWORD}
      lettuce:
        pool:
          max-active: 16
          max-idle: 8
          min-idle: 4

server:
  port: 8080
  compression:
    enabled: true
  http2:
    enabled: true

logging:
  level:
    root: INFO
    com.example: INFO
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when_authorized

GitHub Actions CI/CD

完整流水线

yaml
# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # ============ Job 1:构建与测试 ============
  build:
    runs-on: ubuntu-latest

    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: ./mvnw clean package -B -DskipTests=false

      - name: Run tests
        run: ./mvnw test

      - name: Upload JAR artifact
        uses: actions/upload-artifact@v4
        with:
          name: app-jar
          path: target/demo-0.0.1-SNAPSHOT.jar

  # ============ Job 2:构建镜像 ============
  docker:
    runs-on: ubuntu-latest
    needs: build
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Download JAR artifact
        uses: actions/download-artifact@v4
        with:
          name: app-jar
          path: target/

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # ============ Job 3:部署(仅 push 到 main 时)===========
  deploy:
    runs-on: ubuntu-latest
    needs: docker
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'

    steps:
      - name: Deploy to server via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
            docker-compose -f /opt/blog/docker-compose.yml down
            docker-compose -f /opt/blog/docker-compose.yml up -d
            docker image prune -f

健康检查

java
// 自定义健康指示器
@Component
public class DatabaseHealthIndicator implements HealthIndicator {

    private final DataSource dataSource;

    @Override
    public Health health() {
        try (Connection conn = dataSource.getConnection()) {
            return Health.up()
                    .withDetail("database", conn.getCatalog())
                    .withDetail("connectionPool", getPoolStats())
                    .build();
        } catch (SQLException e) {
            return Health.down()
                    .withDetail("error", e.getMessage())
                    .build();
        }
    }

    private Map<String, Object> getPoolStats() {
        HikariDataSource hikari = (HikariDataSource) dataSource;
        return Map.of(
            "active", hikari.getHikariPoolMXBean().getActiveConnections(),
            "idle", hikari.getHikariPoolMXBean().getIdleConnections(),
            "total", hikari.getHikariPoolMXBean().getTotalConnections()
        );
    }
}

小结

  • Spring Boot fat jar 通过 JarLauncher + 自定义类加载器实现,内部分为 classeslib 两部分
  • 分层镜像利用 Docker 的分层缓存机制,仅在代码变更时重建对应层,大幅提升 CI/CD 效率
  • docker-compose 编排多容器时,使用 depends_on + condition: service_healthy 确保依赖服务就绪后再启动
  • 生产环境关键配置:ddl-auto: validateopen-in-view: false、HikariCP 连接池调优
  • GitHub Actions CI/CD 流水线分为构建 → 测试 → 打包镜像 → 部署 四个阶段
#springboot #docker #docker-compose #cicd #github-actions

评论

A

Written by

AI-Writer

Related Articles

springboot
#8

单元测试与集成测试

@SpringBootTest 集成测试、@MockBean 模拟依赖、@WebMvcTest 切片测试 REST 接口、AssertJ 断言库、测试数据库配置与 Spring Boot Test 新特性

Read More
springboot
#9

Docker 容器化与生产部署

Spring Boot fat jar 原理、分层 Docker 镜像构建(layered jar)、docker-compose 多容器编排、GitHub Actions CI/CD 流水线与生产环境配置示例

Read More