1. 从 ISO 到 Docker 镜像这个需求的真实场景拿到这个标题我猜你不是在玩票而是真的遇到了某个现实问题手头有一台不能随便连外网的机器或者某个业务系统指定要基于特定操作系统版本环境来跑供应商只给了一个 ISO 安装镜像文件但你的交付环境是 Docker。这种场景在信创适配、离线交付、传统业务容器化改造里太常见了。我在实际项目里遇到不少类似的诉求某客户有一套老业务系统当初是在特定操作系统环境下编译部署的现在要整体迁移到容器平台。你不可能找一台物理机装好系统再慢慢搬更不可能要求客户换底层环境最省事的办法就是把操作系统本身也做成一个 Docker 镜像让业务进程直接跑在这个镜像里行为表现和跑在物理机上几乎一致。这里说的“Docker 20.10.7 如何使用 kylin iso 文件构建出 docker 镜像”本质上是用 ISO 文件生成一个包含完整根文件系统rootfs的 Docker 镜像。不是把 ISO 当文件塞进镜像里而是把 ISO 里的操作系统内容变成镜像的文件系统层让容器一启动就像进了一个精简版系统。这条路径的核心价值有三个一是统一交付物整个环境变成一个镜像文件拷到哪都能跑二是屏蔽底层差异容器里看到的文件系统、目录结构、软件包管理方式都和你熟悉的一致三是可以在这个镜像基础上继续叠加中间件、业务代码形成完整的应用镜像。需要先说明一个基础认知Docker 镜像和系统安装镜像不是一回事。系统 ISO 是给安装器用的包含引导程序、安装包、内核等Docker 镜像是一个分层文件系统只包含运行所需的文件和元数据没有引导过程。所以“用 ISO 构建 Docker 镜像”的过程本质上就是“从 ISO 中提取出可用的 rootfs再把它包装成 Docker 镜像层”。2. 构建前你需要想清楚的三个关键决策2.1 用什么方式提取 rootfs实现路径不止一条我列一下常见选择你根据自己手上资源来定方案A把 ISO 挂载后直接拷贝文件系统。ISO 一般是一个 squashfs 或者类似格式的压缩只读文件系统挂载后文件看起来是完整的根目录结构用docker import直接导入目录。优点是命令少、适合快速验证缺点是对内核模块、特殊设备文件处理不够干净镜像可能比较大。方案B用livecd-tools或者febootstrap这类工具提取出干净的 rootfs。优点是能处理设备节点、符号链接、权限这些细节做出来的镜像更“像样”后续跑服务不容易出幺蛾子缺点是需要额外安装这类工具部分工具在特定系统版本上还有兼容性问题。方案C不提取而是用debootstrap或者yum --installroot这种包管理器原生方式构建一个干净的根文件系统然后把这个根文件系统做成镜像。优点是完全可控只装你需要的东西镜像体积能压得很小缺点是你需要一个同源环境的软件源如果是离线环境你得手头有完整的 rpm 包仓库。我在项目里最常用的是方案A加方案C的组合先用方案A快速验证流程是否通如果镜像太大或者缺东西再用方案C重构一版精简镜像。不同方式做出来的镜像我后面会把对比数据列出来你就明白为什么大多数场景下最终会选方案C了。2.2 镜像体积和纯净度的平衡很多人第一次做出来的镜像动辄 2GB、3GB因为 ISO 里塞了一堆你根本用不到的东西桌面组件、办公套件、开发库、语言环境、帮助文档全部被原封不动拷了进来。容器镜像的理想状态是“足够小且够用”。你在物理机里装系统多装点没什么在 Docker 里跑系统每一百兆都是传输和启动成本。所以构建前就得想清楚这个镜像要跑什么业务如果是跑一个 Java 服务那只需要基础 C 库、时区数据、glibc、bash、核心工具别的都可以不要。如果是跑数据库可能还需要一些系统库和初始化脚本。2.3 基础镜像用 scratch 还是别人的镜像这里涉及一个“洁癖”问题。有人会想直接用 Docker Hub 上某个带系统的镜像做基础层然后把 ISO 里的内容覆盖进去这样省事。但这样做不干净别人的镜像里可能有你不想要的环境变量、启动命令、默认配置甚至可能有潜在兼容问题。我的建议是基础层就用scratch这是 Docker 提供的空白镜像没有任何文件系统内容。你的 rootfs 作为唯一的一层压上去镜像内容完全由你控制没有继承任何不确定的东西。后面我会展示具体怎么操作。3. 前置准备先做这些检查和操作3.1 确认 Docker 版本和可用性标题里提到 Docker 20.10.7这算是一个比较新的稳定版本了。有一个点必须注意Docker 20.10 及以上版本对docker build的默认前端已经切换为 BuildKit虽然它会兼容老式docker build命令但在导入镜像、指定平台参数时行为略有差异。我有一套稳妥的做法就是构建时显式指定参数避免因前端不同导致意外。先确认 Docker 可用docker version docker info | grep -i storage driver存储驱动推荐 overlay2这是 20.10 的默认值。如果你发现是别的驱动比如 vfs 或者 aufs建议先调整到 overlay2否则导入大镜像时磁盘占用会翻倍。3.2 预处理 ISO 文件挂载还是解压拿到 kylin ISO 文件后先检查一下文件完整性避免 ISO 损坏导致挂载失败ls -lh kylin.iso sha256sum kylin.iso然后用mount直接挂载或者用7z x解压出全部内容。我习惯上先挂载因为挂载最快而且 ISO 里的 squashfs 文件在挂载后可以直接当作根文件系统来拷贝。mkdir -p /mnt/kylin_iso mount -o loop ./kylin.iso /mnt/kylin_iso ls /mnt/kylin_iso这里有个小坑部分 ISO 里不只有一个 squashfs 文件而是分了多个比如根文件系统一个、附加数据一个你需要逐个挂载后合并实际操作中我遇到过好几次所以第一步先完整看一遍目录结构。3.3 准备 Dockerfile 和构建目录我习惯建一个工作目录放 Dockerfile、构建脚本和可能需要往镜像里额外加的文件mkdir -p /opt/docker-kylin-build/{rootfs,scripts} cd /opt/docker-kylin-buildrootfs 目录就是等一下作为镜像内容的根目录后续提取出来的文件全部放这里。4. 实操全流程两条路径都走一遍4.1 方式一直接导入 ISO 中的完整文件系统这个方法适合你只是想快速拿到一个“能用的系统镜像”不太关心体积也不计划在这个镜像里跑特别复杂的服务。挂载 ISO 后找到 squashfs 文件并挂载它mkdir -p /mnt/kylin_squash mount -t squashfs /mnt/kylin_iso/LiveOS/squashfs.img /mnt/kylin_squash ls /mnt/kylin_squash在 kylin 这类系统的 LiveOS 目录中常见的结构是squashfs.img里面有一个LiveOS目录再里面是rootfs.img这个 rootfs.img 才是完整的系统根文件系统。你需要继续挂载或者直接把它当文件系统读出来。我实际踩过的坑是直接cp -a /mnt/kylin_squash/* /opt/docker-kylin-build/rootfs/速度非常慢因为里面有大量小文件另外cp -a在拷贝设备节点时偶尔报错。更稳的做法是用rsyncrsync -a --progress /mnt/kylin_squash/ /opt/docker-kylin-build/rootfs/拷贝完成后需要对 rootfs 做一些清理cd /opt/docker-kylin-build/rootfs rm -rf dev/* proc/* sys/* tmp/* run/* var/cache/* var/log/* mkdir -p dev proc sys tmp run清理完之后就可以导入 Docker 了tar --numeric-owner -C /opt/docker-kylin-build/rootfs -cf /opt/docker-kylin-build/kylin-rootfs.tar . docker import /opt/docker-kylin-build/kylin-rootfs.tar kylin:imported docker run -it --rm kylin:imported bash如果一切正常你会看到一个可以交互操作的 bash 提示符并且文件系统内容就是 ISO 里的内容。这个方式的优点是操作简单缺点也很明显镜像巨大、包含大量无用软件包、内核模块可能不完整、启动时可能会因为缺少某些系统服务而报错。我的建议是性能要求不高的内部调试或验证环境可以用真正上生产不推荐。4.2 方式二用 Dockerfile 基于 scratch 构建精简镜像先构建一个干净的 rootfs。这个环节我强烈推荐用包管理器原生方式因为整个文件系统内容完全可控。架构大概是镜像里只有基础环境、核心工具和必要的运行库其他一概不要。构建用的脚本大致长这样mkdir -p /opt/kylin-rootfs/rootfs yum --installroot/opt/kylin-rootfs/rootfs \ --releasever8 \ --setoptinstall_weak_depsFalse \ --skip-broken \ -y install bash coreutils findutils grep sed gzip tar vim-minimal passwd注意--installroot必须指向一个空目录或新目录这样生成的 rootfs 是一个没有内核、没有引导、只包含运行所需工具链的最小系统。这个步骤需要软件源可用如果你在离线环境需要把 ISO 自带的 rpm 包目录挂载到本地镜像源再配置 yum repo这个细节我会在下一章详细说。得到 rootfs 后稍微调整一下配置chroot /opt/kylin-rootfs/rootfs /bin/bash -c echo root:root | chpasswd; echo LANGen_US.UTF-8 /etc/locale.conf; 这一步你会发现chroot 到自己的 rootfs 里执行命令时能直接验证这个文件系统是不是一个“健康的”系统。如果 bash 都起不来或者chpasswd报错说明基础包没装对。然后写 DockerfileFROM scratch COPY --chown0:0 rootfs/ / ENV LANGen_US.UTF-8 \ LANGUAGEen_US:en \ LC_ALLen_US.UTF-8 CMD [/bin/bash]构建docker build -t kylin:minimal -f Dockerfile .这里COPY --chown0:0显式把所有文件归属改为 root因为从 ISO 提取的文件偶尔会带着奇怪的属主信息。FROM scratch 的好处是镜像层里没有任何不相关内容每一层都是你显式添加的。构建出来的镜像体积可以做对比方式一通常 2–3GB方式二通常可以控制在 300–500MB。如果连 bash 和 coreutils 都不需要只跑单一静态编译二进制体积可以压到 100MB 以下。4.3 验证镜像是否真的可用镜像构建出来不等于能用我在实践中发现最常见的三类问题网络不通、软件源不可用、PID 1 进程行为异常。先做一个基础验证docker run --rm -it kylin:minimal /bin/bash cat /etc/os-release uname -a ip addr注意容器里看到的 uname 内核版本是宿主机的因为容器共享宿主机内核这是正常的不代表镜像有问题。然后验证网络docker run --rm kylin:minimal ping -c 4 127.0.0.1 docker run --rm kylin:minimal curl -I https://mirror.example.com如果 rootfs 里没装 ping 和 curl就用yum install iputils curl补上或者直接用 bash 的/dev/tcp测试。5. 常见问题与排查技巧实录5.1 yum 源配置报错找不到 repo 或 GPG 校验失败如果你用方式二构建 rootfs离线环境下 yum 源配置文件是需要手动写入的。用 ISO 自带的 rpm 包目录做本地源需要确认两件事ls /mnt/kylin_iso/ find /mnt/kylin_iso -name repodata -type d如果 repodata 目录存在说明这个 ISO 自带软件源仓库。写到 yum 配置里[local] nameLocal Kylin Repo baseurlfile:///mnt/kylin_iso enabled1 gpgcheck0其中gpgcheck0一般用于离线内网环境但如果你有官方 GPG key更安全的做法是保留gpgcheck1并导入 key。提示构建时gpgcheck0省事但交付给别人的镜像里不要残留这个配置避免安全审计出问题。5.2 chroot 后不能用 systemctl 或 systemd 相关命令容器里通常没有 systemd 作为 PID 1你直接systemctl start xxx大概率会报 “System has not been booted with systemd as init system” 错误。这不是你的镜像坏了而是容器设计本来就不管这些。解决方案不依赖 systemd而是用 Docker 的CMD或ENTRYPOINT直接拉起进程。如果确实是遗留业务依赖 service 脚本就用/etc/init.d/xxx start或者写一个包装脚本模拟启动流程。我在实际项目中见过一些老服务明明只需要 nginx 和 java 进程却非要初始化一堆 systemd 服务最后只能写个 shell 脚本逐个启动效果也一样。5.3 网络通但 DNS 不正常容器内 /etc/resolv.conf 一般由 Docker 自动生成但你从 ISO 导入的 rootfs 里可能残留了物理环境里的 DNS 配置导致容器启动后 DNS 冲突。处理方法docker run --rm --dns 223.5.5.5 --dns 8.8.8.8 kylin:minimal cat /etc/resolv.conf如果只是临时验证用--dns参数就行如果长期使用建议构建时把 /etc/resolv.conf 清空让 Docker 自动注入rm -f /opt/kylin-rootfs/rootfs/etc/resolv.conf5.4 镜像做出来了但启动就退出看不到任何日志这是我踩得最多的一类问题。大概率是 PID 1 进程直接返回了。Docker 容器的生命周期绑定在 PID 1 上PID 1 一退出容器就结束。如果你CMD [/bin/bash]bash 没有交互输入读不到 stdin 会立刻退出看起来就是“启动就挂”。正确做法是加-it参数做交互式验证真正运行服务时CMD 应该是类似CMD [/usr/local/bin/start.sh]这样能持续运行的前台脚本。containers 里永远不要用nohup ... 加CMD然后指望容器不退出启动完所有进程后脚本必须进入前台阻塞状态。5.5 架构不匹配导致无法运行如果你的宿主机是 x86_64 架构但你的 ISO 是 ARM64 架构的构建出的镜像在 x86 宿主机上是没法直接运行的。Docker 20.10 支持跨架构模拟但默认不开启。需要装binfmt_misc支持并设置平台参数docker run --rm --platform linux/arm64 /bin/bash但这个依赖宿主机的内核和 qemu-user-static环境复杂程度高。我的建议是除非非做不可否则尽量使用和宿主机同架构的 ISO 构建镜像。实际操作中因为是交叉编译的情况直接构建出来的二进制在模拟环境里性能差而且排查问题会有很多噪音。5.6 做一个快速排查清单症状排查路径常见解法启动即退出检查 CMD 是否前台驻留加上tail -f /dev/null或改用前台启动脚本网络不通检查 resolv.conf、iptables重新配置 DNS确认宿主机防火墙放行容器网段yum 安装报错检查 repo 路径、GPG 校验核对 repodata 位置临时设置 gpgcheck0 测试中文乱码检查 locale 和字体包安装 glibc-langpack-zh 或设置 en_US.UTF-8时间不对容器时区默认UTC安装 tzdata 并设置 TZ 环境变量文件权限怪检查 chown 是否会覆盖构建时加 COPY --chown0:0rootfs 太大检查是否把 ISO 全量拷入改用包管理器安装最小依赖包systemctl 报错容器没有 systemd改用脚本启动或 entrypoint 直接拉进程6. 镜像交付与后续扩展建议镜像构建验证通过之后交付方式很简单把镜像保存成 tar 包拷到目标机器上再 load 进去。docker save kylin:minimal -o kylin-minimal.tar # 在目标机器上 docker load -i kylin-minimal.tar有一种更省事的方式直接用 Dockerfile 引用这个镜像作为基础镜像继续叠加你自己的业务层。比如在构建机上已经有这个镜像后续新项目直接写FROM kylin:minimal COPY app.jar /opt/app.jar ENTRYPOINT [java, -jar, /opt/app.jar]再往下扩展你还可以把这个镜像配合 Jenkins 或 GitLab CI 做流水线每次代码提交后基于这个基础镜像现场构建完整业务镜像彻底解决“开发环境能跑、测试环境不能跑”的老问题。另外想到一个可以顺手做的事给这个镜像加一个/etc/entrypoint.sh在里面统一处理环境变量注入、配置模板替换、初始化数据库之类的动作这样容器启动时能做到完全配置化不需要改镜像。这算是从“能用”到“好用”的一步关键进化。一些实际操作后的心得这条路我前后跑了很多遍总结下来几个经验如果是做验证用docker import的方式最快半天能出结果如果是做交付老老实实用包管理器构建精简 rootfs省下的体积和时间在传输部署阶段会让你很舒服。顺带一个反复强调的细节构建完后别急着删构建目录rootfs 目录保留一份后面要往里补包或者修复问题都能直接用 chroot 修改并重新打包比重新走一遍 ISO 提取速度快得多。