一、核心概念

1.1 什么是 Docker?

  • Docker 是一个开源的容器化平台,允许你将应用及其所有依赖(代码、运行时、库、环境变量、配置文件)打包到一个轻量级、可移植的容器 (Container) 中。
  • 容器在宿主机的操作系统内核上运行,通过命名空间 (Namespaces) 和控制组 (cgroups) 实现隔离,比虚拟机更高效。

1.2 为什么用 Docker 部署 Spring Boot?

优势 说明
环境一致性 “Build Once, Run Anywhere”。开发、测试、生产环境完全一致,避免“在我机器上能跑”问题。
快速部署与扩展 秒级启动/停止容器,轻松实现水平扩展(Scale Out)。
资源隔离与高效 容器共享宿主机内核,资源开销远小于虚拟机,密度更高。
简化依赖管理 所有依赖打包在镜像中,无需在宿主机上安装 Java、Tomcat 等。
微服务架构基础 Docker 是实现微服务部署、编排(如 Kubernetes)的核心技术。
版本控制 镜像可版本化(如 myapp:1.0.0),便于回滚和管理。

1.3 核心术语

术语 说明
镜像 (Image) 一个只读模板,包含运行应用所需的一切(代码、依赖、配置、基础 OS 层)。由 Dockerfile 构建而成。
容器 (Container) 镜像的运行实例。你可以启动、停止、移动或删除容器。容器是进程隔离的。
Dockerfile 一个文本文件,包含一系列指令(FROM, COPY, RUN, CMD 等),用于自动化构建镜像。
仓库 (Registry) 存储和分发 Docker 镜像的服务。公共仓库:Docker Hub;私有仓库:Harbor, AWS ECR, 阿里云容器镜像服务。
层 (Layer) 镜像是由一系列只读层叠加而成。每条 Dockerfile 指令(RUN, COPY 等)会创建一个新层。层是可缓存的,提高构建效率。
多阶段构建 (Multi-stage Build) 在一个 Dockerfile 中使用多个 FROM 指令,将构建过程(需要 JDK)和运行时(只需要 JRE)分离,显著减小最终镜像大小。
卷 (Volume) 用于持久化数据或在容器间共享数据的机制。数据独立于容器生命周期。
网络 (Network) 容器间通信的虚拟网络。Docker 提供 bridge, host, overlay 等网络模式。

二、详细操作步骤

步骤 1:准备 Spring Boot 应用

  1. 创建或获取一个 Spring Boot 项目。
  2. 确保应用可独立运行:使用 spring-boot-maven-plugin 或 spring-boot-gradle-plugin 构建一个可执行的 Fat Jar。
  3. 构建 Fat Jar:
    # Maven
    mvn clean package
    # Gradle
    ./gradlew clean build
    
    生成的 JAR 文件通常在 target/ 或 build/libs/ 目录下,如 myapp-0.0.1-SNAPSHOT.jar。

步骤 2:创建 Dockerfile

在项目根目录创建名为 Dockerfile 的文件(无后缀)。

方案 A:基础方案(不推荐用于生产)

# Dockerfile (基础版 - 不推荐)
# 1. 基础镜像:选择一个包含 JRE 的镜像
FROM openjdk:8-jre-slim

# 2. 设置工作目录
WORKDIR /app

# 3. 将本地构建好的 Fat Jar 复制到容器内的 /app 目录
# 注意:需要先在宿主机执行 mvn package
COPY target/myapp-*.jar app.jar

# 4. 暴露应用端口(如 8080)
EXPOSE 8080

# 5. 容器启动时运行的命令
# 使用 exec 格式避免 shell 问题
CMD ["java", "-jar", "app.jar"]

方案 B:多阶段构建(推荐!生产最佳实践)

# Dockerfile (多阶段构建 - 推荐)
# ==================== 第一阶段:构建 (Builder) ====================
# 使用包含 JDK 的镜像进行编译
FROM maven:3.8.6-openjdk-8-slim AS builder

# 设置工作目录
WORKDIR /workspace

# 先复制 pom.xml,利用 Docker 缓存优化构建速度
COPY pom.xml .

# 下载依赖(如果 pom.xml 未变,此层可缓存)
RUN mvn dependency:go-offline -B

# 复制源码
COPY src src

# 执行编译并打包,生成 Fat Jar
RUN mvn package -DskipTests

# ==================== 第二阶段:运行时 ====================
# 使用极小的 JRE 镜像作为运行时基础
FROM openjdk:8-jre-slim

