目录
3.1. 创建基础 Dockerfile (非最终构建产物)
3.2. 创建多阶段构建 Dockerfile (最终构建产物)并构建镜像
4.1. 创建JDK切换信息显示脚本 docker-use-jdk(可选)
4.2. 创建Maven切换信息显示脚本 docker-use-maven(可选)
一、Docker 构建 JDK 和 Maven 的三种方式
1. 一起构建(统一镜像)
将所有 JDK 和 Maven 版本打包到 同一个镜像 中,通过环境变量或脚本动态切换版本。Dockerfile 例如:
# 基础层(所有版本共享)
FROM debian:11 AS base
RUN apt-get update && apt-get install -y curl
# 多版本安装层
FROM base AS jdk-maven
ARG JDK_VERSION=11
ARG MAVEN_VERSION=3.8.6
RUN curl -sL "https://jdk.example.com/jdk${JDK_VERSION}.tar.gz" | tar -xz
RUN curl -sL "https://maven.example.com/maven-${MAVEN_VERSION}.tar.gz" | tar -xz
ENV PATH="/jdk${JDK_VERSION}/bin:/maven-${MAVEN_VERSION}/bin:${PATH}"
优点
减少重复层:共享基础层(如操作系统、公共依赖),减少镜像总体积。
单镜像多用途:通过 docker run --env JDK_VERSION=17
动态指定版本,适合需要频繁切换版本的环境(如 CI/CD)。
简化标签管理:使用统一镜像标签(如 myimage:latest
),通过参数化配置版本。
缺点
镜像臃肿:包含多个 JDK/Maven 版本会增加镜像体积(例如同时安装 JDK8/11/17)。
潜在冲突:不同版本的环境变量或路径可能冲突(需严格隔离路径)。
更新复杂:更新某个版本需重建整个镜像。
适用场景:需要快速切换版本的本地开发环境;资源有限且镜像存储成本敏感的场景。
2. 分开构建(独立镜像)
为每个 JDK 和 Maven 版本组合构建 独立镜像,例如:myimage:jdk8-maven3.8.6;myimage:jdk11-maven3.8.6;myimage:jdk17-maven3.9.6
优点
镜像轻量化:每个镜像仅包含必要版本,体积更小。
隔离性更好:版本之间无路径或依赖冲突。
更新灵活:更新单个版本只需重建对应镜像。
缺点
存储冗余:不同镜像无法共享基础层(如操作系统层重复占用空间)。
管理成本高:需维护多个 Dockerfile 或矩阵构建逻辑。
标签爆炸:版本组合较多时,标签管理复杂(如 jdkX-mavenY
)。
适用场景:生产环境需要严格版本隔离;需要精准控制镜像内容的场景(如安全合规)。
3. 混合策略(推荐)
结合两者的优点,使用 多阶段构建 和 镜像分层优化:
步骤
基础镜像:构建共享的操作系统层和工具层。Dockerfile 如下:
版本镜像:为每个 JDK 和 Maven 版本构建独立子镜像。Dockerfile 如下:
按需组合:通过 CI/CD 工具(如 GitHub Actions)生成版本矩阵,yaml如下:
jobs:
build:
strategy:
matrix:
jdk: [8, 11, 17]
maven: [3.8.6, 3.9.6]
steps:
- name: Build image
run: |
docker build --build-arg JDK=${{ matrix.jdk }} --build-arg MAVEN=${{ matrix.maven }} -t myimage:jdk${{ matrix.jdk }}-maven${{ matrix.maven }} .
优势
存储优化:共享基础层,减少冗余。
灵活更新:独立重建特定版本。
生产友好:精准控制镜像内容。
4. 总结
选择策略时,优先考虑 生产需求 和 资源限制。大多数团队推荐 混合策略 以平衡效率和维护成本。
策略 | 存储效率 | 构建速度 | 维护成本 | 适用场景 |
---|---|---|---|---|
一起构建 | 低 | 快 | 低 | 本地开发、快速测试 |
分开构建 | 高 | 慢 | 高 | 生产环境、严格隔离 |
混合策略 | 中 | 中 | 中 | 平衡存储与灵活性 |
二、Docker 混合策略构建 JDK 和 Maven
1. 检查 Docker 环境和用户权限
确保 Docker 已正确安装,并确保用户在 docker 组中,命令如下:
# 验证 Docker 安装
docker --version
# 如果未安装,可以使用以下命令安装
apt-get update
apt-get install -y docker.io
systemctl enable --now docker
# 检查当前用户是否在 docker 组中
groups
# 如果不在,需要请管理员将您添加到 docker 组
# sudo usermod -aG docker 您的用户名
# 添加后需要重新登录才能生效
笔者已安装,效果图如下:
2. 创建工作目录结构
目录结构如下:
# 个人开发环境 目录
~/dev/
├── jdk/
│ ├── jdk-8
│ ├── jdk-11
│ └── jdk-17
├── maven/
│ ├── maven-3.6.3
│ ├── maven-3.8.6
│ └── maven-3.9.3
└── docker/ # Docker相关配置
# 系统级 目录
/opt
├── jdk/
└── maven/
命令如下:
# 以app用户登录执行
mkdir -p ~/dev/jdk/{jdk-8,jdk-11,jdk-17}
mkdir -p ~/dev/maven/{maven-3.6.3,maven-3.8.6,maven-3.9.3}
mkdir -p ~/dev/docker
sudo mkdir -p /opt/jdk /opt/maven
sudo chown app:app /opt/jdk /opt/maven
# 权限 700 表示:所有者(app):可读、可写、可执行;用户组和其他人:无权限
sudo chmod 700 /opt/jdk /opt/maven
3. 创建 Dockerfile 并构建镜像
在~/dev
目录下创建 Dockerfile,进入目录命令如下:
cd ~/dev/docker
3.1. 创建基础 Dockerfile (非最终构建产物)
先构建基本镜像(JDK、Maven各版本),基于基本镜像构建应用镜像,每个镜像独立维护和更新
优点:层次清晰,便于维护;可以复用基础镜像;便于版本管理
缺点:存储空间占用大;构建过程冗长;镜像之间存在重复内容
示例:创建基础 JDK、Maven 镜像 Dockerfile 文件,命令如下:
cat > Dockerfile.jdk8 << 'EOF'
FROM eclipse-temurin:8-jdk as jdk8
WORKDIR /opt/java
CMD ["java", "-version"]
EOF
cat > Dockerfile.jdk11 << 'EOF'
FROM eclipse-temurin:11-jdk as jdk11
WORKDIR /opt/java
CMD ["java", "-version"]
EOF
cat > Dockerfile.jdk17 << 'EOF'
FROM eclipse-temurin:17-jdk as jdk17
WORKDIR /opt/java
CMD ["java", "-version"]
EOF
cat > Dockerfile.maven363 << 'EOF'
FROM maven:3.6.3-jdk-8 as maven363
WORKDIR /opt/maven
CMD ["mvn", "-version"]
EOF
cat > Dockerfile.maven386 << 'EOF'
FROM maven:3.8.6-eclipse-temurin-11 as maven386
WORKDIR /opt/maven
CMD ["mvn", "-version"]
EOF
cat > Dockerfile.maven393 << 'EOF'
FROM maven:3.9.3-eclipse-temurin-17 as maven393
WORKDIR /opt/maven
CMD ["mvn", "-version"]
EOF
效果图如下:
构建基础 JDK、Maven 镜像,命令如下:
# 构建基本镜像
docker build -t dev/jdk8 -f Dockerfile.jdk8 .
docker build -t dev/jdk11 -f Dockerfile.jdk11 .
docker build -t dev/jdk17 -f Dockerfile.jdk17 .
docker build -t dev/maven363 -f Dockerfile.maven363 .
docker build -t dev/maven386 -f Dockerfile.maven386 .
docker build -t dev/maven393 -f Dockerfile.maven393 .
笔者以jdk8、maven363 为例,正常构建效果图分别如下:
3.2. 创建多阶段构建 Dockerfile (最终构建产物)并构建镜像
创建一个综合多阶段构建的 Dockerfile.combined,Dockerfile.combined 内容如下:
# JDK stages
FROM eclipse-temurin:8-jdk as jdk8
FROM eclipse-temurin:11-jdk as jdk11
FROM eclipse-temurin:17-jdk as jdk17
# Maven stages
FROM maven:3.6.3-jdk-8 as maven363
FROM maven:3.8.6-eclipse-temurin-11 as maven386
FROM maven:3.9.3-eclipse-temurin-17 as maven393
# 为 maven 添加检查和路径输出
RUN which mvn | xargs dirname | xargs dirname > /maven-path.txt
# Final minimal stage for execution
FROM debian:bullseye-slim as runtime
ARG USER_HOME=/home/app
# Create directories mirroring host structure and set up user
RUN mkdir -p ${USER_HOME}/dev/jdk ${USER_HOME}/dev/maven ${USER_HOME}/bin && \
groupadd --gid 1000 app && \
useradd --uid 1000 --gid 1000 --shell /bin/bash --create-home app && \
chown -R app:app ${USER_HOME}
# Copy JDKs
COPY --from=jdk8 /opt/java ${USER_HOME}/dev/jdk/jdk-8
COPY --from=jdk11 /opt/java ${USER_HOME}/dev/jdk/jdk-11
COPY --from=jdk17 /opt/java ${USER_HOME}/dev/jdk/jdk-17
# Copy Maven
COPY --from=maven363 /usr/share/maven ${USER_HOME}/dev/maven/maven-3.6.3
COPY --from=maven386 /usr/share/maven ${USER_HOME}/dev/maven/maven-3.8.6
COPY --from=maven393 /usr/share/maven ${USER_HOME}/dev/maven/maven-3.9.3
# Set permissions for dev directory
RUN chmod -R 755 ${USER_HOME}/dev
# Create switch script with proper permissions
RUN echo '#!/bin/bash' > ${USER_HOME}/bin/switch.sh && \
echo '' >> ${USER_HOME}/bin/switch.sh && \
echo 'switch_jdk() {' >> ${USER_HOME}/bin/switch.sh && \
echo ' local version=$1' >> ${USER_HOME}/bin/switch.sh && \
echo ' local jdk_path="${HOME}/dev/jdk/jdk-${version}/openjdk"' >> ${USER_HOME}/bin/switch.sh && \
echo '' >> ${USER_HOME}/bin/switch.sh && \
echo ' if [ ! -d "${jdk_path}" ]; then' >> ${USER_HOME}/bin/switch.sh && \
echo ' echo "错误: JDK ${version} 不存在于 ${jdk_path}" >&2' >> ${USER_HOME}/bin/switch.sh && \
echo ' return 1' >> ${USER_HOME}/bin/switch.sh && \
echo ' fi' >> ${USER_HOME}/bin/switch.sh && \
echo '' >> ${USER_HOME}/bin/switch.sh && \
echo ' export JAVA_HOME="${jdk_path}"' >> ${USER_HOME}/bin/switch.sh && \
echo ' local new_path=$(echo $PATH | sed -E "s|:[^:]*jdk[^:]*bin::|:|g")' >> ${USER_HOME}/bin/switch.sh && \
echo ' export PATH="${JAVA_HOME}/bin:${new_path}"' >> ${USER_HOME}/bin/switch.sh && \
echo ' echo "已切换到 JDK ${version}: ${jdk_path}"' >> ${USER_HOME}/bin/switch.sh && \
echo '}' >> ${USER_HOME}/bin/switch.sh && \
echo '' >> ${USER_HOME}/bin/switch.sh && \
echo 'switch_maven() {' >> ${USER_HOME}/bin/switch.sh && \
echo ' local version=$1' >> ${USER_HOME}/bin/switch.sh && \
echo ' local maven_path="${HOME}/dev/maven/maven-${version}"' >> ${USER_HOME}/bin/switch.sh && \
echo '' >> ${USER_HOME}/bin/switch.sh && \
echo ' if [ ! -d "${maven_path}" ]; then' >> ${USER_HOME}/bin/switch.sh && \
echo ' echo "错误: Maven ${version} 不存在于 ${maven_path}" >&2' >> ${USER_HOME}/bin/switch.sh && \
echo ' return 1' >> ${USER_HOME}/bin/switch.sh && \
echo ' fi' >> ${USER_HOME}/bin/switch.sh && \
echo '' >> ${USER_HOME}/bin/switch.sh && \
echo ' export MAVEN_HOME="${maven_path}"' >> ${USER_HOME}/bin/switch.sh && \
echo ' local new_path=$(echo $PATH | sed -E "s|:[^:]*maven[^:]*bin::|:|g")' >> ${USER_HOME}/bin/switch.sh && \
echo ' export PATH="${MAVEN_HOME}/bin:${new_path}"' >> ${USER_HOME}/bin/switch.sh && \
echo ' echo "已切换到 Maven ${version}: ${maven_path}"' >> ${USER_HOME}/bin/switch.sh && \
echo '}' >> ${USER_HOME}/bin/switch.sh && \
chmod +x ${USER_HOME}/bin/switch.sh && \
chown app:app ${USER_HOME}/bin/switch.sh
# Create init-env script with proper permissions
RUN echo '#!/bin/bash' > ${USER_HOME}/bin/init-env.sh && \
echo 'source ${HOME}/bin/switch.sh' >> ${USER_HOME}/bin/init-env.sh && \
echo 'if [ ! -z "$INIT_JDK" ]; then' >> ${USER_HOME}/bin/init-env.sh && \
echo ' switch_jdk $INIT_JDK' >> ${USER_HOME}/bin/init-env.sh && \
echo 'fi' >> ${USER_HOME}/bin/init-env.sh && \
echo 'if [ ! -z "$INIT_MAVEN" ]; then' >> ${USER_HOME}/bin/init-env.sh && \
echo ' switch_maven $INIT_MAVEN' >> ${USER_HOME}/bin/init-env.sh && \
echo 'fi' >> ${USER_HOME}/bin/init-env.sh && \
echo 'exec "$@"' >> ${USER_HOME}/bin/init-env.sh && \
chmod +x ${USER_HOME}/bin/init-env.sh && \
chown app:app ${USER_HOME}/bin/init-env.sh
# Create .bashrc with proper permissions
RUN echo '# .bashrc' > ${USER_HOME}/.bashrc && \
echo 'export PATH=${HOME}/bin:${PATH}' >> ${USER_HOME}/.bashrc && \
echo 'source ${HOME}/bin/switch.sh' >> ${USER_HOME}/.bashrc && \
chown app:app ${USER_HOME}/.bashrc
# Verify script permissions
RUN test -x ${USER_HOME}/bin/init-env.sh && \
test -x ${USER_HOME}/bin/switch.sh
# Switch to app user
USER app
WORKDIR ${USER_HOME}
# Set PATH
ENV PATH=${USER_HOME}/bin:$PATH
# Set entrypoint
ENTRYPOINT ["/home/app/bin/init-env.sh"]
CMD ["bash"]
构建镜像,命令如下:
# 构建综合镜像
docker build -t dev/tools -f Dockerfile.combined .
笔者执行上述命令第一次失败,异常效果图如下:
配置国内(中国大陆)镜像加速可以解决上述问题,命令如下:
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"data-root": "/data/docker",
"exec-opts": ["native.cgroupdriver=systemd"],
"runtimes": {
"containerd": {
"path": "/run/containerd/containerd.sock"
}
},
"registry-mirrors": [
"https://mirror.baidubce.com",
"https://docker.mirrors.ustc.edu.cn",
"https://hub-mirror.c.163.com",
"https://mirrors.aliyun.com/docker-ce"
]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
正常效果图如下:
4. 创建和使用不同版本的脚本
4.1. 创建JDK切换信息显示脚本 docker-use-jdk(可选)
docker-use-jdk 内容如下:
#!/bin/bash
# 默认值和输入处理
VERSION="$1"
COMMAND="${@:2}"
# 检查版本参数
if [[ ! "$VERSION" =~ ^(8|11|17)$ ]]; then
echo "Usage: docker-use-jdk [8|11|17] [command]"
echo "Examples:"
echo " docker-use-jdk 8 # 运行Java 8,显示版本"
echo " docker-use-jdk 11 java -jar app.jar # 用Java 11运行JAR文件"
echo " docker-use-jdk 17 javac Hello.java # 用Java 17编译Java文件"
exit 1
fi
if [ -z "$COMMAND" ]; then
COMMAND="java -version"
fi
# 获取当前用户的UID和GID
USER_ID=$(id -u)
GROUP_ID=$(id -g)
CURRENT_DIR=$(pwd)
# 确保需要的目录挂载正确,并直接在容器中执行命令
docker run --rm -it \
-v "$CURRENT_DIR":"$CURRENT_DIR" \
-w "$CURRENT_DIR" \
--user $USER_ID:$GROUP_ID \
dev/tools bash -c "
# 直接设置Java路径,避免使用可能不存在的脚本
if [ \"$VERSION\" = \"8\" ]; then
export JAVA_HOME=/home/app/dev/jdk/jdk-8/openjdk
elif [ \"$VERSION\" = \"11\" ]; then
export JAVA_HOME=/home/app/dev/jdk/jdk-11/openjdk
elif [ \"$VERSION\" = \"17\" ]; then
export JAVA_HOME=/home/app/dev/jdk/jdk-17/openjdk
fi
export PATH=\$JAVA_HOME/bin:\$PATH
echo \"已切换到 JDK $VERSION: \$JAVA_HOME\"
$COMMAND
"
命令如下:
# 保存脚本后,确保它有执行权限
chmod +x ~/bin/docker-use-jdk
# 切换 JDK 11 信息
docker-use-jdk 11
效果图如下:
4.2. 创建Maven切换信息显示脚本 docker-use-maven(可选)
docker-use-maven 内容如下:
#!/bin/bash
# 默认值和输入处理
MAVEN_VERSION="$1"
JDK_VERSION="$2"
COMMAND="${@:3}"
# 检查版本参数
if [[ ! "$MAVEN_VERSION" =~ ^(3\.6\.3|3\.8\.6|3\.9\.3)$ ]]; then
echo "Usage: docker-use-maven [3.6.3|3.8.6|3.9.3] [8|11|17] [command]"
echo "Examples:"
echo " docker-use-maven 3.6.3 11 # 运行Maven 3.6.3与JDK 11,显示版本"
echo " docker-use-maven 3.8.6 8 mvn clean install # 用Maven 3.8.6与JDK 8运行构建"
exit 1
fi
# 检查JDK版本
if [[ ! "$JDK_VERSION" =~ ^(8|11|17)$ ]]; then
# 使用默认的JDK 11
JDK_VERSION="11"
fi
if [ -z "$COMMAND" ]; then
COMMAND="mvn -version"
fi
# 获取当前用户的UID和GID
USER_ID=$(id -u)
GROUP_ID=$(id -g)
CURRENT_DIR=$(pwd)
# 运行对应版本的Maven容器,同时设置JDK
docker run --rm -it \
-v "$CURRENT_DIR":"$CURRENT_DIR" \
-v "$HOME/.m2":/home/app/.m2 \
-w "$CURRENT_DIR" \
--user $USER_ID:$GROUP_ID \
dev/tools bash -c "source /home/app/bin/switch.sh && switch_jdk $JDK_VERSION && switch_maven $MAVEN_VERSION && $COMMAND"
命令如下:
# 保存脚本后,确保它有执行权限
chmod +x ~/bin/docker-use-maven
# 切换 Maven 3.8.6 信息
docker-use-maven 3.8.6
效果图如下:
4.3. 创建一个综合开发环境脚本 dev-env
dev-env 文件内容如下:
#!/bin/bash
# 默认值
JDK_VERSION="8"
MAVEN_VERSION="3.6.3"
MODE="interactive" # interactive 或 run 模式
COMMAND=""
DOCKER_EXTRA_ARGS=""
# 获取当前用户的UID和GID
USER_ID=$(id -u)
GROUP_ID=$(id -g)
CURRENT_DIR=$(pwd)
# 函数:打印帮助信息
print_help() {
echo "用法: dev-env [选项] [-- 命令]"
echo ""
echo "选项:"
echo " -j, --jdk VERSION 指定JDK版本 (默认: 8)"
echo " -m, --maven VERSION 指定Maven版本 (默认: 3.6.3)"
echo " -d, --docker-args ARGS 传递额外参数给docker run"
echo " -h, --help 显示此帮助信息"
echo ""
echo "命令示例:"
echo " dev-env # 启动带有默认JDK 8和Maven 3.6.3的交互式bash"
echo " dev-env -j 11 -m 3.8.6 # 启动带有JDK 11和Maven 3.8.6的交互式bash"
echo " dev-env -j 17 -- mvn clean install # 在JDK 17环境中执行mvn clean install"
echo " dev-env -- java -version # 运行java -version命令并退出"
echo " dev-env -d \"-p 8080:8080\" # 启动带有端口映射的环境"
}
# 解析命令行参数
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
print_help
exit 0
;;
-j|--jdk)
if [[ $# -gt 1 ]]; then
JDK_VERSION="$2"
shift 2
else
echo "错误: JDK版本参数缺失" >&2
exit 1
fi
;;
-m|--maven)
if [[ $# -gt 1 ]]; then
MAVEN_VERSION="$2"
shift 2
else
echo "错误: Maven版本参数缺失" >&2
exit 1
fi
;;
-d|--docker-args)
if [[ $# -gt 1 ]]; then
DOCKER_EXTRA_ARGS="$2"
shift 2
else
echo "错误: Docker参数缺失" >&2
exit 1
fi
;;
--)
shift
MODE="run"
COMMAND="$*"
break
;;
*)
echo "错误: 未知选项 $1" >&2
print_help
exit 1
;;
esac
done
# 运行容器 - 修复引号和路径问题
if [[ "$MODE" == "run" ]]; then
# 运行模式
docker run --rm -it \
-v "$CURRENT_DIR":"$CURRENT_DIR" \
-v "$HOME/.m2":/home/app/.m2 \
-w "$CURRENT_DIR" \
--user $USER_ID:$GROUP_ID \
-e INIT_JDK="$JDK_VERSION" \
-e INIT_MAVEN="$MAVEN_VERSION" \
$DOCKER_EXTRA_ARGS \
dev/tools $COMMAND
else
# 交互式bash模式
docker run --rm -it \
-v "$CURRENT_DIR":"$CURRENT_DIR" \
-v "$HOME/.m2":/home/app/.m2 \
-w "$CURRENT_DIR" \
--user $USER_ID:$GROUP_ID \
-e INIT_JDK="$JDK_VERSION" \
-e INIT_MAVEN="$MAVEN_VERSION" \
$DOCKER_EXTRA_ARGS \
dev/tools bash
fi
命令如下:
# 保存脚本后,确保它有执行权限
chmod +x ~/bin/dev-env
# 添加PATH并使脚本可用
echo 'export PATH=$HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
# 使用Maven 3.8.6和JDK 11
dev-env -j 11 -m 3.8.6
效果图如下: