Java 是一种面向对象的编译型编程语言,具有"一次编写,到处运行"的特性。它是静态类型语言,变量需声明类型,编译时检查错误。Java 代码先编译为字节码,再由 JVM(Java 虚拟机)解释执行,兼具编译型和解释型的特点。Java 支持跨平台、自动垃圾回收和强大的生态系统,广泛应用于企业级应用、Android 开发和大型分布式系统。
背景介绍
Java 语言的特点使其非常适合容器化:
- 跨平台能力:Java 的"一次编写,到处运行"特性与容器的可移植性理念高度契合
- 企业级支持:成熟的企业级框架如 Spring Boot、Quarkus 等易于容器化部署
- 丰富的生态系统:大量库和工具可用于构建高性能、可扩展的应用
我们将遵循设计篇: 设计篇: 04-Dockerfile设计原则与镜像管理规范中的逻辑分层思想,采用标准化的多阶段构建方式:
- 编译环境:使用 OpenJDK 和 Maven 工具构建完整的 Java 开发编译环境
- 运行环境:构建极简的运行基础镜像,仅包含运行 Java 应用所需的组件
- 应用镜像:将编译好的 JAR 文件从编译阶段拷贝到运行阶段镜像,实现精简部署
构建 Java 工具环境镜像
创建 OpenJDK 工具环境目录
首先创建 openjdk 工具环境的目录:
mkdir -p common/tools/openjdk
cd common/tools/openjdk
OpenJDK 工具环境 Dockerfile 详解
#syntax=harbor.leops.local/library/docker/dockerfile:1
FROM harbor.leops.local/common/os/debian:bullseye
ARG JAVA_VERSION=24-ea+36 \
JAVA_FILE_URL=https://download.java.net/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24
LABEL org.opencontainers.image.authors="ops@leops.local" \
org.opencontainers.image.source="http://git.leops.local/ops/dockerfiles-base/common/tools/openjdk/Dockerfile" \
org.opencontainers.image.description="java ${JAVA_VERSION} compiler environment."
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
# utilities for keeping Debian and OpenJDK CA certificates in sync
ca-certificates p11-kit \
; \
rm -rf /var/lib/apt/lists/*
ENV JAVA_HOME /usr/local/openjdk-24
ENV PATH $JAVA_HOME/bin:$PATH
# Default to UTF-8 file.encoding
ENV LANG C.UTF-8
# https://jdk.java.net/
# >
# > Java Development Kit builds, from Oracle
# >
ENV JAVA_VERSION ${JAVA_VERSION}
RUN set -eux; \
\
arch="$(dpkg --print-architecture)"; \
case"$arch"in \
'amd64') \
downloadUrl="${JAVA_FILE_URL}_linux-x64_bin.tar.gz"; \
;; \
'arm64') \
downloadUrl="${JAVA_FILE_URL}_linux-aarch64_bin.tar.gz"; \
;; \
*) echo >&2 "error: unsupported architecture: '$arch'"; exit 1 ;; \
esac; \
\
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends \
wget \
; \
rm -rf /var/lib/apt/lists/*; \
\
wget --progress=dot:giga -O openjdk.tgz "$downloadUrl"; \
\
mkdir -p "$JAVA_HOME"; \
tar --extract \
--file openjdk.tgz \
--directory "$JAVA_HOME" \
--strip-components 1 \
--no-same-owner \
; \
rm openjdk.tgz*; \
\
apt-mark auto '.*' > /dev/null; \
[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
\
# update "cacerts" bundle to use Debian's CA certificates (and make sure it stays up-to-date with changes to Debian's store)
# see https://github.com/docker-library/openjdk/issues/327
# http://rabexc.org/posts/certificates-not-working-java#comment-4099504075
# https://salsa.debian.org/java-team/ca-certificates-java/blob/3e51a84e9104823319abeb31f880580e46f45a98/debian/jks-keystore.hook.in
# https://git.alpinelinux.org/aports/tree/community/java-cacerts/APKBUILD?id=761af65f38b4570093461e6546dcf6b179d2b624#n29
{ \
echo '#!/usr/bin/env bash'; \
echo 'set -Eeuo pipefail'; \
echo 'trust extract --overwrite --format=java-cacerts --filter=ca-anchors --purpose=server-auth "$JAVA_HOME/lib/security/cacerts"'; \
} > /etc/ca-certificates/update.d/docker-openjdk; \
chmod +x /etc/ca-certificates/update.d/docker-openjdk; \
/etc/ca-certificates/update.d/docker-openjdk; \
\
# https://github.com/docker-library/openjdk/issues/331#issuecomment-498834472
find "$JAVA_HOME/lib" -name '*.so' -exec dirname '{}'';' | sort -u > /etc/ld.so.conf.d/docker-openjdk.conf; \
ldconfig; \
\
# https://github.com/docker-library/openjdk/issues/212#issuecomment-420979840
# https://openjdk.java.net/jeps/341
java -Xshare:dump; \
\
# basic smoke test
fileEncoding="$(echo 'System.out.println(System.getProperty("file.encoding"))' | jshell -s -)"; [ "$fileEncoding" = 'UTF-8' ]; rm -rf ~/.java; \
javac --version; \
java --version
CMD ["jshell"]
Dockerfile 关键点解析
这个 OpenJDK 工具环境 Dockerfile 有以下几个重要特点:
- 基于 Debian:使用了我们之前创建的标准化 Debian 基础镜像
- 动态版本:通过 ARG JAVA_VERSION 参数化 Java 版本,便于维护和更新
- 环境配置:设置了 JAVA_HOME 和 PATH 环境变量
- 多架构支持:检测当前 CPU 架构并安装相应版本的 OpenJDK
- 证书同步:确保容器内的 Java 证书与操作系统证书保持同步
- 性能优化:执行 java -Xshare:dump 生成类数据共享归档,提高启动性能
- 清理临时文件:每个步骤后清理不必要的缓存文件,减小镜像体积
- 默认命令:设置默认启动 jshell 交互环境
OpenJDK 镜像构建脚本
使用以下脚本 (build.sh) 来构建和推送 OpenJDK 工具环境镜像:
#!/bin/bash
set -e
# 配置
REGISTRY="harbor.leops.local"
IMAGE_BASE_NAME="common/tools/openjdk"
VERSION="24-ea+36"
JAVA_FILE_URL="https://download.java.net/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24"
# 声明镜像地址数组
declare -a IMAGE_PATHS
IMAGE_PATHS+=(
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION/\+/_}"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION%%-*}"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION/\+/_}-debian11"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION%%-*}-debian11"
)
build_image() {
echo "Building and pushing image:"
for img in "${IMAGE_PATHS[@]}"; do echo -e " $img"; done
# 构建镜像
docker buildx build \
$(for img in "${IMAGE_PATHS[@]}"; do echo -n "-t $img "; done) \
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
--build-arg "JAVA_VERSION=${VERSION}" \
--build-arg "JAVA_FILE_URL=${JAVA_FILE_URL}" \
--add-host nexus.leops.local=192.168.77.140 \
--provenance=false \
--pull \
--push \
.
echo "Build complete."
}
# 参数处理
case "$1" in
"list-tags")
# 输出镜像标签列表
printf '%s\n'"${IMAGE_PATHS[@]}"
;;
*)
build_image
;;
esac
脚本支持创建多个标签版本,包括完整版本号、主版本号以及带有系统标识的组合标签,确保镜像引用的灵活性。
创建 Maven 工具环境目录
接着我们需要一个 Maven 编译环境,这个编译环境是基于 openjdk 的。
mkdir -p common/tools/maven
cd common/tools/maven
Maven 工具环境 Dockerfile 详解
#syntax=harbor.leops.local/library/docker/dockerfile:1
FROM harbor.leops.local/common/tools/openjdk:24
ARG MAVEN_VERSION=3.9.9
LABEL org.opencontainers.image.authors="ops@leops.local" \
org.opencontainers.image.source="http://git.leops.local/ops/dockerfiles-base/common/tools/maven/Dockerfile" \
org.opencontainers.image.description="Apache Maven ${MAVEN_VERSION} compiler environment."
ENV MAVEN_HOME=/usr/share/maven \
MAVEN_CONFIG="/root/.m2"
RUN set -eux; \
curl -fsSLO --compressed https://downloads.apache.org/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz; \
mkdir -p ${MAVEN_HOME} ${MAVEN_HOME}/refi ${MAVEN_CONFIG}; \
tar -xzf apache-maven-${MAVEN_VERSION}-bin.tar.gz -C ${MAVEN_HOME} --strip-components=1; \
ln -s ${MAVEN_HOME}/bin/mvn /usr/bin/mvn; \
mvn --version; \
rm -fv apache-maven-*.tar.gz;
COPY settings.xml ${MAVEN_CONFIG}/
Maven 配置文件
Maven 的 settings.xml 配置文件用于定义 Maven 的行为,包括仓库位置、代理和凭证等:
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>/usr/share/maven/ref/repository</localRepository>
<profiles>
<profile>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>maven-artifacts</id>
<name>Artifacts created</name>
<url>http://nexus.leops.local/repository/maven-snapshots/</url>
</repository>
<repository>
<id>maven-releases</id>
<name>Artifacts created</name>
<url>http://nexus.leops.local/repository/maven-releases/</url>
</repository>
</repositories>
</profile>
</profiles>
<servers>
<server>
<id>maven-snapshots</id>
<username>admin</username>
<password>admin123</password>
</server>
<server>
<id>maven-releases</id>
<username>admin</username>
<password>admin123</password>
</server>
</servers>
<mirrors>
<mirror>
<id>maven-proxy</id>
<name>maven proxy</name>
<mirrorOf>*</mirrorOf>
<url>http://nexus.leops.local/repository/maven-public/</url>
</mirror>
</mirrors>
</settings>
Maven 镜像重点解析
这个 Maven 工具环境的关键特点:
- 基于 OpenJDK:继承了我们之前创建的 OpenJDK 环境
- 版本参数化:通过 ARG 变量设置 Maven 版本,方便后续更新
- 环境变量配置:设置 MAVEN_HOME 和 MAVEN_CONFIG 环境变量
- 私有仓库配置:通过自定义 settings.xml 配置私有 Maven 仓库
- 镜像加速:配置了内部 Maven 镜像,加速依赖下载
- 凭证管理:预置了仓库访问凭证,简化构建流程
Maven 镜像构建脚本
使用以下脚本 (build.sh) 来构建和推送 Maven 工具环境镜像:
#!/bin/bash
set -e
# 配置
REGISTRY="harbor.leops.local"
IMAGE_BASE_NAME="common/tools/maven"
VERSION="3.9.9"
# 声明镜像地址数组
declare -a IMAGE_PATHS
IMAGE_PATHS+=(
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION}"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION%.*}"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION%%.*}"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION}-debian11"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION%.*}-debian11"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION%%.*}-debian11"
)
build_image() {
echo "Building and pushing image:"
for img in "${IMAGE_PATHS[@]}"; do echo -e " $img"; done
# 构建镜像
docker buildx build \
$(for img in "${IMAGE_PATHS[@]}"; do echo -n "-t $img "; done) \
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
--build-arg "MAVEN_VERSION=${VERSION}" \
--add-host nexus.leops.local=192.168.77.140 \
--provenance=false \
--pull \
--push \
.
echo "Build complete."
}
# 参数处理
case "$1" in
"list-tags")
# 输出镜像标签列表
printf '%s\n'"${IMAGE_PATHS[@]}"
;;
*)
build_image
;;
esac
构建 Java 应用的运行镜像
编译环境负责构建应用,而运行镜像则专注于高效安全地运行 Java 应用。我们需要创建一个精简的运行环境,仅包含必要的组件。
为什么需要专门的运行镜像?
好的 Java 运行镜像应该具备以下特点:
- 最小化攻击面:减少不必要的组件和工具
- 合理的权限:避免使用 root 用户运行应用
- 适当的文件结构:提供标准化的应用目录结构
- 优化的 JVM 设置:合理配置 JVM 参数,提高性能和资源利用率
创建 Java 运行环境目录
mkdir -p common/runtime/openjdk
cd common/runtime/openjdk
Java 运行环境 Dockerfile
#syntax=harbor.leops.local/library/docker/dockerfile:1
ARG OPENJDK_VERSION=24
FROM harbor.leops.local/common/tools/openjdk:$OPENJDK_VERSION
LABEL org.opencontainers.image.authors="ops@leops.local" \
org.opencontainers.image.source="http://git.leops.local/ops/dockerfiles-base/common/runtime/openjdk/Dockerfile" \
org.opencontainers.image.description="Minimal base runtime for Openjdk applications with non-root user."
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
RUN \
groupadd -r nonroot \
&& useradd -r -m -g nonroot nonroot \
&& mkdir -p /app/logs \
&& chown nonroot:nonroot -R /app
USER nonroot:nonroot
运行镜像重点解析
这个运行镜像的关键特点:
- 基于 OpenJDK:继承了我们之前创建的 OpenJDK 环境
- 非 root 用户:创建了专用的 nonroot 用户,提高安全性
- 标准目录结构:预先创建了应用和日志目录,并设置了正确的权限
- 用户切换:通过 USER 指令切换到非特权用户,防止容器内进程获取过高权限
- 版本参数化:通过 ARG 变量可以选择不同的 OpenJDK 版本
运行镜像构建脚本
使用以下脚本(build.sh)构建运行环境镜像:
#!/bin/bash
set -e
# 配置
REGISTRY="harbor.leops.local"
IMAGE_BASE_NAME="common/runtime/openjdk"
VERSION="24-debian11"
# 声明镜像地址数组
declare -a IMAGE_PATHS
IMAGE_PATHS+=(
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION}"
)
build_image() {
echo "Building and pushing image:"
for img in "${IMAGE_PATHS[@]}"; do echo -e " $img"; done
# 构建镜像
docker buildx build \
$(for img in "${IMAGE_PATHS[@]}"; do echo -n "-t $img "; done) \
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
--build-arg "OPENJDK_VERSION=${VERSION}" \
--provenance=false \
--pull \
--push \
.
echo "Build complete."
}
# 参数处理
case "$1" in
"list-tags")
# 输出镜像标签列表
printf '%s\n'"${IMAGE_PATHS[@]}"
;;
*)
build_image
;;
esac
构建应用镜像 - 多阶段构建实战
在准备好编译环境和运行环境后,我们可以通过多阶段构建来创建最终的应用镜像。多阶段构建允许我们在同一个 Dockerfile 中使用多个 FROM 指令,每个阶段可以独立运行并复用结果。
Docker 多阶段构建简介
多阶段构建的主要优势:
- 分离构建和运行环境:编译阶段可以包含所有开发工具,而运行阶段仅包含必要组件
- 减小最终镜像体积:最终镜像只包含运行所需文件,不含编译工具和中间产物
- 优化缓存:合理使用构建缓存加速重复构建
- 简化流程:将多个构建步骤整合到单个 Dockerfile 中
准备示例应用
首先,我们获取一个简单的 Spring Boot 应用示例:
git clone https://github.com/lework/ci-demo-springboot.git
cd ci-demo-springboot
应用 Dockerfile 详解
下面是一个典型的 Java 应用多阶段构建 Dockerfile:
#syntax=harbor.leops.local/library/docker/dockerfile:1
FROM harbor.leops.local/common/tools/maven:3 AS builder
ARG APP_ENV=test \
APP=undefine \
GIT_BRANCH= \
GIT_COMMIT_ID=
ENV APP_ENV=$APP_ENV \
APP=$APP \
GIT_BRANCH=$GIT_BRANCH \
GIT_COMMIT_ID=$GIT_COMMIT_ID
WORKDIR /app_build
# Maven dependencies
ADD pom.xml ./pom.xml
RUN --mount=type=cache,id=${APP}-maven-repo,target=/usr/share/maven/ref/repository \
mvn -B clean install -DskipTests -Dcheckstyle.skip -Dasciidoctor.skip -Djacoco.skip -Dmaven.gitcommitid.skip -Dspring-boot.repackage.skip -Dmaven.exec.skip=true -Dmaven.test.skip=true -Dmaven.compile.skip=true -Dmaven.resources.skip=true -Dmaven.javadoc.skip=true -Dmaven.install.skip=true -Dmaven.jar.skip=true
# Build
COPY ./ .
RUN --mount=type=cache,id=${APP}-maven-repo,target=/usr/share/maven/ref/repository \
mvn clean package -Dmaven.test.skip=true
#
# ---- 运行环境 ----
FROM harbor.leops.local/common/runtime/openjdk:24-debian11 AS running
ARG APP_ENV=test \
APP=undefine
ENV APP_ENV=$APP_ENV \
APP=$APP
WORKDIR /app
COPY --from=builder /app_build/target/*.jar /app/$APP.jar
ENTRYPOINT ["/bin/bash", "-c", "exec java -Djava.security.egd=file:/dev/./urandom ${JAVA_OPTS} -Dfile.encoding=UTF8 -jar /app/${APP}.jar --spring.profiles.actvie=${APP_ENV} --server.port=${SERVER_PORT:-8080}"]
多阶段构建关键点解析
这个 Dockerfile 分为两个明确的阶段:
- 编译阶段 (builder):
- 使用我们的 Maven 编译环境镜像
- 接收构建参数(环境、应用名称、Git 信息等)
- 分两步构建应用,利用 Docker 缓存优化:
- 首先只复制 pom.xml 文件并下载依赖,这一步骤在依赖不变时可以复用缓存
- 然后复制源代码并执行实际构建
- 使用 BuildKit 缓存加速依赖下载和编译
- 运行阶段 (running):
- 使用我们的 Java 运行环境镜像
- 仅从编译阶段复制构建好的 JAR 文件
- 设置环境变量,支持不同部署环境
- 配置 ENTRYPOINT 启动应用,包含优化的 JVM 参数:
- 使用 /dev/urandom 作为随机数生成源,提高启动速度
- 配置 UTF-8 编码
- 支持通过环境变量注入 JVM 选项
构建应用镜像
执行以下命令构建示例应用:
bash /data/dockerfiles-base/app-build/build-app.sh dev ci-demo-springboot
构建完成后,会生成如下格式的镜像标签:
harbor.leops.local/dev/ci-demo-springboot:master-256c81b-202504270330
版本控制
完成构建后,将所有文件提交到 Git 仓库进行版本控制:
git add -A .
git commit -m "feat: add springboot"
git push
运行 Java 应用容器
最终,我们可以运行构建好的 Java 应用容器,并验证其功能。
容器运行与验证
# 运行容器
docker run --rm -d --name ci-demo-springboot -p 18082:8080 harbor.leops.local/dev/ci-demo-springboot:master-256c81b-202504270330
# 访问应用
curl http://localhost:18082/api/info
# 查看日志
docker logs ci-demo-springboot
# 停止容器
docker stop ci-demo-springboot
生产环境最佳实践
在生产环境中部署 Java 应用容器时,可以考虑以下最佳实践:
- JVM 调优:根据容器资源限制调整 JVM 内存参数,例如:
- -XX:+UseContainerSupport 确保 JVM 感知容器限制
- -XX:MaxRAMPercentage=75.0 限制最大堆内存为容器内存的 75%
- -XX:+ExitOnOutOfMemoryError 在 OOM 时快速失败,便于重启
- 资源限制:使用 --memory 和 --cpus 设置容器资源上限,确保应用不会消耗过多资源
- 健康检查:添加 --health-cmd 配置容器健康检查,例如访问应用健康端点
- 日志管理:配置合适的日志驱动,将应用日志发送到集中式日志系统
- 安全设置:禁用不必要的功能,减少攻击面
- 容器编排:在生产环境中使用 Kubernetes 等工具进行编排和管理
总结
通过采用多阶段构建和遵循最佳实践,我们成功为 Java 应用程序创建了优化、安全且可维护的 Docker 镜像。这种方法具有以下优势:
- 镜像体积优化:运行镜像仅包含必要组件,减少了存储和网络传输开销
- 构建流程标准化:统一的构建流程,易于集成到 CI/CD 系统
- 安全性增强:使用非 root 用户运行,减少潜在安全风险
- 资源利用率提高:合理的 JVM 配置和容器资源限制,提高资源利用效率
- 部署灵活性:支持不同环境的配置注入,简化多环境部署
通过这种方式构建的 Java 应用容器既保留了 Java 生态系统的丰富功能,又充分发挥了容器技术的优势,为企业级应用提供了理想的部署方案。