# 创建非 root 用户(安全最佳实践)
RUN addgroup --system spring && adduser --system spring --ingroup spring
USER spring:spring

# 设置工作目录
WORKDIR /app

# 从第一阶段的构建结果中复制 Fat Jar
# 注意:路径是第一阶段的 WORKDIR
COPY --from=builder /workspace/target/myapp-*.jar app.jar

# 暴露端口
EXPOSE 8080

# 使用 exec 格式运行
# 可以添加 JVM 参数
CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]

多阶段构建优势:

  • 最终镜像极小:只包含 JRE 和你的 JAR,不含 Maven、JDK、源码。
  • 安全性更高:运行时容器没有构建工具和源码。
  • 构建缓存:pom.xml 和依赖层可缓存,加快后续构建。

步骤 3:构建 Docker 镜像

在 Dockerfile 所在目录执行:

# 构建镜像,-t 指定镜像名称和标签
# 格式:docker build -t <镜像名>:<标签> <上下文路径>
# . 表示当前目录为构建上下文
docker build -t my-spring-boot-app:1.0.0 .

# 查看构建的镜像
docker images | grep my-spring-boot-app
# 输出示例:
# my-spring-boot-app   1.0.0   a1b2c3d4e5f6   20 minutes ago   250MB

步骤 4:运行容器

# 基本运行
docker run -d --name myapp-container -p 8080:8080 my-spring-boot-app:1.0.0

# 常用参数详解:
# -d: 后台运行 (detached)
# --name: 指定容器名称
# -p: 端口映射,宿主机端口:容器端口
# -e: 设置环境变量
# -v: 挂载卷
# --rm: 容器停止后自动删除 (适合临时测试)
# --network: 指定网络

步骤 5:验证应用

# 查看容器运行状态
docker ps

# 查看容器日志
docker logs myapp-container

# 访问应用
curl http://localhost:8080/actuator/health

# 进入容器内部 (调试用)
docker exec -it myapp-container /bin/sh
# 或 bash (如果镜像有 bash)
# docker exec -it myapp-container /bin/bash

步骤 6:使用 .dockerignore (可选但推荐)

创建 .dockerignore 文件,防止不必要的文件被复制到构建上下文中,加快构建速度。

# .dockerignore
target/
!target/*.jar
*.log
.git
.gitignore
README.md
Dockerfile
.dockerignore
*.md
**/.idea/
**/*.iml
**/.classpath
**/.project
**/.settings/
**/node_modules/

步骤 7:推送镜像到仓库(生产环境)

# 1. 登录到镜像仓库 (例如 Docker Hub)
docker login

# 2. 给本地镜像打标签 (Tag),格式: <仓库地址>/<用户名>/<镜像名>:<标签>
# Docker Hub 示例:
docker tag my-spring-boot-app:1.0.0 your-dockerhub-username/my-spring-boot-app:1.0.0
# 私有仓库示例 (阿里云):
# docker tag my-spring-boot-app:1.0.0 registry.cn-hangzhou.aliyuncs.com/your-namespace/my-spring-boot-app:1.0.0

# 3. 推送镜像
docker push your-dockerhub-username/my-spring-boot-app:1.0.0
# 或
# docker push registry.cn-hangzhou.aliyuncs.com/your-namespace/my-spring-boot-app:1.0.0

步骤 8:使用 docker-compose (本地开发/测试)

创建 docker-compose.yml 文件,用于定义和运行多容器应用。

# docker-compose.yml
version: '3.8' # 指定版本

services:
  # 定义你的 Spring Boot 应用服务
  app:
    # 构建方式:基于当前目录的 Dockerfile
    build:
      context: .
      # 可指定 Dockerfile 文件名
      # dockerfile: Dockerfile
    # 或使用已构建的镜像
    # image: my-spring-boot-app:1.0.0
    # 端口映射
    ports:
      - "8080:8080"
    # 环境变量
    environment:
      SPRING_PROFILES_ACTIVE: "docker"
      SPRING_DATASOURCE_URL: "jdbc:mysql://db:3306/mydb"
      # ... 其他配置
    # 依赖的服务,确保 db 先启动
    depends_on:
      - db
    # 挂载卷 (用于配置文件热更新,开发环境)
    # volumes:
    #   - ./config/application-docker.yml:/app/config/application-docker.yml
    # 重启策略
    restart: unless-stopped

  # 定义数据库服务 (MySQL)
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: mydb
      MYSQL_USER: user
      MYSQL_PASSWORD: userpass
    ports:
      - "3306:3306"
    volumes:
      - db_data:/var/lib/mysql # 持久化数据
    restart: unless-stopped

  # 定义 Redis 服务
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    restart: unless-stopped

