Java项目还在手动部署?Docker容器化从0到1,我踩过的坑全告诉你
上周新来一个同事,问我要测试环境的部署文档。我发给他一份:
- 安装JDK 1.8
- 安装MySQL 8
- 安装Redis
- 配置环境变量
- 修改application.yaml
- 打包上传
- 启动……
他花了整整一天才跑起来。而用Docker容器化部署,同样的项目,5分钟。更关键的是——再也不会出现"我本地能跑啊"这种灵异事件。
今天这篇文章,从Docker基础到Java项目容器化实战,把整个过程和踩过的坑全讲清楚。
一、为什么Java项目要容器化?
传统部署的痛
| 痛点 | 场景 |
|---|---|
| 环境不一致 | “我本地能跑啊” → 测试环境JDK版本不同 |
| 依赖冲突 | 服务器上跑3个项目,MySQL版本各不相同 |
| 部署慢 | 新服务器要从零装JDK、MySQL、Redis…… |
| 扩容难 | 流量来了,手动加服务器+部署,黄花菜都凉了 |
| 回滚难 | 发版出问题,回滚靠人肉,容易出错 |
容器化之后
| 优势 | 效果 |
|---|---|
| 环境一致性 | 镜像打包,到处运行 |
| 依赖隔离 | 每个容器独立,互不干扰 |
| 一键部署 | docker-compose up -d,5分钟整套环境 |
| 弹性扩容 | K8s一个命令扩容 |
| 快速回滚 | 镜像版本回退,秒级恢复 |
二、Docker核心概念
只需要记住3个核心概念:
镜像(Image)──→ 容器(Container)──→ 仓库(Registry)
类比:类 对象 Maven仓库
- 镜像:只读模板,包含运行环境和代码(类比Java中的Class)
- 容器:镜像的运行实例(类比new出来的对象)
- 仓库:存放镜像的地方(类比Maven中央仓库)
常用命令速查
# 镜像相关
docker build -t myapp:1.0 . # 构建镜像
docker images # 查看镜像列表
docker pull openjdk:8 # 拉取镜像
docker rmi myapp:1.0 # 删除镜像
# 容器相关
docker run -d -p 8080:8080 myapp:1.0 # 启动容器
docker ps # 查看运行中的容器
docker logs container_id # 查看日志
docker exec -it container_id /bin/sh # 进入容器
docker stop container_id # 停止容器
docker rm container_id # 删除容器
三、实战:Spring Boot项目容器化
第1步:项目打包
pom.xml 确认打包方式:
<packaging>jar</packaging>
<build>
<finalName>easy-platform</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
打包:
mvn clean package -DskipTests
生成 target/easy-platform.jar
第2步:编写Dockerfile
这是最核心的一步。很多教程的Dockerfile有坑,下面是我线上用了2年的版本:
# ===== 构建阶段 =====
FROM maven:3.8-openjdk-8 AS builder
WORKDIR /build
COPY pom.xml .
# 先下载依赖(利用Docker缓存层,依赖不变时不重新下载)
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# ===== 运行阶段 =====
FROM openjdk:8-jre-slim
# 安装必要工具(排查问题用)
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# 时区设置(不设置的话,日志时间差8小时)
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /app
# 从构建阶段复制jar包
COPY --from=builder /build/target/easy-platform.jar app.jar
# 创建日志目录
RUN mkdir -p /app/logs
# 暴露端口
EXPOSE 8080
# JVM参数(根据服务器配置调整)
ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/"
# 非root用户运行(安全最佳实践)
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app
USER appuser
# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
这份Dockerfile的6个关键设计:
| 设计 | 作用 |
|---|---|
| 多阶段构建 | 构建环境和运行环境分离,镜像体积小 |
| 先COPY pom.xml | 利用Docker缓存层,依赖不变时不重新下载 |
| 设置时区 | 避免日志时间差8小时 |
| JVM参数可配置 | 通过环境变量覆盖,不同环境不同配置 |
| 非root用户 | 安全最佳实践,K8s环境必须 |
| HeapDump | OOM时自动dump,线上排查必备 |
第3步:构建镜像
docker build -t easy-platform:1.0 .
查看镜像大小:
docker images | grep easy-platform
# 多阶段构建后约 200-300MB(单阶段可能 500MB+)
第4步:启动容器
docker run -d \
--name easy-platform \
-p 8080:8080 \
-v /data/logs:/app/logs \
-e JAVA_OPTS="-Xms1g -Xmx1g -XX:+UseG1GC" \
-e SPRING_PROFILES_ACTIVE=product \
easy-platform:1.0
参数解读:
| 参数 | 作用 |
|---|---|
-d |
后台运行 |
-p 8080:8080 |
端口映射 |
-v /data/logs:/app/logs |
日志挂载到宿主机,容器删除不丢日志 |
-e JAVA_OPTS=... |
覆盖JVM参数 |
-e SPRING_PROFILES_ACTIVE=product |
指定Spring配置环境 |
四、Docker Compose:一键部署整套环境
实际项目中,Java应用要依赖MySQL和Redis。Docker Compose一条命令启动全部:
docker-compose.yml
version: '3.8'
services:
# ===== MySQL 8 =====
mysql:
image: mysql:8.0.31
container_name: easy-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: easy_platform
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./init-sql:/docker-entrypoint-initdb.d # 初始化SQL
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
# ===== Redis =====
redis:
image: redis:7-alpine
container_name: easy-redis
restart: always
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
# ===== Java应用 =====
app:
build:
context: .
dockerfile: Dockerfile
container_name: easy-platform
restart: always
ports:
- "8080:8080"
environment:
JAVA_OPTS: "-Xms1g -Xmx1g -XX:+UseG1GC"
SPRING_PROFILES_ACTIVE: product
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/easy_platform?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD}
SPRING_REDIS_HOST: redis
SPRING_REDIS_PASSWORD: ${REDIS_PASSWORD}
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- app-logs:/app/logs
volumes:
mysql-data:
redis-data:
app-logs:
.env文件(敏感信息不要硬编码)
MYSQL_ROOT_PASSWORD=your_strong_password
REDIS_PASSWORD=your_redis_password
一键启动
# 启动全部服务
docker-compose up -d
# 查看状态
docker-compose ps
# 查看日志
docker-compose logs -f app
# 停止全部
docker-compose down
# 停止并删除数据卷(慎用!)
docker-compose down -v
关键设计说明:
| 设计 | 为什么 |
|---|---|
| healthcheck | 应用等MySQL/Redis就绪后再启动,避免连接失败 |
| depends_on + condition | 确保依赖服务健康后才启动应用 |
| 服务名作为主机名 | mysql:3306、redis:6379,Docker内部DNS自动解析 |
| .env管理密码 | 敏感信息不入镜像、不入代码仓库 |
| volumes | 数据持久化,容器重建不丢数据 |
五、我踩过的5个坑
坑1:时区不对,日志时间差8小时
现象:容器里日志时间比北京时间少8小时。
原因:Docker默认时区是UTC。
修复:Dockerfile中设置时区(已在上面模板中包含):
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
坑2:应用启动比MySQL快,连接失败
现象:应用启动报 Communications link failure,手动重启就好了。
原因:应用启动时MySQL还没准备好。
修复:docker-compose中使用 healthcheck + depends_on(已在上面模板中包含)。
Spring Boot层面也可以加重试:
spring:
datasource:
hikari:
connection-timeout: 30000
maximum-pool-size: 20
坑3:容器内OOM被系统杀掉,没有HeapDump
现象:容器直接消失,docker ps 看不到了。
原因:容器内存超限,被Docker/系统直接kill。
修复:
# 限制容器内存 + JVM堆不超过容器内存
docker run -m 2g \
-e JAVA_OPTS="-Xms1g -Xmx1g" \
easy-platform:1.0
⚠️ 铁律:JVM最大堆内存必须小于容器内存限制,留出给元空间、线程栈、堆外内存等。
容器限制2g → -Xmx最多1.5g
坑4:日志存在容器里,容器删了日志也没了
修复:挂载到宿主机
-v /data/logs:/app/logs
坑5:镜像体积太大,推送慢
修复:多阶段构建 + 用jre镜像代替jdk镜像
# ❌ 500MB+
FROM openjdk:8
# ✅ 200MB左右
FROM openjdk:8-jre-slim
# ✅✅ 多阶段构建,最终200-300MB
FROM maven:3.8-openjdk-8 AS builder
# ... 构建阶段
FROM openjdk:8-jre-slim
# ... 运行阶段
六、镜像推送到私有仓库
生产环境不可能每台服务器都本地build,需要推送到私有仓库:
# 登录私有仓库
docker login registry.your-company.com
# 打tag
docker tag easy-platform:1.0 registry.your-company.com/easy-platform:1.0
# 推送
docker push registry.your-company.com/easy-platform:1.0
# 服务器上拉取运行
docker pull registry.your-company.com/easy-platform:1.0
docker run -d -p 8080:8080 registry.your-company.com/easy-platform:1.0
七、CI/CD自动构建
让构建过程自动化,提交代码 → 自动打镜像 → 自动推送:
// Jenkinsfile
pipeline {
agent any
environment {
IMAGE_NAME = 'registry.your-company.com/easy-platform'
IMAGE_TAG = "${env.BUILD_NUMBER}"
}
stages {
stage('构建镜像') {
steps {
sh 'docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .'
}
}
stage('推送镜像') {
steps {
sh 'docker push ${IMAGE_NAME}:${IMAGE_TAG}'
}
}
stage('部署') {
steps {
sh '''
ssh deploy@prod-server \
"docker pull ${IMAGE_NAME}:${IMAGE_TAG} && \
docker stop easy-platform || true && \
docker rm easy-platform || true && \
docker run -d --name easy-platform \
-p 8080:8080 \
-v /data/logs:/app/logs \
${IMAGE_NAME}:${IMAGE_TAG}"
'''
}
}
}
post {
always {
sh 'docker rmi ${IMAGE_NAME}:${IMAGE_TAG} || true'
}
}
}
一图总结
Java项目容器化部署全景图
1. 编写Dockerfile
→ 多阶段构建(maven build + jre run)
→ 设时区、非root用户、HeapDump
2. Docker Compose编排
→ MySQL 8 + Redis 7 + Spring Boot
→ healthcheck + depends_on确保启动顺序
→ .env管理敏感信息
3. 私有仓库推送
→ tag + push + pull
4. CI/CD自动化
→ Jenkins: build → push → deploy
⚠️ 5个必避坑:
时区 / 启动顺序 / OOM / 日志持久化 / 镜像体积
🔑 核心铁律:
JVM堆内存 < 容器内存限制(留1/3给非堆)
面试速答
面试官:说一下你们项目是怎么容器化部署的?
答:我们项目用Docker容器化部署。首先编写多阶段Dockerfile,第一阶段用maven镜像编译打包,第二阶段用jre-slim镜像运行,这样镜像体积小、安全性高。Dockerfile里会设置时区、配置HeapDump、用非root用户运行。然后用Docker Compose编排MySQL、Redis和应用三个服务,通过healthcheck确保MySQL和Redis就绪后再启动应用。敏感信息用.env文件管理,不入代码仓库。CI/CD通过Jenkins自动构建镜像、推送到私有仓库、SSH到服务器部署。
更多推荐

所有评论(0)