目录
- 为什么用 Docker 部署 Java 应用?
- 核心概念:
Dockerfile - 分步实战:构建并运行一个 Spring Boot 应用
- 第一步:准备一个 Java 应用(以 Spring Boot 为例)
- 第二步:编写
Dockerfile - 第三步:构建 Docker 镜像
- 第四步:运行 Docker 容器
- Docker 镜像优化技巧
- 生产环境部署最佳实践
- 使用
.dockerignore - 使用 Docker Compose(多容器应用)
- 数据持久化
- 日志管理
- 使用
为什么用 Docker 部署 Java 应用?
在 Docker 出现之前,我们常说 "在我的电脑上是好的"(It works on my machine),这通常是因为开发、测试和生产环境的操作系统、Java 版本、依赖库、配置文件等存在差异。

Docker 通过以下方式解决了这个问题:
- 环境一致性:将你的 Java 应用、其依赖(如 JAR 包)、运行时环境(如 JDK)以及配置文件打包到一个轻量级、可移植的 镜像 中,这个镜像在任何安装了 Docker 的机器上都能以相同的方式运行。
- 隔离性:容器之间是相互隔离的,每个容器都有自己的文件系统、进程空间和网络栈,这避免了应用之间的冲突。
- 可扩展性:可以通过简单的命令快速启动或停止多个容器实例,轻松实现应用的横向扩展。
- 简化部署:部署过程从一个复杂的安装脚本变成了一个简单的
docker run命令。 - 版本控制:Docker 镜像就像 Git 仓库一样,可以被版本化、追踪和回滚。
核心概念:Dockerfile
Dockerfile 是一个文本文件,它包含了用户为镜像构建所有步骤的指令,Docker 会通过读取这个文件来自动构建镜像。
一个典型的 Dockerfile 包含以下指令:
FROM: 指定基础镜像,openjdk:17-jre-slim。COPY: 将文件从构建上下文复制到镜像中。ADD: 类似COPY,但还可以解压压缩文件或从 URL 获取文件。WORKDIR: 在容器内设置一个工作目录,后续的RUN,CMD,COPY等指令都会在这个目录下执行。RUN: 在镜像中执行命令,通常用于安装软件包或进行环境配置。CMD: 容器启动时默认执行的命令。Dockerfile中只能有一个CMD,如果有多个,最后一个生效。ENTRYPOINT: 配置容器启动时运行的命令。CMD的参数会追加到ENTRYPOINT后面。ENTRYPOINT不像CMD那么容易被命令行参数覆盖。EXPOSE: 声明容器运行时监听的端口,但这只是一个声明,不会实际映射端口。VOLUME: 定义一个数据卷,用于数据持久化。
分步实战:构建并运行一个 Spring Boot 应用
我们以一个打包好的 Spring Boot JAR 文件为例。

第一步:准备一个 Java 应用
假设你已经有一个 Spring Boot 项目,并且已经通过 mvn package 或 gradle build 命令打包生成了 my-java-app.jar 文件。
项目结构如下:
my-java-app/
├── src/
├── target/
│ └── my-java-app-0.0.1-SNAPSHOT.jar <-- 这是我们的应用
├── pom.xml
└── ...其他文件
第二步:编写 Dockerfile
在 my-java-app 项目的根目录下,创建一个名为 Dockerfile 的文件(注意没有后缀名)。
直接运行 JAR(简单直接)