# 定义卷
volumes:
  db_data:

使用 docker-compose:

# 构建并启动所有服务
docker-compose up -d

# 查看服务状态
docker-compose ps

# 查看日志
docker-compose logs -f app # -f 表示实时输出

# 停止并删除所有服务
docker-compose down

# 仅停止
# docker-compose stop

三、常见错误与解决方案

错误现象 原因分析 解决方案
COPY failed: no such file or directory Dockerfile 中 COPY 的路径在构建上下文中不存在 1. 确认 COPY 的源路径相对于 docker build 命令的上下文路径正确。
2. 确保 Fat Jar 已构建 (mvn package)。
3. 检查 .dockerignore 是否误排除了 JAR 文件。
java.lang.UnsupportedClassVersionError 镜像中的 JRE 版本低于应用编译的 JDK 版本 检查 Dockerfile 的 FROM 镜像(如 openjdk:17-jre-slim)是否匹配应用的 sourceCompatibility/targetCompatibility。
应用启动后立即退出 CMD 或 ENTRYPOINT 配置错误,或应用启动失败 1. 使用 docker logs <container_name> 查看详细错误日志。
2. 检查 CMD 语法(推荐 exec 格式)。
3> 确认主类和 JAR 名称正确。
端口无法访问 (Connection refused) 1. 容器未正确映射端口 (-p)
2. 应用监听地址非 0.0.0.0
3. 防火墙/安全组限制
1. 确认 docker run -p 8080:8080。
2. 确保 Spring Boot 应用监听 0.0.0.0 (默认)。
3> 检查宿主机防火墙和云服务商安全组。
docker: permission denied Docker 守护进程权限问题 (Linux) 将用户加入 docker 组:sudo usermod -aG docker $USER,然后重新登录。
镜像构建缓慢 1. 未利用缓存
2. 依赖下载慢
1. 使用 .dockerignore。
2. 优化 Dockerfile 顺序(不变的指令放前面)。
3> 配置 Docker 镜像加速器(如阿里云镜像加速)。
Cannot connect to the Docker daemon Docker 服务未启动 启动 Docker 服务:sudo systemctl start docker (Linux)。

四、注意事项

  1. 基础镜像选择:
    • 优先选择 -slim, -alpine 等精简版镜像(如 openjdk:17-jre-slim)。
    • Alpine 镜像更小,但基于 musl libc,某些 JNI 库可能有兼容性问题。
    • 避免使用 latest 标签,使用具体版本(如 17-jre-slim)保证可重复性。
  2. 非 root 用户:
    • 生产环境必须在镜像中创建非 root 用户并使用 USER 指令切换,提高安全性。
  3. JVM 参数:
    • 容器中设置 JVM 内存 (-Xmx, -Xms) 时,需考虑容器内存限制。
    • 使用 -XX:+UseContainerSupport (JDK 8u191+, 10+) 让 JVM 识别容器内存限制。
    • 使用 -Djava.security.egd=file:/dev/./urandom 加速启动(避免 /dev/random 阻塞)。
  4. 健康检查:
    • 在 Dockerfile 或 docker-compose.yml 中添加 HEALTHCHECK 指令,让 Docker 监控应用状态。
    HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
      CMD curl -f http://localhost:8080/actuator/health || exit 1
    
  5. 日志管理:
    • 应用日志输出到 stdout/stderr,由 Docker 收集(docker logs)。
    • 避免在容器内写大日志文件到磁盘,除非挂载了卷。
  6. 数据持久化:
    • 数据库、文件上传等需要持久化的数据,必须使用 Volume 挂载,不能存储在容器内部。
  7. 网络:
    • 默认 bridge 网络,容器间可通过服务名通信(在 docker-compose 中)。
    • 生产环境考虑使用更复杂的网络方案(如 overlay for Swarm/K8s)。

五、使用技巧

5.1 优化 Dockerfile 构建缓存

# 好的做法:先复制依赖文件,利用缓存
COPY pom.xml .
RUN mvn dependency:go-offline -B # 依赖不变时,此层可缓存
COPY src src
RUN mvn package -DskipTests # 只有源码改变时才重新编译

5.2 使用 .dockerignore

如前所述,防止无关文件进入构建上下文。

