Docker与Java实战指南

Docker与Java实战指南:从入门到生产部署

前言

在现代微服务架构中,Docker已经成为Java应用部署的标准方式。本文将深入介绍Docker与Java的结合使用,包括镜像构建、容器编排、以及与Spring Boot等主流框架的集成实践。

一、Docker基础架构

复制代码
+--------------------------------------------------+
|                    Docker Host                    |
|  +--------------------------------------------+  |
|  |              Docker Engine                 |  |
|  |  +--------+  +--------+  +--------+       |  |
|  |  |Container|  |Container|  |Container|     |  |
|  |  | Java   |  | MySQL  |  | Redis  |       |  |
|  |  | App    |  |        |  |        |       |  |
|  |  +--------+  +--------+  +--------+       |  |
|  |                                            |  |
|  |  +--------------------------------------+  |  |
|  |  |           Docker Images              |  |  |
|  |  | openjdk:17 | mysql:8 | redis:7      |  |  |
|  |  +--------------------------------------+  |  |
|  +--------------------------------------------+  |
+--------------------------------------------------+

二、Java应用Docker化

2.1 基础Dockerfile编写

dockerfile 复制代码
# 多阶段构建 - 构建阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

# 运行阶段
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar

# JVM优化参数
ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC"

EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

2.2 Spring Boot项目结构

复制代码
+-- my-spring-app/
    +-- src/
    |   +-- main/
    |       +-- java/
    |       |   +-- com/example/
    |       |       +-- Application.java
    |       |       +-- controller/
    |       |       +-- service/
    |       +-- resources/
    |           +-- application.yml
    +-- Dockerfile
    +-- docker-compose.yml
    +-- pom.xml

三、Java代码实现Docker管理

3.1 引入Docker Java客户端

pom.xml中添加依赖:

xml 复制代码
<dependencies>
    <!-- Docker Java Client -->
    <dependency>
        <groupId>com.github.docker-java</groupId>
        <artifactId>docker-java-core</artifactId>
        <version>3.3.4</version>
    </dependency>
    <dependency>
        <groupId>com.github.docker-java</groupId>
        <artifactId>docker-java-transport-httpclient5</artifactId>
        <version>3.3.4</version>
    </dependency>

    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3.2 Docker客户端配置类

java 复制代码
package com.example.docker.config;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
public class DockerConfig {

    @Value("${docker.host:unix:///var/run/docker.sock}")
    private String dockerHost;

    @Bean
    public DockerClient dockerClient() {
        DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerHost(dockerHost)
                .withDockerTlsVerify(false)
                .build();

        DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
                .dockerHost(config.getDockerHost())
                .sslConfig(config.getSSLConfig())
                .maxConnections(100)
                .connectionTimeout(Duration.ofSeconds(30))
                .responseTimeout(Duration.ofSeconds(45))
                .build();

        return DockerClientImpl.getInstance(config, httpClient);
    }
}

3.3 Docker服务管理类

