一句话定义:本文系统讲解如何使用Docker将Java SaaS应用及其依赖的MySQL、Redis等中间件进行全容器化部署,通过Dockerfile镜像构建与docker-compose容器编排,实现"一次构建,随处运行"的标准化交付。
一、引言:从"手工部署"到"一键启动"
在之前的15篇文章中,我们走完了从Linux基础到Java环境搭建、从Nginx配置到MySQL/Redis部署的全流程。这个过程你肯定深有体会------每部署一套新环境,都要重复一遍:装JDK、配MySQL、装Redis、改配置文件、启动服务...... 少说也要半天时间。
如果有一天服务器宕机了要迁移,或者要把应用从测试环境部署到生产环境,难道要重新来一遍?
Docker就是来解决这个问题的。
Docker的核心思想是**"一次构建,随处运行"** ------把应用连同它依赖的所有东西(JDK、配置文件、环境变量)打包成一个镜像 ,这个镜像可以在任何安装了Docker的服务器上直接运行成容器 。开发环境跑通了,镜像推到生产环境,行为完全一致------"在我机器上是好的"这句话将彻底成为历史。
在Java SaaS部署的全链路中(第13篇JDK → 第14篇应用部署 → 第15篇Nginx → 第16篇数据库 → 第17篇Redis → 本篇Docker → 第19篇性能调优),Docker是从"手工运维"走向"自动化运维"的分水岭。掌握了Docker,你就掌握了现代软件交付的"通用语言"。
二、Docker核心概念:镜像、容器、仓库
在动手之前,花3分钟搞清楚三个核心概念------这是理解一切Docker操作的基础。
| 概念 | 通俗理解 | 类比 |
|---|---|---|
| 镜像(Image) | 一个只读的"模板",包含了运行应用所需的一切(代码、运行时、系统工具、库) | 类似于安装光盘/ISO文件 |
| 容器(Container) | 镜像的运行实例,一个隔离的进程 | 类似于用光盘安装好的操作系统 |
| 仓库(Repository) | 存储和分发镜像的地方 | 类似于应用商店/代码仓库 |
关系 :镜像 → 运行 → 容器。你可以从仓库拉取别人做好的镜像,也可以自己用Dockerfile构建镜像。一个镜像可以启动无数个容器实例。
三、在Linux上安装Docker
3.1 安装前的系统检查
为什么这样写:Docker对操作系统内核版本有明确要求。很多人在老旧的CentOS 7上装Docker,结果因为内核版本太低(❤️.10)导致overlay2存储驱动无法使用,容器启动各种报错。先检查再安装,能避免90%的坑。
踩过的坑:
- 坑1:系统里装了旧版Docker(如docker.io)没卸载干净,与新版本冲突
- 坑2:没加当前用户到docker组,每次执行docker命令都要sudo
注意事项:
- Docker要求Linux内核版本不低于3.10
- 生产环境推荐Rocky Linux 9 / Ubuntu 22.04 LTS及以上
- 以下同时给出Rocky Linux/CentOS 和Ubuntu两套安装命令
代码块:环境检查与旧版本清理
bash
# 1. 检查内核版本(必须 >= 3.10)
uname -r
# 2. 检查操作系统版本
cat /etc/os-release
# 3. 卸载旧版本Docker(如有)
# Rocky Linux / CentOS:
sudo dnf remove -y docker docker-client docker-client-latest \
docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine
# Ubuntu / Debian:
sudo apt remove -y docker docker-engine docker.io containerd runc
执行后说明 :uname -r应输出类似5.14.0-284.el9.x86_64的内核版本。如果内核低于3.10(如CentOS 7的3.10.0),虽然能满足最低要求,但建议升级到更新的发行版以获得更好的容器支持。
3.2 安装Docker Engine(双系统适配)
为什么这样写:Docker官方提供了针对各Linux发行版的官方仓库,这是生产环境最推荐的安装方式------能自动处理依赖、方便后续升级、来源可靠。不要用系统默认仓库里的旧版本Docker。
踩过的坑:
- 坑:Rocky Linux用
dnf install docker装的是Podman而不是Docker - 坑:Ubuntu用
apt install docker.io装的是旧版,缺少新特性
注意事项:
- 以下使用Docker官方仓库安装,确保版本最新
- 如需配置国内镜像加速器以提升拉取速度,见3.3节
代码块:Rocky Linux 9 / CentOS 9 安装Docker
bash
# 1. 安装必要依赖
sudo dnf install -y dnf-plugins-core
# 2. 添加Docker官方YUM仓库
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 3. 安装Docker Engine及相关组件
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 4. 启动Docker服务并设置开机自启
sudo systemctl start docker
sudo systemctl enable docker
# 5. 验证安装
sudo docker --version
代码块:Ubuntu 22.04 / 24.04 安装Docker
bash
# 1. 更新软件包列表并安装依赖
sudo apt update
sudo apt install -y ca-certificates curl
# 2. 添加Docker官方GPG密钥
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# 3. 添加Docker官方APT仓库
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 4. 安装Docker Engine
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 5. 启动Docker服务并验证
sudo systemctl start docker
sudo systemctl enable docker
sudo docker --version
执行后说明 :安装完成后,sudo docker --version应输出类似Docker version 27.0.3的版本信息。Docker服务已启动并设置为开机自启。
3.3 配置用户权限与镜像加速(非必须但强烈推荐)
为什么这样写 :每次执行docker命令都要加sudo很烦人,把当前用户加入docker组可以省去这个麻烦。另外,在国内访问Docker Hub拉取镜像速度很慢,配置镜像加速器能大幅提升体验。
注意事项:
- 加入docker组后需要重新登录才能生效
- 镜像加速器地址请咨询你的云服务商(阿里云、腾讯云、华为云均提供免费加速器)
代码块:用户权限配置与镜像加速
bash
# 1. 将当前用户加入docker组(避免每次sudo)
sudo usermod -aG docker $USER
# ⚠️ 执行后需要退出重新登录才能生效
# 2. 配置Docker守护进程(/etc/docker/daemon.json)
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json << 'EOF'
{
"registry-mirrors": [
"https://your-mirror.aliyuncs.com" # 替换为你的镜像加速器地址
],
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2"
}
EOF
# 3. 重启Docker使配置生效
sudo systemctl daemon-reload
sudo systemctl restart docker
# 4. 验证配置
docker info | grep -A 5 "Registry Mirrors"
执行后说明 :docker info会显示当前的镜像加速器配置。如果看到你配置的加速器地址,说明生效了。storage-driver: overlay2是当前最推荐的存储驱动,性能和稳定性都很好。
四、编写Dockerfile:把Java应用变成镜像
4.1 为什么需要多阶段构建
为什么这样写 :一个Java应用的Dockerfile写得好不好,最直接的衡量标准就是镜像体积。一个未经优化的Spring Boot镜像动辄500MB-1GB,拉取慢、传输慢、浪费存储空间。而采用多阶段构建(Multi-stage Build)后,镜像可以瘦身到100MB左右。
多阶段构建的核心思想是:
- 第一阶段(构建阶段) :用包含完整JDK和Maven的"重型"镜像来编译打包代码
- 第二阶段(运行阶段) :只把打好的jar包复制到一个"轻型"镜像(如JRE slim)中运行
最终镜像里只包含运行时需要的东西------没有源码、没有Maven、没有编译工具,体积大幅缩小,安全性也更高。
踩过的坑:
- 坑1:把编译和运行放在一个阶段,镜像里塞满了Maven依赖和源码,体积1GB+
- 坑2:用了
openjdk:17(约400MB)而不是eclipse-temurin:17-jre-alpine(约80MB) - 坑3:没有写
.dockerignore,把target/、.git/等无用文件也打进了镜像
注意事项:
- 本文示例基于Spring Boot 3.x + JDK 17
- 基础镜像推荐使用
eclipse-temurin:17-jre-alpine------基于Alpine Linux(仅5MB),加上JRE后总大小约80MB - 不要使用已弃用的
openjdk官方镜像
4.2 生产级Dockerfile完整示例
代码块:多阶段构建Dockerfile(生产环境推荐)
dockerfile
# ============================================================
# 第一阶段:构建阶段(builder)
# 使用包含完整JDK和Maven的镜像来编译打包
# ============================================================
FROM maven:3.9-eclipse-temurin-17-alpine AS builder
# 设置工作目录
WORKDIR /build
# 先复制pom.xml------利用Docker层缓存,依赖不变时不重复下载
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 再复制源代码并打包
COPY src ./src
RUN mvn package -DskipTests -B
# ============================================================
# 第二阶段:运行阶段(最终镜像)
# 只复制jar包,使用轻量级JRE镜像
# ============================================================
FROM eclipse-temurin:17-jre-alpine
# 创建应用运行用户(非root,安全加固)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# 设置工作目录
WORKDIR /app
# 从构建阶段复制jar包
COPY --from=builder /build/target/*.jar ./app.jar
# 创建挂载点(用于日志和配置的外部挂载)
VOLUME /app/logs
VOLUME /app/config
# 暴露应用端口(根据你的应用调整)
EXPOSE 8080
# 切换到非root用户
USER appuser
# JVM参数优化(容器环境专用)
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/app/logs/heapdump.hprof \
-Djava.security.egd=file:/dev/./urandom"
# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]
执行后说明:这个Dockerfile有几个关键设计:
-
分层缓存优化 :先复制
pom.xml并下载依赖(dependency:go-offline),再复制源码。只要pom.xml不变,Maven依赖就不会重新下载,构建速度提升80%。 -
多阶段构建 :构建阶段用Maven镜像(约500MB),最终运行阶段只用JRE Alpine镜像(约80MB),最终镜像体积约90MB。
-
非root运行 :
USER appuser让容器内的Java进程不以root身份运行,符合安全最佳实践。 -
容器感知JVM参数 :
-XX:+UseContainerSupport(JDK 10+默认开启)让JVM能识别容器的内存限制。-XX:MaxRAMPercentage=75.0表示JVM堆内存最多占容器内存的75%------如果容器内存限制为2GB,堆最大为1.5GB,留有500MB给元空间和系统开销。 -
VOLUME声明 :声明
/app/logs和/app/config为挂载点,方便在宿主机上持久化日志和覆盖配置文件。
4.3 编写.dockerignore------别把"垃圾"打包进镜像
为什么这样写 :docker build默认会把当前目录下所有文件发送给Docker守护进程来构建镜像。如果不加限制,target/(几百MB的编译产物)、.git/(整个仓库历史)、node_modules/等都会被塞进构建上下文,拖慢构建速度,还可能泄露源码。
代码块:.dockerignore文件
dockerignore
# Git相关
.git/
.gitignore
# 构建产物
target/
*.jar
*.war
*.log
# IDE相关
.idea/
*.iml
.vscode/
.settings/
.classpath
.project
# 系统文件
.DS_Store
Thumbs.db
# 容器相关(避免递归)
Dockerfile
.dockerignore
docker-compose*.yml
# 其他
*.md
README*
执行后说明 :.dockerignore的语法和.gitignore完全一样。配置好后,docker build只会把必要的源码和pom.xml发送给Docker守护进程,构建速度和安全性都得到保障。
五、构建镜像与运行容器
5.1 构建镜像
为什么这样写 :Dockerfile写好了,现在要把它"变成"一个真正的镜像。docker build命令就是做这个的。
踩过的坑:
- 坑:忘了加
-t参数给镜像打标签,构建出来的镜像叫<none>,根本认不出来 - 坑:在项目根目录执行
docker build .时,没有.dockerignore,构建上下文巨大
注意事项:
-t参数给镜像起名,格式为仓库名/镜像名:标签- 末尾的
.表示构建上下文为当前目录
代码块:构建镜像
bash
# 1. 在项目根目录执行构建
cd /path/to/your-java-project
# 2. 构建镜像(给镜像打上标签)
docker build -t myapp:1.0.0 .
# 3. 查看已构建的镜像
docker images | grep myapp
# 4. 查看镜像分层历史(理解镜像的构成)
docker history myapp:1.0.0
执行后说明 :docker images应显示myapp镜像,大小约90MB。docker history会展示镜像的每一层------你会看到第一层是Alpine基础镜像(约5MB),然后是JRE层,最后是app.jar层。每一层都是一个只读文件系统,层与层之间复用,这就是Docker镜像高效的原因。
5.2 运行容器
为什么这样写:镜像构建好了,现在要把它"跑起来"------这就是容器。
踩过的坑:
- 坑:没映射端口(
-p),容器里的8080端口外界访问不到 - 坑:没设置
--restart策略,容器崩溃后不会自动重启 - 坑:容器内存限制没设,JVM可能吃光宿主机内存
代码块:运行容器
bash
# 1. 基本运行(前台运行,用于调试)
docker run -p 8080:8080 myapp:1.0.0
# 2. 后台运行 + 重启策略 + 资源限制(生产环境推荐)
docker run -d \
--name myapp \
--restart=unless-stopped \
-p 8080:8080 \
-m 2g \
--memory-reservation 1.5g \
-e "SPRING_PROFILES_ACTIVE=prod" \
-v /data/myapp/logs:/app/logs \
-v /data/myapp/config:/app/config \
myapp:1.0.0
# 3. 查看容器运行状态
docker ps
# 4. 查看容器日志
docker logs -f myapp
# 5. 进入容器内部(调试用)
docker exec -it myapp sh
执行后说明:
-d:后台运行(detach模式)--name:给容器起名,方便后续管理--restart=unless-stopped:容器意外退出时自动重启,除非手动停止-p 8080:8080:将宿主机的8080端口映射到容器的8080端口-m 2g:限制容器最大使用内存为2GB--memory-reservation 1.5g:软性内存预留-e:设置环境变量,这里激活了Spring Boot的prod配置文件-v:挂载卷,将宿主机目录映射到容器内,实现日志持久化和配置外部化
关键提示 :-m 2g和JVM的-XX:MaxRAMPercentage=75.0要配合使用------容器内存2GB,JVM堆最大1.5GB,留500MB给JVM元空间和系统开销。
六、docker-compose:一键启动整个SaaS应用栈
6.1 为什么需要docker-compose
为什么这样写 :到目前为止,我们只容器化了Java应用本身。但一个完整的SaaS应用还需要MySQL、Redis、Nginx等中间件。如果每个服务都手动docker run,不但命令冗长,还要处理服务之间的网络连通、启动顺序等问题。
docker-compose 就是来解决这个问题的------它用一个docker-compose.yml文件定义所有服务,然后一条命令启动全部。
踩过的坑:
- 坑:服务之间的依赖关系没配置(
depends_on),应用启动时数据库还没就绪 - 坑:没配置健康检查,容器显示"running"但服务实际不可用
6.2 docker-compose.yml完整示例
代码块:docker-compose.yml(Java应用 + MySQL + Redis)
yaml
version: '3.8'
services:
# ============ MySQL 数据库 ============
mysql:
image: mysql:8.0
container_name: saas-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root_strong_passwd
MYSQL_DATABASE: saas_db
MYSQL_USER: saas_user
MYSQL_PASSWORD: saas_passwd
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --max_connections=500
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- saas-network
# ============ Redis 缓存 ============
redis:
image: redis:7.0-alpine
container_name: saas-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes --requirepass redis_strong_passwd
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- saas-network
# ============ Java 应用 ============
app:
build:
context: .
dockerfile: Dockerfile
image: myapp:1.0.0
container_name: saas-app
restart: unless-stopped
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: docker
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/saas_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: saas_user
SPRING_DATASOURCE_PASSWORD: saas_passwd
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: 6379
SPRING_REDIS_PASSWORD: redis_strong_passwd
volumes:
- app_logs:/app/logs
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- saas-network
# ============ 卷定义 ============
volumes:
mysql_data:
redis_data:
app_logs:
# ============ 网络定义 ============
networks:
saas-network:
driver: bridge
执行后说明:这个docker-compose.yml定义了三个服务:
- mysql:使用MySQL 8.0镜像,配置了初始数据库、用户、密码,并做了健康检查
- redis:使用Redis 7.0 Alpine镜像(体积更小),开启了AOF持久化和密码认证
- app:使用我们刚才构建的Java应用镜像,通过环境变量注入数据库和Redis的连接信息
关键设计:
depends_on配合condition: service_healthy确保Java应用只在MySQL和Redis都健康就绪后才启动,避免启动时连接失败- 所有服务在同一个自定义网络
saas-network中,可以通过服务名 互相访问(如mysql:3306),无需IP地址 - 数据卷(volumes)将数据库和Redis的数据持久化在宿主机上,容器删除后数据不丢失
6.3 一键启动与运维管理
代码块:docker-compose常用命令
bash
# 1. 启动所有服务(后台运行)
docker-compose up -d
# 2. 查看所有服务状态
docker-compose ps
# 3. 查看某个服务的日志
docker-compose logs -f app
# 4. 停止所有服务
docker-compose down
# 5. 停止并删除数据卷(⚠️ 会丢失数据!)
docker-compose down -v
# 6. 重新构建并启动(代码更新后使用)
docker-compose up -d --build
# 7. 进入某个服务的容器内部
docker-compose exec app sh
docker-compose exec mysql bash
执行后说明 :执行docker-compose up -d后,三个容器会依次启动。用docker-compose ps查看状态,当所有服务的STATUS都显示Up时,访问http://你的服务器IP:8080即可访问应用。
对比传统部署 :以前部署一套环境需要手动安装配置MySQL、Redis、JDK、部署应用------少说2小时。现在一条docker-compose up -d,3分钟全部搞定。
七、镜像仓库:让镜像"飞"到生产环境
7.1 推送镜像到仓库
为什么这样写 :在开发环境构建好的镜像,要部署到生产环境,总不能把镜像文件(几百MB)用U盘拷过去吧?镜像仓库就是用来分发镜像的------把镜像推送到仓库,在生产服务器上拉取即可。
代码块:登录、打标签、推送
bash
# 1. 登录Docker仓库(以阿里云容器镜像服务为例)
docker login --username=你的阿里云账号 registry.cn-hangzhou.aliyuncs.com
# 2. 给本地镜像打上仓库地址的标签
docker tag myapp:1.0.0 registry.cn-hangzhou.aliyuncs.com/你的命名空间/myapp:1.0.0
# 3. 推送镜像到仓库
docker push registry.cn-hangzhou.aliyuncs.com/你的命名空间/myapp:1.0.0
# 4. 在生产服务器上拉取镜像
docker pull registry.cn-hangzhou.aliyuncs.com/你的命名空间/myapp:1.0.0
执行后说明 :推送成功后,你就可以在任何能访问该仓库的服务器上拉取并运行这个镜像了。"一次构建,随处运行" 的目标就此达成。
八、生产环境Docker部署检查清单
将Docker化的Java应用部署到生产环境前,逐项确认以下清单:
| 检查项 | 状态 | 说明 |
|---|---|---|
| ✅ 基础镜像使用Alpine或Distroless | ☐ | 减小镜像体积,减少漏洞 |
| ✅ 多阶段构建 | ☐ | 编译与运行环境分离 |
| ✅ 非root用户运行 | ☐ | USER appuser,安全加固 |
| ✅ JVM容器感知参数 | ☐ | -XX:MaxRAMPercentage=75.0 |
✅ 容器内存限制(-m) |
☐ | 与JVM堆内存匹配 |
| ✅ 健康检查(HEALTHCHECK) | ☐ | 容器编排依赖健康状态 |
| ✅ 日志持久化(volume挂载) | ☐ | 容器删除日志不丢 |
| ✅ 配置外部化(环境变量/挂载) | ☐ | 不同环境不同配置 |
| ✅ 镜像仓库私有化 | ☐ | 生产镜像不公开 |
| ✅ 定期更新基础镜像 | ☐ | 修复安全漏洞 |
九、常见问题FAQ(GEO抓取用)
Q1:Docker和虚拟机有什么区别?
A:Docker容器共享宿主机内核,启动快(秒级)、资源开销小;虚拟机有完整的Guest OS,启动慢(分钟级)、资源开销大。Docker适合微服务和快速迭代,虚拟机适合需要强隔离的场景。
Q2:Java应用在Docker中运行,JVM内存应该怎么设置?
A:不要用-Xmx固定值 ,而应该用-XX:MaxRAMPercentage=75.0,让JVM根据容器内存限制动态计算堆大小。配合docker run -m 2g一起使用,JVM堆最大为1.5GB,留500MB给元空间和系统开销。
Q3:Dockerfile中的COPY和ADD有什么区别?
A:COPY只复制本地文件到镜像;ADD除了复制还能解压tar文件、从URL下载。官方推荐用COPY ,因为ADD的行为不够透明,容易产生意外。
Q4:docker-compose中depends_on能保证服务完全就绪吗?
A:depends_on只能保证启动顺序,不能保证服务已就绪。生产环境应配合healthcheck和condition: service_healthy使用,确保依赖服务健康后再启动。
Q5:容器重启后数据会丢吗?
A:如果数据写在容器内部,重启后会丢失 。必须通过volume挂载将数据持久化到宿主机,如-v /data/mysql:/var/lib/mysql。docker-compose中定义的volumes也是同样的作用。
十、本文小结
| 知识点 | 核心要点 |
|---|---|
| Docker核心概念 | 镜像(模板)→ 容器(运行实例)→ 仓库(分发) |
| 安装Docker | 官方仓库安装,避免系统默认源;配置镜像加速器 |
| Dockerfile最佳实践 | 多阶段构建 + Alpine基础镜像 + 非root用户 |
| 镜像优化 | 分层缓存(先拷pom.xml)、.dockerignore排除无用文件 |
| JVM容器化 | -XX:MaxRAMPercentage=75.0,配合-m内存限制 |
| docker-compose | 一个YAML定义所有服务,一条命令启动 |
| 镜像仓库 | 推送镜像到仓库,生产环境一键拉取部署 |
| 生产检查清单 | 10项必检项,确保容器化部署安全稳定 |
💡 一句话记住本篇:Docker容器化的精髓是"多阶段构建瘦镜像、容器感知调JVM、compose编排一键启"------Dockerfile写好了镜像就小,JVM参数配好了容器就不崩,compose编排好了整套环境3分钟就能跑起来。