5.3 动态注入配置

  • 环境变量:在 docker run -e 或 docker-compose.yml 中设置 SPRING_PROFILES_ACTIVE, SPRING_DATASOURCE_URL 等。
  • 外部配置文件:通过 -v 挂载卷,将宿主机的配置文件映射到容器内。
    docker run -d \
      -v /host/config/application-prod.yml:/app/config/application-prod.yml \
      my-spring-boot-app:1.0.0
    
    Spring Boot 会自动加载 config/ 目录下的配置。

5.4 调试容器

# 查看容器内进程
docker top myapp-container

# 查看容器资源使用 (CPU, 内存)
docker stats myapp-container

# 进入容器
docker exec -it myapp-container sh

# 查看容器详细信息 (IP, 端口映射等)
docker inspect myapp-container

5.5 使用 BuildKit (Docker 18.09+)

启用 BuildKit 可以获得更好的性能和特性(如秘密管理)。

# 临时启用
DOCKER_BUILDKIT=1 docker build -t myapp:1.0.0 .

# 或在 ~/.docker/config.json 中永久启用
# { "features": { "buildkit": true } }

六、最佳实践

  1. 使用多阶段构建:这是减小镜像大小和提高安全性的黄金标准。
  2. 选择合适的精简基础镜像:*-slim 或 *-alpine。
  3. 以非 root 用户运行:显著降低安全风险。
  4. 设置合理的资源限制:
    docker run -d \
      --memory=512m \
      --cpus=1.0 \
      my-spring-boot-app:1.0.0
    
  5. 配置健康检查 (HEALTHCHECK):让编排系统(如 K8s)能正确管理容器生命周期。
  6. 日志输出到 stdout/stderr:方便集中收集(如 ELK, Fluentd)。
  7. 使用 .dockerignore:优化构建过程。
  8. 镜像版本化:使用语义化版本号(1.0.0, v1.2.3),避免 latest。
  9. 使用 docker-compose 管理多服务:简化本地开发和测试。
  10. 集成 CI/CD:在 Jenkins, GitLab CI, GitHub Actions 等中自动化构建、测试、推送镜像。
  11. 安全扫描:使用 docker scan <image> 或 Trivy 等工具扫描镜像漏洞。
  12. 监控:结合 Prometheus/Grafana 监控容器和应用指标。

七、性能优化

  1. 减小镜像大小:
    • 多阶段构建。
    • 选择精简基础镜像。
    • 删除不必要的文件(如文档、缓存)。
    • 合并 RUN 指令(减少层数,但注意可读性和缓存)。
  2. JVM 调优:
    • 设置合理的堆内存:-Xms 和 -Xmx 设为相同值,避免动态调整开销。
    • 考虑容器内存限制:确保 -Xmx < 容器内存限制,留出空间给非堆内存和系统。
    • 选择合适的 GC:对于响应时间敏感的应用,考虑 G1GC 或 ZGC/Shenandoah (JDK 11+)。
    • 利用容器支持:确保使用 -XX:+UseContainerSupport。
  3. 优化构建速度:
    • .dockerignore。
    • 利用构建缓存(依赖文件放前面)。
    • 使用 BuildKit。
    • 配置镜像加速器。
  4. 网络优化:
    • 对于高并发应用,考虑使用 host 网络模式(--network=host)减少网络开销(牺牲隔离性)。
    • 确保 EXPOSE 端口与应用实际监听端口一致。
  5. 资源隔离:
    • 使用 --memory, --cpus 限制容器资源,防止一个容器耗尽宿主机资源。
  6. 应用自身优化:
    • Spring Boot 应用本身的性能(数据库查询、缓存、异步处理)是关键。容器化不会解决应用层面的性能瓶颈。

总结

使用 Docker 部署 Spring Boot 应用已成为现代开发的标准实践。

核心步骤回顾:

  1. 构建 Fat Jar (mvn package)。
  2. 编写 Dockerfile:务必使用多阶段构建。
  3. 构建镜像 (docker build)。
  4. 运行容器 (docker run)。
  5. (可选) 使用 docker-compose 管理多服务。
  6. (生产) 推送镜像到仓库。
  7. (生产) 配置健康检查、资源限制、安全策略。

成功关键:安全(非 root 用户、最小权限)、效率(小镜像、快构建)、可观测性(日志、监控)、自动化(CI/CD)。遵循最佳实践,你的 Spring Boot 应用将高效、可靠地运行在 Docker 容器中。