java 复制代码
package com.example.docker.service;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.PullImageResultCallback;
import com.github.dockerjava.api.model.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class DockerService {

    private static final Logger log = LoggerFactory.getLogger(DockerService.class);

    private final DockerClient dockerClient;

    public DockerService(DockerClient dockerClient) {
        this.dockerClient = dockerClient;
    }

    /**
     * 拉取镜像
     */
    public void pullImage(String imageName, String tag) throws InterruptedException {
        log.info("开始拉取镜像: {}:{}", imageName, tag);

        dockerClient.pullImageCmd(imageName)
                .withTag(tag)
                .exec(new PullImageResultCallback())
                .awaitCompletion(5, TimeUnit.MINUTES);

        log.info("镜像拉取完成: {}:{}", imageName, tag);
    }

    /**
     * 创建并启动容器
     */
    public String createAndStartContainer(String imageName, String containerName,
                                          int hostPort, int containerPort) {
        // 端口绑定
        PortBinding portBinding = PortBinding.parse(hostPort + ":" + containerPort);

        HostConfig hostConfig = HostConfig.newHostConfig()
                .withPortBindings(portBinding)
                .withMemory(512 * 1024 * 1024L)  // 512MB内存限制
                .withCpuCount(1L);

        // 创建容器
        CreateContainerResponse container = dockerClient.createContainerCmd(imageName)
                .withName(containerName)
                .withHostConfig(hostConfig)
                .withExposedPorts(ExposedPort.tcp(containerPort))
                .withEnv("JAVA_OPTS=-Xms256m -Xmx256m")
                .exec();

        String containerId = container.getId();
        log.info("容器创建成功, ID: {}", containerId);

        // 启动容器
        dockerClient.startContainerCmd(containerId).exec();
        log.info("容器启动成功: {}", containerName);

        return containerId;
    }

    /**
     * 获取所有容器列表
     */
    public List<Container> listContainers(boolean showAll) {
        return dockerClient.listContainersCmd()
                .withShowAll(showAll)
                .exec();
    }

    /**
     * 停止容器
     */
    public void stopContainer(String containerId) {
        dockerClient.stopContainerCmd(containerId)
                .withTimeout(30)
                .exec();
        log.info("容器已停止: {}", containerId);
    }

    /**
     * 删除容器
     */
    public void removeContainer(String containerId, boolean force) {
        dockerClient.removeContainerCmd(containerId)
                .withForce(force)
                .exec();
        log.info("容器已删除: {}", containerId);
    }

    /**
     * 获取容器日志
     */
    public String getContainerLogs(String containerId, int tailLines) {
        StringBuilder logs = new StringBuilder();

        try {
            dockerClient.logContainerCmd(containerId)
                    .withStdOut(true)
                    .withStdErr(true)
                    .withTail(tailLines)
                    .exec(new ResultCallback.Adapter<Frame>() {
                        @Override
                        public void onNext(Frame frame) {
                            logs.append(new String(frame.getPayload()));
                        }
                    }).awaitCompletion(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("获取日志被中断", e);
        }

        return logs.toString();
    }

    /**
     * 获取容器统计信息
     */
    public Statistics getContainerStats(String containerId) {
        return dockerClient.statsCmd(containerId)
                .exec(new ResultCallback.Adapter<Statistics>() {})
                .getStats();
    }
}

3.4 REST控制器

java 复制代码
package com.example.docker.controller;

import com.example.docker.service.DockerService;
import com.github.dockerjava.api.model.Container;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/docker")
public class DockerController {

    private final DockerService dockerService;

    public DockerController(DockerService dockerService) {
        this.dockerService = dockerService;
    }

    @GetMapping("/containers")
    public ResponseEntity<List<Container>> listContainers(
            @RequestParam(defaultValue = "false") boolean showAll) {
        return ResponseEntity.ok(dockerService.listContainers(showAll));
    }

    @PostMapping("/containers")
    public ResponseEntity<Map<String, String>> createContainer(
            @RequestParam String image,
            @RequestParam String name,
            @RequestParam int hostPort,
            @RequestParam int containerPort) {

        String containerId = dockerService.createAndStartContainer(
                image, name, hostPort, containerPort);

        return ResponseEntity.ok(Map.of(
                "containerId", containerId,
                "message", "容器创建并启动成功"
        ));
    }

    @PostMapping("/containers/{id}/stop")
    public ResponseEntity<Map<String, String>> stopContainer(@PathVariable String id) {
        dockerService.stopContainer(id);
        return ResponseEntity.ok(Map.of("message", "容器已停止"));
    }

    @DeleteMapping("/containers/{id}")
    public ResponseEntity<Map<String, String>> removeContainer(
            @PathVariable String id,
            @RequestParam(defaultValue = "false") boolean force) {
        dockerService.removeContainer(id, force);
        return ResponseEntity.ok(Map.of("message", "容器已删除"));
    }

    @GetMapping("/containers/{id}/logs")
    public ResponseEntity<String> getContainerLogs(
            @PathVariable String id,
            @RequestParam(defaultValue = "100") int lines) {
        return ResponseEntity.ok(dockerService.getContainerLogs(id, lines));
    }
}

四、Docker Compose编排

4.1 多服务编排配置

yaml 复制代码
# docker-compose.yml
version: '3.8'

services:
  # Java应用
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: java-app
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/mydb
      - SPRING_REDIS_HOST=redis
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1'

  # MySQL数据库
  mysql:
    image: mysql:8.0
    container_name: mysql-db
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: mydb
      MYSQL_USER: appuser
      MYSQL_PASSWORD: apppass
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  # Redis缓存
  redis:
    image: redis:7-alpine
    container_name: redis-cache
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  mysql-data:
  redis-data:

4.2 服务依赖关系图

复制代码
+------------------+
|    Java App      |
|   (port 8080)    |
+--------+---------+
         |
    +----+----+
    |         |
    v         v
+-------+  +-------+
| MySQL |  | Redis |
| 3306  |  | 6379  |
+-------+  +-------+
    |         |
    v         v
+-------+  +-------+
|Volume |  |Volume |
+-------+  +-------+

五、生产环境最佳实践

5.1 优化的生产Dockerfile

dockerfile 复制代码
# 多阶段构建 - 第一阶段:构建
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build

# 先复制pom.xml,利用Docker缓存层
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 复制源码并构建
COPY src ./src
RUN mvn clean package -DskipTests -Dmaven.javadoc.skip=true

# 第二阶段:运行
FROM eclipse-temurin:17-jre-alpine

# 安全:创建非root用户
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -D appuser

WORKDIR /app

# 从构建阶段复制jar
COPY --from=builder /build/target/*.jar app.jar

# 设置文件权限
RUN chown -R appuser:appgroup /app

# 切换到非root用户
USER appuser

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1

# JVM参数:容器感知
ENV JAVA_OPTS="-XX:+UseContainerSupport \
    -XX:MaxRAMPercentage=75.0 \
    -XX:+UseG1GC \
    -XX:+HeapDumpOnOutOfMemoryError \
    -Djava.security.egd=file:/dev/./urandom"

EXPOSE 8080

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

5.2 Spring Boot健康检查端点

java 复制代码
package com.example.docker.health;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;

@Component
public class DatabaseHealthIndicator implements HealthIndicator {

    private final DataSource dataSource;

    public DatabaseHealthIndicator(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Health health() {
        try (Connection conn = dataSource.getConnection()) {
            if (conn.isValid(5)) {
                return Health.up()
                        .withDetail("database", "MySQL")
                        .withDetail("status", "Connected")
                        .build();
            }
        } catch (Exception e) {
            return Health.down()
                    .withDetail("error", e.getMessage())
                    .build();
        }
        return Health.down().build();
    }
}

5.3 应用配置文件

yaml 复制代码
# application-prod.yml
spring:
  application:
    name: docker-java-app

  datasource:
    url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/mydb}
    username: ${DB_USER:appuser}
    password: ${DB_PASSWORD:apppass}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 30000

  redis:
    host: ${SPRING_REDIS_HOST:localhost}
    port: ${SPRING_REDIS_PORT:6379}
    timeout: 5000ms

# Actuator配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when_authorized
      probes:
        enabled: true

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

六、Testcontainers集成测试

6.1 添加依赖

xml 复制代码
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.19.3</version>
    <scope>test</scope>
</dependency>

6.2 集成测试示例

java 复制代码
package com.example.docker;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Testcontainers
class ApplicationIntegrationTest {

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
            .withExposedPorts(6379);

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.datasource.username", mysql::getUsername);
        registry.add("spring.datasource.password", mysql::getPassword);
        registry.add("spring.redis.host", redis::getHost);
        registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void contextLoads() {
        assertThat(mysql.isRunning()).isTrue();
        assertThat(redis.isRunning()).isTrue();
    }

    @Test
    void shouldSaveAndRetrieveUser() {
        User user = new User("testuser", "test@example.com");
        User saved = userRepository.save(user);

        assertThat(saved.getId()).isNotNull();
        assertThat(userRepository.findById(saved.getId())).isPresent();
    }
}

七、容器监控与日志

7.1 监控架构

复制代码
+-------------+     +-------------+     +-------------+
|  Java App   | --> | Prometheus  | --> |   Grafana   |
| /metrics    |     |   :9090     |     |    :3000    |
+-------------+     +-------------+     +-------------+
      |
      v
+-------------+
|    Loki     |
|   (Logs)    |
+-------------+

7.2 Prometheus配置

yaml 复制代码
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'java-app'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['app:8080']

7.3 监控指标收集

java 复制代码
package com.example.docker.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class AppMetrics {

    private final Counter requestCounter;
    private final Timer responseTimer;

    public AppMetrics(MeterRegistry registry) {
        this.requestCounter = Counter.builder("app.requests.total")
                .description("Total number of requests")
                .tag("application", "docker-java-app")
                .register(registry);

        this.responseTimer = Timer.builder("app.response.time")
                .description("Response time")
                .tag("application", "docker-java-app")
                .register(registry);
    }

    public void incrementRequestCount() {
        requestCounter.increment();
    }

    public void recordResponseTime(long timeMs) {
        responseTimer.record(timeMs, TimeUnit.MILLISECONDS);
    }
}

八、CI/CD集成

8.1 GitHub Actions配置

yaml 复制代码
# .github/workflows/docker-build.yml
name: Build and Push Docker Image

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven

      - name: Build with Maven
        run: mvn clean package -DskipTests

      - name: Run Tests
        run: mvn test

      - name: Build Docker Image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Login to Docker Hub
        if: github.event_name == 'push'
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Push Docker Image
        if: github.event_name == 'push'
        run: |
          docker tag myapp:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/myapp:latest
          docker push ${{ secrets.DOCKER_USERNAME }}/myapp:latest

九、常见问题与解决方案

9.1 问题排查流程

复制代码
+------------------+
|   容器无法启动    |
+--------+---------+
         |
         v
+------------------+
| docker logs查看  |
+--------+---------+
         |
    +----+----+
    |         |
    v         v
+-------+  +--------+
|内存不足|  |端口冲突 |
+-------+  +--------+
    |         |
    v         v
+-------+  +--------+
|增加内存|  |修改端口 |
+-------+  +--------+

9.2 常用调试命令

bash 复制代码
# 查看容器日志
docker logs -f --tail 100 container_name

# 进入容器调试
docker exec -it container_name /bin/sh

# 查看容器资源使用
docker stats container_name

# 检查容器健康状态
docker inspect --format='{{.State.Health.Status}}' container_name

# 查看容器网络
docker network inspect app-network

9.3 JVM容器优化参数

java 复制代码
// 获取容器环境信息
public class ContainerInfo {
    public static void printContainerInfo() {
        Runtime runtime = Runtime.getRuntime();

        System.out.println("可用处理器: " + runtime.availableProcessors());
        System.out.println("最大内存: " + runtime.maxMemory() / 1024 / 1024 + "MB");
        System.out.println("已分配内存: " + runtime.totalMemory() / 1024 / 1024 + "MB");
        System.out.println("空闲内存: " + runtime.freeMemory() / 1024 / 1024 + "MB");
    }
}

十、总结

本文介绍了Docker与Java结合的核心实践:

  1. 基础镜像构建:使用多阶段构建优化镜像大小
  2. Java客户端操作:通过docker-java库实现容器管理
  3. 服务编排:使用Docker Compose管理多服务应用
  4. 生产部署:安全配置、健康检查、资源限制
  5. 集成测试:Testcontainers实现真实环境测试
  6. 监控告警:Prometheus + Grafana监控体系

掌握这些技能,可以让Java应用在容器化环境中更加稳定高效地运行。

相关推荐
是一个Bug3 小时前
声明式事务
java·开发语言·面试
妮妮喔妮3 小时前
Redis Cluster故障处理机制
java·数据库·redis
lang201509283 小时前
Sentinel预热限流:WarmUpController原理详解
java·spring·sentinel
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ3 小时前
SSE技术详解及应用场景
java
2501_941982053 小时前
RPA 赋能企业微信外部群:多群同步操作的技术实现
java·开发语言
Seven973 小时前
剑指offer-49、把字符串转换成整数
java
编程修仙3 小时前
第六篇 HttpServletRequest对象
java·spring boot·后端
杀死那个蝈坦3 小时前
微服务网关(Spring Cloud Gateway)实战攻略
java·微服务·架构
凌云若寒3 小时前
半导体标签打印的核心痛点分析
java