一、核心概念
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 应用
- 创建或获取一个 Spring Boot 项目。
- 确保应用可独立运行:使用
spring-boot-maven-plugin
或spring-boot-gradle-plugin
构建一个可执行的 Fat Jar。 - 构建 Fat Jar:
生成的 JAR 文件通常在# Maven mvn clean package # Gradle ./gradlew clean build
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)。 |
四、注意事项
- 基础镜像选择:
- 优先选择
-slim
,-alpine
等精简版镜像(如openjdk:17-jre-slim
)。 - Alpine 镜像更小,但基于
musl libc
,某些 JNI 库可能有兼容性问题。 - 避免使用
latest
标签,使用具体版本(如17-jre-slim
)保证可重复性。
- 优先选择
- 非 root 用户:
- 生产环境必须在镜像中创建非 root 用户并使用
USER
指令切换,提高安全性。
- 生产环境必须在镜像中创建非 root 用户并使用
- JVM 参数:
- 容器中设置 JVM 内存 (
-Xmx
,-Xms
) 时,需考虑容器内存限制。 - 使用
-XX:+UseContainerSupport
(JDK 8u191+, 10+) 让 JVM 识别容器内存限制。 - 使用
-Djava.security.egd=file:/dev/./urandom
加速启动(避免/dev/random
阻塞)。
- 容器中设置 JVM 内存 (
- 健康检查:
- 在
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
- 在
- 日志管理:
- 应用日志输出到
stdout
/stderr
,由 Docker 收集(docker logs
)。 - 避免在容器内写大日志文件到磁盘,除非挂载了卷。
- 应用日志输出到
- 数据持久化:
- 数据库、文件上传等需要持久化的数据,必须使用
Volume
挂载,不能存储在容器内部。
- 数据库、文件上传等需要持久化的数据,必须使用
- 网络:
- 默认
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
挂载卷,将宿主机的配置文件映射到容器内。
Spring Boot 会自动加载docker run -d \ -v /host/config/application-prod.yml:/app/config/application-prod.yml \ my-spring-boot-app:1.0.0
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 } }
六、最佳实践
- 使用多阶段构建:这是减小镜像大小和提高安全性的黄金标准。
- 选择合适的精简基础镜像:
*-slim
或*-alpine
。 - 以非 root 用户运行:显著降低安全风险。
- 设置合理的资源限制:
docker run -d \ --memory=512m \ --cpus=1.0 \ my-spring-boot-app:1.0.0
- 配置健康检查 (HEALTHCHECK):让编排系统(如 K8s)能正确管理容器生命周期。
- 日志输出到 stdout/stderr:方便集中收集(如 ELK, Fluentd)。
- 使用
.dockerignore
:优化构建过程。 - 镜像版本化:使用语义化版本号(
1.0.0
,v1.2.3
),避免latest
。 - 使用
docker-compose
管理多服务:简化本地开发和测试。 - 集成 CI/CD:在 Jenkins, GitLab CI, GitHub Actions 等中自动化构建、测试、推送镜像。
- 安全扫描:使用
docker scan <image>
或 Trivy 等工具扫描镜像漏洞。 - 监控:结合 Prometheus/Grafana 监控容器和应用指标。
七、性能优化
- 减小镜像大小:
- 多阶段构建。
- 选择精简基础镜像。
- 删除不必要的文件(如文档、缓存)。
- 合并
RUN
指令(减少层数,但注意可读性和缓存)。
- JVM 调优:
- 设置合理的堆内存:
-Xms
和-Xmx
设为相同值,避免动态调整开销。 - 考虑容器内存限制:确保
-Xmx
< 容器内存限制,留出空间给非堆内存和系统。 - 选择合适的 GC:对于响应时间敏感的应用,考虑
G1GC
或ZGC
/Shenandoah
(JDK 11+)。 - 利用容器支持:确保使用
-XX:+UseContainerSupport
。
- 设置合理的堆内存:
- 优化构建速度:
.dockerignore
。- 利用构建缓存(依赖文件放前面)。
- 使用 BuildKit。
- 配置镜像加速器。
- 网络优化:
- 对于高并发应用,考虑使用
host
网络模式(--network=host
)减少网络开销(牺牲隔离性)。 - 确保
EXPOSE
端口与应用实际监听端口一致。
- 对于高并发应用,考虑使用
- 资源隔离:
- 使用
--memory
,--cpus
限制容器资源,防止一个容器耗尽宿主机资源。
- 使用
- 应用自身优化:
- Spring Boot 应用本身的性能(数据库查询、缓存、异步处理)是关键。容器化不会解决应用层面的性能瓶颈。
总结
使用 Docker 部署 Spring Boot 应用已成为现代开发的标准实践。
核心步骤回顾:
- 构建 Fat Jar (
mvn package
)。 - 编写
Dockerfile
:务必使用多阶段构建。 - 构建镜像 (
docker build
)。 - 运行容器 (
docker run
)。 - (可选) 使用
docker-compose
管理多服务。 - (生产) 推送镜像到仓库。
- (生产) 配置健康检查、资源限制、安全策略。
成功关键:安全(非 root 用户、最小权限)、效率(小镜像、快构建)、可观测性(日志、监控)、自动化(CI/CD)。遵循最佳实践,你的 Spring Boot 应用将高效、可靠地运行在 Docker 容器中。