上周新来一个同事,问我要测试环境的部署文档。我发给他一份:

  1. 安装JDK 1.8
  2. 安装MySQL 8
  3. 安装Redis
  4. 配置环境变量
  5. 修改application.yaml
  6. 打包上传
  7. 启动……

他花了整整一天才跑起来。而用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:3306redis: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到服务器部署。

更多推荐