实践篇:12-构建 Java 应用程序镜像

Java 是一种面向对象的编译型编程语言,具有"一次编写,到处运行"的特性。它是静态类型语言,变量需声明类型,编译时检查错误。Java 代码先编译为字节码,再由 JVM(Java 虚拟机)解释执行,兼具编译型和解释型的特点。Java 支持跨平台、自动垃圾回收和强大的生态系统,广泛应用于企业级应用、Android 开发和大型分布式系统。

背景介绍
Java 语言的特点使其非常适合容器化:

  • 跨平台能力:Java 的"一次编写,到处运行"特性与容器的可移植性理念高度契合
  • 企业级支持:成熟的企业级框架如 Spring Boot、Quarkus 等易于容器化部署
  • 丰富的生态系统:大量库和工具可用于构建高性能、可扩展的应用

我们将遵循设计篇: 设计篇: 04-Dockerfile设计原则与镜像管理规范中的逻辑分层思想,采用标准化的多阶段构建方式:

  1. 编译环境:使用 OpenJDK 和 Maven 工具构建完整的 Java 开发编译环境
  2. 运行环境:构建极简的运行基础镜像,仅包含运行 Java 应用所需的组件
  3. 应用镜像:将编译好的 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 有以下几个重要特点:

  1. 基于 Debian:使用了我们之前创建的标准化 Debian 基础镜像
  2. 动态版本:通过 ARG JAVA_VERSION 参数化 Java 版本,便于维护和更新
  3. 环境配置:设置了 JAVA_HOME 和 PATH 环境变量
  4. 多架构支持:检测当前 CPU 架构并安装相应版本的 OpenJDK
  5. 证书同步:确保容器内的 Java 证书与操作系统证书保持同步
  6. 性能优化:执行 java -Xshare:dump 生成类数据共享归档,提高启动性能
  7. 清理临时文件:每个步骤后清理不必要的缓存文件,减小镜像体积
  8. 默认命令:设置默认启动 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 工具环境的关键特点:

  1. 基于 OpenJDK:继承了我们之前创建的 OpenJDK 环境
  2. 版本参数化:通过 ARG 变量设置 Maven 版本,方便后续更新
  3. 环境变量配置:设置 MAVEN_HOME 和 MAVEN_CONFIG 环境变量
  4. 私有仓库配置:通过自定义 settings.xml 配置私有 Maven 仓库
  5. 镜像加速:配置了内部 Maven 镜像,加速依赖下载
  6. 凭证管理:预置了仓库访问凭证,简化构建流程

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

运行镜像重点解析

这个运行镜像的关键特点:

  1. 基于 OpenJDK:继承了我们之前创建的 OpenJDK 环境
  2. 非 root 用户:创建了专用的 nonroot 用户,提高安全性
  3. 标准目录结构:预先创建了应用和日志目录,并设置了正确的权限
  4. 用户切换:通过 USER 指令切换到非特权用户,防止容器内进程获取过高权限
  5. 版本参数化:通过 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 分为两个明确的阶段:

  1. 编译阶段 (builder):
    • 使用我们的 Maven 编译环境镜像
    • 接收构建参数(环境、应用名称、Git 信息等)
    • 分两步构建应用,利用 Docker 缓存优化:
    • 首先只复制 pom.xml 文件并下载依赖,这一步骤在依赖不变时可以复用缓存
    • 然后复制源代码并执行实际构建
    • 使用 BuildKit 缓存加速依赖下载和编译
  2. 运行阶段 (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 应用容器时,可以考虑以下最佳实践:

  1. JVM 调优:根据容器资源限制调整 JVM 内存参数,例如:
    • -XX:+UseContainerSupport 确保 JVM 感知容器限制
    • -XX:MaxRAMPercentage=75.0 限制最大堆内存为容器内存的 75%
    • -XX:+ExitOnOutOfMemoryError 在 OOM 时快速失败,便于重启
  2. 资源限制:使用 --memory 和 --cpus 设置容器资源上限,确保应用不会消耗过多资源
  3. 健康检查:添加 --health-cmd 配置容器健康检查,例如访问应用健康端点
  4. 日志管理:配置合适的日志驱动,将应用日志发送到集中式日志系统
  5. 安全设置:禁用不必要的功能,减少攻击面
  6. 容器编排:在生产环境中使用 Kubernetes 等工具进行编排和管理

总结

通过采用多阶段构建和遵循最佳实践,我们成功为 Java 应用程序创建了优化、安全且可维护的 Docker 镜像。这种方法具有以下优势:

  1. 镜像体积优化:运行镜像仅包含必要组件,减少了存储和网络传输开销
  2. 构建流程标准化:统一的构建流程,易于集成到 CI/CD 系统
  3. 安全性增强:使用非 root 用户运行,减少潜在安全风险
  4. 资源利用率提高:合理的 JVM 配置和容器资源限制,提高资源利用效率
  5. 部署灵活性:支持不同环境的配置注入,简化多环境部署

通过这种方式构建的 Java 应用容器既保留了 Java 生态系统的丰富功能,又充分发挥了容器技术的优势,为企业级应用提供了理想的部署方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

企鹅侠客

您的打赏是我创作旅程中的关键燃

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值