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结合的核心实践:
- 基础镜像构建:使用多阶段构建优化镜像大小
- Java客户端操作:通过docker-java库实现容器管理
- 服务编排:使用Docker Compose管理多服务应用
- 生产部署:安全配置、健康检查、资源限制
- 集成测试:Testcontainers实现真实环境测试
- 监控告警:Prometheus + Grafana监控体系
掌握这些技能,可以让Java应用在容器化环境中更加稳定高效地运行。