QxMO的空间

构建docker镜像——做出自己需要的镜像

在使用 Docker 部署应用时,我们通常会直接从 Docker Hub 拉取官方镜像(比如 Nginx、MySQL 或各类基础系统)。但很多时候,官方提供的现成镜像并不能完美契合我们的实际需求。

当找不到合适的镜像时,最好的解决办法就是——自己造一个!

我们这篇文章就以构建一个MCDR镜像作为示范,教你如何构建镜像

构建镜像无外乎就几样东西,你只需要牢记一句话:“基于什么,加入什么,做什么,什么样。”

文章基于这期视频


📝 什么是 Dockerfile?

Dockerfile 就像是一张“菜谱”,它包含了一系列指令,告诉 Docker 引擎如何一步步地把基础系统、依赖软件和你的代码组装成最终的镜像。与docker compose不同的就是,Dockerfile是用来构建镜像的,docker compose是用来构建容器的

接下来,我们将演示两种不同复杂度的“菜谱”写法。


方案 A:极简基础版(快速入门)

这种写法代码精简,非常适合新手理解 Docker 镜像构建的基本逻辑。我们的目标是基于现有的 MCDR 镜像,通过 Ubuntu 的自带包管理器安装 OpenJDK 25。

在一个空白目录下创建一个名为 Dockerfile 的文件,填入以下内容:

FROM mcdreforged/mcdreforged-extra:2.15.0-py3.13-slim
RUN apt-get update && \
    apt-get install -y --no-install-recommends openjdk-25-jdk && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN java -version
EXPOSE 25565
WORKDIR /mcdr

基于什么

FROM 这条指令就是告诉我们,以下操作都将基于mcdreforged/mcdreforged-extra:2.15.0-py3.13-slim这个镜像

加入什么,做什么

RUN 表示运行后面这些命令,后面这些的东西总结一下就是升级apt包管理,通过apt安装了openjdk25,清除掉apt升级和安装jdk的缓存(这一步可以帮你减小最后得到的镜像体积)多条指令之间使用&& 相连,最后输出java的版本,方便你确认安装是否成功

什么样

EXPOSE 表示容器对外开放这个端口(这里表示对外开放25565)

WORKDIR 表示这个路径为文件存放位置,方便开启容器后挂载文件


方案 B:进阶优化版(掌握高级技巧)

我们可以尝试去做一些更进阶一些的玩法,比如,我们不装apt提供的jdk,我们在镜像中加入temurin的jkd25,这个方案稍微复杂,但它演示了 Docker 镜像构建中的几个高级技巧:参数化构建Here-Doc 语法以及环境变量配置

同样创建 Dockerfile 文件,填入以下内容:

FROM mcdreforged/mcdreforged-extra:2.15.0-py3.13-slim

ARG JAVA=25

RUN <<EOT
set -eux
export DEBIAN_FRONTEND="noninteractive"

apt-get update
apt-get install -y --no-install-recommends gnupg ca-certificates curl

curl -fsSL https://packages.adoptium.net/artifactory/api/gpg/key/public \
    | gpg --dearmor -o /usr/share/keyrings/adoptium.gpg

ARCH=$(dpkg --print-architecture)
VERSION_CODENAME=$(. /etc/os-release && echo "$VERSION_CODENAME")

cat > /etc/apt/sources.list.d/adoptium.sources <<EOF
Types: deb
URIs: https://packages.adoptium.net/artifactory/deb
Suites: ${VERSION_CODENAME}
Components: main
Architectures: ${ARCH}
Signed-By: /usr/share/keyrings/adoptium.gpg
EOF

apt-get update

for i in 1 2 3; do
    if apt-get install -y "temurin-${JAVA}-jdk" && break; then
        true
    else
        echo "Attempt $i failed, retrying in 30s..."
        sleep 30
    fi
done

JAVA_HOME=$(find /usr/lib/jvm -maxdepth 1 -name "temurin-${JAVA}*" -type d | head -1)
ln -sf "${JAVA_HOME}/bin/"* /usr/local/bin/
java -version
javac -version

apt-get clean
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
EOT

ENV JAVA_HOME=/usr/lib/jvm/temurin-${JAVA}-jdk

看起来很复杂,但万变不离其宗,任然是牢记那句话,“基于什么,加入什么,做什么,什么样”,我们来拆解分析一下

基于什么

FROM mcdreforged/mcdreforged-extra:2.15.0-py3.13-slim

解析:FROM 指令定义了构建的起点(基础镜像)。这里使用的是 MCDReforged 官方提供的附加版镜像,内部已经预装了 Python 3.13 和 MCDR 运行所需的依赖,且 slim 代表这是一个去除了不必要文件的精简版系统(通常基于 Debian)。

加入什么,做什么

RUN <<EOT

解析:RUN 指令用于在容器中执行命令。<<EOT 是 Dockerfile 支持的现代 Here-Doc 语法,允许你在里面像写 Shell 脚本一样写多行代码,而不需要在每行末尾加 \和&&,从这个EOT到下面清理后的EOT中间这部分都是由这一个RUN执行。这让整个操作都在一个“镜像层”中完成,能有效减小镜像体积。

set -eux

解析:设置 Shell 脚本的安全和调试模式: -e:遇到任何错误立刻停止构建(防止带着错误继续构建)。 -u:遇到未定义的变量立刻报错。 -x:将执行的每一行命令打印到控制台,方便排查报错(调试用)。

export DEBIAN_FRONTEND="noninteractive"

解析:告诉 apt-get 等包管理器“当前处于非交互环境”。这样在安装软件时,如果遇到需要选择时区、确认键盘布局等提示,系统会使用默认选项静默跳过,防止构建过程卡死。

