在基础镜像上构建支持 Maven 和 Docker 的镜像

你抬头看烟花的神情,比烟花还寂寞

Posted by yishuifengxiao on 2025-01-05

在 eclipse-temurin:17-jre 基础上构建支持 Maven 和 Docker 的镜像

下面是一个详细的步骤指南,用于在 eclipse-temurin:17-jre 基础镜像上构建一个支持 Maven 和 Docker 的镜像,并处理相关的权限和配置问题。

Dockerfile 内容

# 使用 eclipse-temurin:17-jre 作为基础镜像
FROM eclipse-temurin:17-jre

# 安装必要的系统工具
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
gnupg \
apt-transport-https \
ca-certificates \
software-properties-common && \
rm -rf /var/lib/apt/lists/*

# 安装 Docker CLI
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
apt-get update && \
apt-get install -y --no-install-recommends docker-ce-cli && \
rm -rf /var/lib/apt/lists/*

# 安装 Maven
ARG MAVEN_VERSION=3.8.6
ARG USER_HOME_DIR="/root"
ARG SHA=f790857f3b1f90ae8d16281f902c689e4f136ebe584aba45e4b1fa66c80cba826d3e0e52fdd04ed44b4c66f6d3fe3584a057c26dfcac544a60b301e6d0f91c26
ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries

RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
&& curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
&& echo "${SHA} /tmp/apache-maven.tar.gz" | sha512sum -c - \
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
&& rm -f /tmp/apache-maven.tar.gz \
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn

# 设置 Maven 环境变量
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"

# 创建一个非 root 用户并设置适当的权限
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN addgroup --gid ${GROUP_ID} appgroup && \
adduser --disabled-password --gecos '' --uid ${USER_ID} --gid ${GROUP_ID} appuser && \
# 将用户添加到 docker 组
addgroup appuser docker

# 创建挂载点目录并设置权限
RUN mkdir -p /home/appuser/.m2 && \
mkdir -p /home/appuser/project && \
chown -R appuser:appgroup /home/appuser/.m2 && \
chown -R appuser:appgroup /home/appuser/project

# 设置工作目录
WORKDIR /home/appuser/project

# 切换到非 root 用户
USER appuser

# 设置容器启动时的默认命令
CMD ["/bin/bash"]

构建和使用说明

构建镜像

docker build -t temurin-maven-docker:17 .

运行容器并挂载外部配置

docker run -it --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /path/to/host/m2:/home/appuser/.m2 \
-v /path/to/project:/home/appuser/project \
-v /path/to/docker/config.json:/home/appuser/.docker/config.json \
temurin-maven-docker:17

使用示例

在容器内执行 Maven 和 Docker 命令:

# 在容器内
mvn clean install
docker build -t my-app .

关键注意事项

Docker 套接字挂载

  • 挂载 /var/run/docker.sock 允许容器内使用宿主机的 Docker 守护进程
  • 这意味着容器内的 Docker 客户端实际上是在宿主机上执行命令

用户和组权限处理

  • 创建了非 root 用户 appuser 和组 appgroup
  • 用户可以自定义用户和组的 ID(通过构建参数 USER_IDGROUP_ID
  • 用户被添加到 docker 组以获得 Docker 套接字的访问权限
  • 确保挂载的目录有正确的所有权,避免权限问题

Maven 配置挂载

  • 挂载宿主机的 Maven 配置目录(~/.m2)到容器内
  • 这样可以重用本地 Maven 缓存,加速构建过程
  • 同时保持一致的 Maven 配置

构建时可以自定义用户ID和组ID,以匹配宿主机用户:

docker build \
--build-arg USER_ID=$(id -u) \
--build-arg GROUP_ID=$(id -g) \
-t temurin-maven-docker:17 .

替代方案:使用 Docker-in-Docker (DinD)

如果需要完全隔离的 Docker 环境,可以考虑使用 Docker-in-Docker:

# 在基础镜像中选择一个包含 Docker 守护进程的版本
FROM docker:dind

# 然后安装 Java 和 Maven
RUN apk add --no-cache openjdk17-jre

# 安装 Maven(同上)
# ... Maven 安装步骤 ...

# 设置用户和权限
# ... 用户设置步骤 ...

但这种方案会增加镜像大小和运行时资源消耗。

这个方案提供了一个灵活的环境,既可以使用宿主机 Docker 守护进程的效率,又保持了适当的用户权限隔离和配置管理。

在 Alpine 基础镜像上安装完整 Docker 引擎

使用 apk add 方式安装完整的 Docker 引擎,而不是只安装 Docker CLI。这需要在基于 Alpine Linux 的镜像上进行,因为 eclipse-temurin:17-jre 是基于 Debian 的,不支持 apk 包管理器。使用 eclipse-temurin:17-jre-alpine 作为基础镜像,并安装完整的 Docker 引擎。

下面是一个完整的 Dockerfile,它在 eclipse-temurin:17-jre-alpine 基础上构建支持 Maven 和 Docker 的镜像,并允许挂载外部 Maven 仓库和配置文件。

Dockerfile 内容

# 使用基于 Alpine 的 Eclipse Temurin JRE
FROM eclipse-temurin:17-jre-alpine

# 安装系统依赖和工具
RUN apk update && \
apk add --no-cache \
curl \
bash \
git \
openssh-client \
shadow

# 安装 Docker 引擎和 OpenRC
RUN apk add --no-cache docker openrc && \
rc-update add docker boot

# 安装 Maven (但不包含默认的 settings.xml 和仓库)
ARG MAVEN_VERSION=3.8.6
ARG USER_HOME_DIR="/home/appuser"
ARG SHA=f790857f3b1f90ae8d16281f902c689e4f136ebe584aba45e4b1fa66c80cba826d3e0e52fdd04ed44b4c66f6d3fe3584a057c26dfcac544a60b301e6d0f91c26
ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries

RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
&& curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
&& echo "${SHA} /tmp/apache-maven.tar.gz" | sha512sum -c - \
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
&& rm -f /tmp/apache-maven.tar.gz \
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn

# 设置 Maven 环境变量
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"

# 创建一个非 root 用户并设置适当的权限
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN addgroup -g ${GROUP_ID} appgroup && \
adduser -D -u ${USER_ID} -G appgroup appuser && \
# 将用户添加到 docker 组
adduser appuser docker

# 创建挂载点目录并设置权限
RUN mkdir -p /home/appuser/.m2 && \
mkdir -p /home/appuser/project && \
chown -R appuser:appgroup /home/appuser/.m2 && \
chown -R appuser:appgroup /home/appuser/project

# 创建 Maven 配置和仓库的符号链接目录
# 这样即使挂载外部目录,Maven 也能找到正确的路径
RUN mkdir -p /mnt/m2/repository && \
mkdir -p /mnt/m2/conf && \
chown -R appuser:appgroup /mnt/m2

# 配置 Docker 守护进程(可选)
RUN mkdir -p /etc/docker && \
echo '{"storage-driver": "vfs"}' > /etc/docker/daemon.json

# 设置工作目录
WORKDIR /home/appuser/project

# 复制启动脚本
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# 切换到非 root 用户
USER appuser

# 设置入口点
ENTRYPOINT ["/entrypoint.sh"]

# 设置容器启动时的默认命令
CMD ["/bin/bash"]

Entrypoint 脚本

创建一个 entrypoint.sh 文件来处理外部挂载的 Maven 配置:

#!/bin/bash

# 如果挂载了外部的 Maven 配置目录
if [ -d "/mnt/m2/conf" ] && [ "$(ls -A /mnt/m2/conf)" ]; then
echo "Using external Maven configuration..."
# 创建符号链接到挂载的配置
ln -sf /mnt/m2/conf/settings.xml /home/appuser/.m2/settings.xml
ln -sf /mnt/m2/conf/settings-security.xml /home/appuser/.m2/settings-security.xml 2>/dev/null || true
fi

# 如果挂载了外部的 Maven 仓库目录
if [ -d "/mnt/m2/repository" ]; then
echo "Using external Maven repository..."
# 创建符号链接到挂载的仓库
ln -sf /mnt/m2/repository /home/appuser/.m2/repository
fi

# 启动 Docker 守护进程(如果需要)
if [ "$1" = "dockerd" ]; then
echo "Starting Docker daemon..."
sudo dockerd > /var/log/docker.log 2>&1 &

# 等待 Docker 守护进程启动
while ! docker info > /dev/null 2>&1; do
echo "Waiting for Docker to start..."
sleep 1
done
echo "Docker daemon started."
fi

# 执行后续命令
exec "$@"

构建和使用说明

构建镜像

docker build -t temurin-maven-docker:17-alpine .

准备外部 Maven 配置和仓库

在宿主机上准备 Maven 配置和仓库目录:

mkdir -p /path/to/m2/conf
mkdir -p /path/to/m2/repository

# 将您的 settings.xml 放在 conf 目录中
cp ~/.m2/settings.xml /path/to/m2/conf/

运行容器

docker run -it --rm --privileged \
-v /path/to/m2/conf:/mnt/m2/conf \
-v /path/to/m2/repository:/mnt/m2/repository \
-v /path/to/project:/home/appuser/project \
temurin-maven-docker:17-alpine

在容器内使用 Maven 和 Docker

容器启动后,Maven 会自动使用外部挂载的配置和仓库:

# 在容器内执行 Maven 命令
mvn clean install

# 使用 Docker 命令
docker version

关键注意事项

Maven 配置和仓库挂载

  • 使用专门的挂载点 /mnt/m2/conf/mnt/m2/repository 来挂载外部配置
  • Entrypoint 脚本会创建符号链接,将外部配置连接到 Maven 的默认位置
  • 这样可以保持容器内路径的一致性,同时使用外部配置

权限处理

  • 容器内创建了非 root 用户 appuser,UID 和 GID 可以通过构建参数自定义
  • 所有相关目录都设置了正确的所有权
  • 使用 sudo 来执行需要特权的操作(如启动 Docker 守护进程)

Docker 守护进程管理

  • Entrypoint 脚本可以自动启动 Docker 守护进程(当第一个参数是 “dockerd” 时)
  • 使用 vfs 存储驱动以避免在容器内使用 OverlayFS 的复杂性

自定义构建参数

构建时可以自定义用户 ID 和组 ID,以匹配宿主机用户:

docker build \
--build-arg USER_ID=$(id -u) \
--build-arg GROUP_ID=$(id -g) \
-t temurin-maven-docker:17-alpine .

安全考虑

  • 虽然使用了非 root 用户,但容器仍然需要特权模式来运行 Docker 守护进程
  • 考虑使用更细粒度的权限控制,如 --cap-add 选项
  • 谨慎处理挂载的 Maven 配置,特别是如果包含敏感信息(如凭据)

使用示例

简单使用(只使用 Maven)

docker run -it --rm \
-v /path/to/m2/conf:/mnt/m2/conf \
-v /path/to/m2/repository:/mnt/m2/repository \
-v /path/to/project:/home/appuser/project \
temurin-maven-docker:17-alpine mvn clean install

完整使用(包括 Docker 守护进程)

docker run -it --rm --privileged \
-v /path/to/m2/conf:/mnt/m2/conf \
-v /path/to/m2/repository:/mnt/m2/repository \
-v /path/to/project:/home/appuser/project \
temurin-maven-docker:17-alpine dockerd mvn clean install

交互式使用

docker run -it --rm --privileged \
-v /path/to/m2/conf:/mnt/m2/conf \
-v /path/to/m2/repository:/mnt/m2/repository \
-v /path/to/project:/home/appuser/project \
temurin-maven-docker:17-alpine dockerd bash

这个方案提供了一个完整的开发环境,支持外部挂载的 Maven 配置和仓库,同时包含了完整的 Docker 引擎。通过符号链接和入口点脚本的巧妙组合,确保了容器内路径的一致性和外部配置的正确使用。

交互式构建容器镜像的方法

您希望先进入一个基础容器,然后在容器内交互式地安装所需软件,最后将容器保存为镜像。这是一个非常实用的方法,特别适合调试和测试不同的软件配置。以下是详细的步骤:

方法一:使用交互式会话手动安装

启动基础容器

# 使用 eclipse-temurin:17-jre-alpine 启动一个交互式容器
docker run -it --name builder-container \
--privileged \
-v /tmp/m2:/mnt/m2 \
-v $(pwd):/mnt/project \
eclipse-temurin:17-jre-alpine /bin/sh

在容器内安装所需软件

进入容器后,您可以交互式地安装和配置软件:

# 更新包管理器
apk update

# 安装基本工具
apk add curl bash git openssh-client shadow vim

# 安装 Docker 和 OpenRC
apk add docker openrc
rc-update add docker boot

# 安装 Maven
MAVEN_VERSION=3.8.6
cd /tmp
curl -O https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz
tar xzf apache-maven-${MAVEN_VERSION}-bin.tar.gz -C /opt
ln -s /opt/apache-maven-${MAVEN_VERSION} /opt/maven
ln -s /opt/maven/bin/mvn /usr/bin/mvn

# 设置环境变量
echo 'export MAVEN_HOME=/opt/maven' >> /etc/profile
echo 'export PATH=$MAVEN_HOME/bin:$PATH' >> /etc/profile

# 创建非 root 用户
addgroup -g 1000 appgroup
adduser -D -u 1000 -G appgroup appuser
adduser appuser docker

# 创建目录结构
mkdir -p /home/appuser/.m2
mkdir -p /home/appuser/project
chown -R appuser:appgroup /home/appuser

# 配置 Docker 守护进程
mkdir -p /etc/docker
echo '{"storage-driver": "vfs"}' > /etc/docker/daemon.json

# 退出容器(但保持容器运行)
# 按 Ctrl+P, Ctrl+Q 退出而不停止容器

将容器提交为镜像

在另一个终端中执行:

# 查看运行中的容器
docker ps

# 将容器提交为新的镜像
docker commit builder-container my-custom-builder:latest

# 停止容器
docker stop builder-container

# 删除容器(可选)
docker rm builder-container

方法二:使用 Dockerfile 和交互式构建

创建基础 Dockerfile

FROM eclipse-temurin:17-jre-alpine

# 安装基本工具
RUN apk update && \
apk add --no-cache \
curl \
bash \
git \
openssh-client \
shadow \
vim

# 设置工作目录
WORKDIR /root

# 保持容器运行
CMD ["tail", "-f", "/dev/null"]

构建基础镜像并运行

# 构建基础镜像
docker build -t interactive-builder .

# 运行交互式容器
docker run -it --name interactive-builder \
--privileged \
-v /tmp/m2:/mnt/m2 \
-v $(pwd):/mnt/project \
interactive-builder

在容器内安装软件

在容器内执行安装命令(与方法一相同)。

保存更改

# 在另一个终端中提交容器
docker commit interactive-builder my-final-builder:latest

# 停止并删除容器
docker stop interactive-builder
docker rm interactive-builder

方法三:使用脚本记录安装过程

创建安装脚本

创建 install-software.sh 文件:

#!/bin/sh

# 安装 Docker 和 OpenRC
apk add docker openrc
rc-update add docker boot

# 安装 Maven
MAVEN_VERSION=3.8.6
cd /tmp
curl -O https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz
tar xzf apache-maven-${MAVEN_VERSION}-bin.tar.gz -C /opt
ln -s /opt/apache-maven-${MAVEN_VERSION} /opt/maven
ln -s /opt/maven/bin/mvn /usr/bin/mvn

# 设置环境变量
echo 'export MAVEN_HOME=/opt/maven' >> /etc/profile
echo 'export PATH=$MAVEN_HOME/bin:$PATH' >> /etc/profile

# 创建非 root 用户
addgroup -g 1000 appgroup
adduser -D -u 1000 -G appgroup appuser
adduser appuser docker

# 创建目录结构
mkdir -p /home/appuser/.m2
mkdir -p /home/appuser/project
chown -R appuser:appgroup /home/appuser

# 配置 Docker 守护进程
mkdir -p /etc/docker
echo '{"storage-driver": "vfs"}' > /etc/docker/daemon.json

启动容器并执行脚本

# 启动容器
docker run -it --name scripted-builder \
--privileged \
-v $(pwd):/mnt \
eclipse-temurin:17-jre-alpine /bin/sh

# 在容器内执行
chmod +x /mnt/install-software.sh
/mnt/install-software.sh

提交镜像

# 在另一个终端中提交容器
docker commit scripted-builder scripted-builder:latest

方法四:使用多阶段构建和保存点

创建 Dockerfile 并设置保存点

FROM eclipse-temurin:17-jre-alpine AS base

# 安装基本工具
RUN apk update && \
apk add --no-cache \
curl \
bash \
git \
openssh-client \
shadow \
vim

# 设置保存点
FROM base AS intermediate

# 安装 Docker
RUN apk add docker openrc && \
rc-update add docker boot

# 另一个保存点
FROM intermediate AS final

# 安装 Maven
ARG MAVEN_VERSION=3.8.6
RUN cd /tmp && \
curl -O https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz && \
tar xzf apache-maven-${MAVEN_VERSION}-bin.tar.gz -C /opt && \
ln -s /opt/apache-maven-${MAVEN_VERSION} /opt/maven && \
ln -s /opt/maven/bin/mvn /usr/bin/mvn && \
rm apache-maven-${MAVEN_VERSION}-bin.tar.gz

# 设置环境变量
ENV MAVEN_HOME=/opt/maven
ENV PATH=$MAVEN_HOME/bin:$PATH

# 创建用户和目录
RUN addgroup -g 1000 appgroup && \
adduser -D -u 1000 -G appgroup appuser && \
adduser appuser docker && \
mkdir -p /home/appuser/.m2 /home/appuser/project && \
chown -R appuser:appgroup /home/appuser

# 配置 Docker
RUN mkdir -p /etc/docker && \
echo '{"storage-driver": "vfs"}' > /etc/docker/daemon.json

# 设置工作目录和用户
WORKDIR /home/appuser/project
USER appuser

构建到特定阶段

# 构建到基础阶段
docker build --target base -t my-builder:base .

# 构建到中间阶段
docker build --target intermediate -t my-builder:intermediate .

# 构建完整镜像
docker build --target final -t my-builder:final .

使用最终镜像

无论使用哪种方法,最终都可以使用构建的镜像:

# 运行最终镜像
docker run -it --rm --privileged \
-v /path/to/m2/conf:/mnt/m2/conf \
-v /path/to/m2/repository:/mnt/m2/repository \
-v /path/to/project:/home/appuser/project \
my-builder:final /bin/sh

注意事项

  1. 权限问题:确保挂载的目录有正确的权限,特别是当使用非 root 用户时。

  2. 容器状态:使用 docker commit 时,容器中所有更改(包括临时文件)都会被保存。建议在提交前清理不必要的文件。

  3. 可重复性:交互式方法不适合生产环境,因为它不可重复。对于生产环境,建议使用 Dockerfile。

  4. 镜像大小:交互式安装可能会留下不必要的缓存文件,增加镜像大小。可以在安装完成后运行清理命令。

  5. Docker 守护进程:在容器内运行 Docker 守护进程需要特权模式,这有安全风险,只应在开发环境中使用。

  6. 保存工作:如果您在容器中做了很多配置工作,定期提交容器为镜像,以免丢失工作进度。

这种方法非常适合探索和测试不同的软件配置,然后再将确认的配置转换为可重复的 Dockerfile。