这是最简单的方式,适合快速原型和演示。
# 1. 指定基础镜像,选择一个精简版的 OpenJRE 17 # 你可以去 Docker Hub 查找最新的版本,如 eclipse-temurin, openjdk, adoptopenjdk 等 FROM openjdk:17-jre-slim # 2. 设置工作目录,后续的操作都在这个目录下进行 WORKDIR /app # 3. 将构建好的 JAR 文件复制到容器的 /app 目录下 # --chown=0:0 表示将文件所有者设置为 root (0:0),这是一个安全最佳实践 COPY --chown=0:0 target/my-java-app-0.0.1-SNAPSHOT.jar app.jar # 4. (可选) 创建一个非 root 用户来运行应用,增加安全性 # RUN addgroup --system appuser && adduser --system --group appuser # USER appuser # 5. 声明容器监听的端口(这是应用内部监听的端口,不是宿主机端口) EXPOSE 8080 # 6. 容器启动时执行的命令 # 使用 `java -jar` 来运行我们的 JAR 文件 # -Dserver.address=0.0.0.0 让应用监听所有网络接口,而不仅仅是 localhost CMD ["java", "-jar", "app.jar", "-Dserver.address=0.0.0.0"]
使用多阶段构建(推荐,用于生产)
多阶段构建可以显著减小最终镜像的大小,因为它只保留运行应用所必需的文件,而将构建工具(如 Maven/Gradle)和源代码丢弃。
# 第一阶段:构建阶段 # 这个阶段使用完整的 JDK 和 Maven/Gradle 来编译和打包项目 FROM maven:3.8.6-openjdk-17 AS builder WORKDIR /app # 复制 pom.xml 和其他配置文件,利用 Docker 的缓存机制 # 如果这些文件没变,就不会重新下载依赖 COPY pom.xml . # 如果是 Gradle 项目,使用 COPY build.gradle* . 和 settings.gradle* COPY src ./src # 运行 Maven 命令进行打包 # -DskipTests 跳过测试,加快构建速度 RUN mvn clean package -DskipTests # 第二阶段:运行阶段 # 这个阶段只运行 JRE,镜像更小、更安全 FROM openjdk:17-jre-slim WORKDIR /app # 从构建阶段复制的 JAR 文件 # 注意这里的路径:builder 是第一阶段我们起的别名,target/... 是第一阶段的路径 COPY --from=builder /app/target/my-java-app-0.0.1-SNAPSHOT.jar app.jar # 创建非 root 用户 RUN addgroup --system appuser && adduser --system --group appuser USER appuser EXPOSE 8080 CMD ["java", "-jar", "app.jar", "-Dserver.address=0.0.0.0"]
第三步:构建 Docker 镜像
打开你的终端,进入 my-java-app 项目根目录,运行以下命令:
# -t: 为镜像打一个标签,格式为 name:tag # .: 指定构建上下文的路径,这里是当前目录,Dockerfile 也在这个目录下 docker build -t my-java-app:1.0 .
构建完成后,你可以用 docker images 命令查看新创建的镜像:
REPOSITORY TAG IMAGE ID CREATED SIZE my-java-app 1.0 <image-id> <time> <size>
第四步:运行 Docker 容器
我们基于刚刚构建的镜像来运行一个容器。
# -d: 后台运行容器 # --name: 给容器起一个名字 # -p: 端口映射,格式为 <宿主机端口>:<容器端口> # 将宿主机的 8080 端口映射到容器的 8080 端口 # my-java-app:1.0: 要运行的镜像名称和标签 docker run -d --name my-app-container -p 8080:8080 my-java-app:1.0
你的 Java 应用已经在 Docker 容器中运行了!你可以通过访问 http://localhost:8080 来验证它是否正常工作。
常用管理命令:
# 查看正在运行的容器 docker ps # 停止容器 docker stop my-app-container # 启动已停止的容器 docker start my-app-container # 删除容器 docker rm my-app-container # 查看容器日志 docker logs my-app-container
Docker 镜像优化技巧
-
使用多阶段构建:如上所述,这是减小镜像体积最有效的方法。
-
选择合适的基础镜像:
openjdk:17-jre-slim或eclipse-temurin:17-jre-alpine比openjdk:17小得多。alpine版本基于 Alpine Linux,非常小,但使用musl而不是glibc库,某些 Native 库可能不兼容,需要先安装apk add --no-cache ca-certificates等依赖。
-
合并
RUN指令:将多个RUN指令合并成一个,以减少镜像层数。# 不推荐 RUN apt-get update RUN apt-get install -y curl # 推荐 RUN apt-get update && apt-get install -y curl && \ rm -rf /var/lib/apt/lists/* # 清理 apt 缓存 -
清理缓存:在
RUN指令的最后清理掉不需要的文件(如apt,yum,npm的缓存)。 -
使用
.dockerignore:避免将不必要的文件(如.git,target,node_modules)发送给 Docker 构建上下文,可以加快构建速度并减小镜像大小。
生产环境部署最佳实践
使用 .dockerignore
在项目根目录创建 .dockerignore 文件,内容如下:
# 忽略所有 Git 相关文件
.git
.gitignore
# 忽略构建产物和 IDE 文件
target/
.idea/
.vscode/
*.class
*.jar
*.war
*.zip
*.tar.gz
# 忽略日志和临时文件
logs/
tmp/
*.log
使用 Docker Compose
当你的应用需要依赖其他服务时(如数据库 Redis、消息队列 RabbitMQ),使用 Docker Compose 可以轻松管理多容器应用。
创建一个 docker-compose.yml 文件:
version: '3.8'
services:
app:
build: . # 使用当前目录下的 Dockerfile 构建镜像
container_name: my-java-app-prod
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod # 设置生产环境配置
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mydb
depends_on:
- db # 确保 db 服务先启动
networks:
- my-network
db:
image: postgres:13-alpine
container_name: my-postgres-db
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres-data:/var/lib/postgresql/data # 数据持久化
networks:
- my-network
volumes:
postgres-data:
networks:
my-network:
driver: bridge
你只需要一个命令就可以启动整个应用栈:
# 启动并后台运行所有服务 docker-compose up -d # 查看所有服务状态 docker-compose ps # 停止并删除所有服务、网络和数据卷 docker-compose down -v
数据持久化
数据库的数据不能存在容器内,因为容器被删除后数据会丢失,解决方案是使用 数据卷 或 绑定挂载。
在 docker-compose.yml 中,我们通过 volumes 定义了数据卷:
volumes: postgres-data: # 命名卷
日志管理
容器内的标准输出和标准错误会自动被 Docker 收集,你可以使用 docker logs 查看日志。
对于生产环境,建议将日志输出到标准输出/错误,然后使用 日志驱动 将它们发送到集中的日志管理系统,如 ELK Stack (Elasticsearch, Logstash, Kibana)、Splunk 或云服务商的日志服务。
在 docker-compose.yml 中可以这样配置:
services:
app:
# ...
logging:
driver: "json-file" # 默认驱动
options:
max-size: "10m"
max-file: "3"
通过以上步骤,你已经掌握了使用 Docker 部署 Java 应用的核心流程和最佳实践。
核心流程回顾:
- 准备应用:打好 JAR 包。
- 编写
Dockerfile:定义如何构建镜像,推荐使用多阶段构建。 - 构建镜像:使用
docker build命令。 - 运行容器:使用
docker run或docker-compose up命令。
最佳实践回顾:
- 安全:使用非 root 用户运行应用。
- 轻量:使用多阶段构建和精简版基础镜像。
- 可维护:使用
.dockerignore和Docker Compose。 - 可靠:为数据库等有状态服务配置数据持久化。
Docker 为 Java 应用的部署带来了革命性的变化,是现代 DevOps 和云原生应用不可或缺的工具,希望这份详细的教程对你有帮助!