apt-get update apt-get install -y --no-install-recommends gnupg ca-certificates curl

解析:更新自带的软件源列表,并安装三个必要工具:gnupg(用于处理加密密钥)、ca-certificates(验证 HTTPS 证书)、curl(下载工具)。--no-install-recommends 用于拒绝安装非必需的推荐包,保持镜像小巧。

curl -fsSL https://packages.adoptium.net/artifactory/api/gpg/key/public
| gpg --dearmor -o /usr/share/keyrings/adoptium.gpg

解析:使用 curl 下载 Adoptium(提供 Temurin JDK 的官方组织)的 GPG 公钥,并通过 gpg --dearmor 解码,保存到系统的密钥环目录中。这是为了后续让系统信任来自 Adoptium 仓库的软件包,确保下载的 Java 没有被篡改。

ARCH=$(dpkg --print-architecture)

VERSION_CODENAME=$(. /etc/os-release && echo "$VERSION_CODENAME")

解析:动态获取当前系统信息: ARCH 获取 CPU 架构(例如 amd64 或 arm64)。 VERSION_CODENAME 获取当前 Debian/Ubuntu 系统的代号(例如 bookworm 或 jammy)。 这保证了这份 Dockerfile 在不同系统或 CPU 架构(如树莓派、Mac M芯片)上都能通用——也就是如果你切确的知道这个镜像将会在哪里跑起来,那么你可以直接写死这部分内容

cat > /etc/apt/sources.list.d/adoptium.sources <<EOF

Types: deb

URIs: https://packages.adoptium.net/artifactory/deb

Suites: ${VERSION_CODENAME}

Components: main

Architectures: ${ARCH}

Signed-By: /usr/share/keyrings/adoptium.gpg

EOF

解析:使用 DEB822 格式(一种更现代、更安全的源配置格式)将 Adoptium 仓库写入系统的软件源配置中。其中动态注入了前面获取的系统代号 ${VERSION_CODENAME}、架构 ${ARCH},并指定了刚刚下载的公钥用于校验 (Signed-By)。

apt-get update

解析:由于刚刚添加了新的 Adoptium 源,必须再次更新列表,系统才能“看”到 Temurin JDK。

for i in 1 2 3; do

if apt-get install -y "temurin-${JAVA}-jdk" && break; then

true

else

echo "Attempt $i failed, retrying in 30s..."

sleep 30

fi

done

解析:这是一个带重试机制的安装脚本。它尝试安装 temurin-25-jdk(${JAVA} 变量被替换为 25)。如果因为网络波动下载失败,它会等待 30 秒后重试,最多尝试 3 次。这能大幅提高国内服务器构建镜像的成功率。

JAVA_HOME=$(find /usr/lib/jvm -maxdepth 1 -name "temurin-${JAVA}*" -type d | head -1)

ln -sf "${JAVA_HOME}/bin/"* /usr/local/bin/

解析: find 命令去 /usr/lib/jvm 目录下自动寻找刚刚安装的 JDK 实际路径,并赋值给临时变量 JAVA_HOME。 ln -sf 将 JDK bin 目录下的所有执行文件(如 java, javac)创建软链接到系统的 /usr/local/bin/ 中,这样在系统的任何地方都可以直接运行 java 命令。

java -version javac -version

解析:在构建过程中打印出 Java 和 Javac 的版本号。如果在构建日志里看到了对应的版本输出,说明安装成功。

apt-get clean rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

解析:清理 apt-get 留下的所有缓存包(.deb 文件),以及临时文件夹中的杂乱数据。这是构建优质 Docker 镜像的“黄金法则”,能让镜像体积缩小几十甚至上百兆。EOT 代表这段长脚本到此结束。

什么样

ARG JAVA=25

解析:ARG 定义了一个构建参数(仅在构建镜像时生效)。这里定义了变量 JAVA 并赋值为 25。这样做的好处是,如果未来需要升级到 Java 26,只需要修改这一个数字即可,后续脚本会自动调用它。

ENV JAVA_HOME=/usr/lib/jvm/temurin-${JAVA}-jdk

解析:ENV 指令用于设置运行时的环境变量(容器启动后仍然有效)。很多基于 Java 的服务端或插件在运行时会自动寻找系统环境中的 JAVA_HOME 变量来定位运行环境,这句话确保了程序的兼容性。


🏗️ 如何构建并使用你的镜像?

1. 执行构建命令

将上述任意一个 Dockerfile 保存后,打开终端进入该文件所在的目录。运行以下命令,让 Docker 照着“菜谱”开始做菜:

docker build -t my-mcdr-java25:latest .

参数解析:

  • build:告诉 Docker 执行构建操作。

  • -t my-mcdr-java25:latest:给你构建的镜像打个标签(Tag),名字叫 my-mcdr-java25,版本是 latest。你可以自己写一个自己看得懂的

  • .千万别漏掉这个点! 它代表构建上下文为当前目录——就是告诉docker,需要用于构建镜像的Dockerfiles在当前目录

2. 在项目中使用新镜像

构建完成后,通过 docker images 命令就能看到你新鲜出炉的自定义镜像了。

现在,你只需要在你的 docker-compose.yml 中,将原本的官方镜像名替换为你自己的镜像名即可(怎么写docker compose的看上一篇)

最后执行 docker compose up -d,一个完美适配 Java 25 且集成了 MCDR 框架的专属环境就跑起来了!

总结

其实构建镜像没什么难点,本期视频虽然以制作MCDR镜像为主,但是其他镜像的构建方式依然相同,“基于什么,加入什么,做什么,什么样”,围绕这么一句话即可