
Docker 部署 Rocky Linux轻松搭建 RHEL 兼容企业级基础镜像平台前阵子帮客户迁移一套跑了五年的业务系统原环境是 CentOS 7用的还是经典的 Docker 多容器架构。迁移一开始就撞上硬墙Red Hat 订阅到期CentOS 8 停更CentOS Stream 的滚动更新模式客户不敢用。最终我把目光落在了 Rocky Linux 上——它是 RHEL 的二进制兼容发行版继承 RHEL 8/9 的完整生态没有订阅成本社区维护稳定配合 Docker 镜像做企业级基础平台完全可行。这篇文章把整个落地过程、踩过的坑、以及最后沉淀下来的基础镜像搭建思路全部拆开讲适合正在做 CentOS 替换、RHEL 兼容环境迁移、或者在 Docker 里跑 RHEL 系业务镜像的团队参考。全文会覆盖几个核心问题为什么选 Rocky Linux 而不是其他 RHEL 兼容发行版、官方镜像怎么拉取和验证、企业级基础镜像怎么定制系统源、时区、用户、安全基线、容器静态 IP 怎么配、以及从镜像构建到实际跑业务MySQL、Redis、LibreOffice的完整链路。最后会聊聊 Windows 宿主机上 Docker Desktop 的常见坑和基础镜像瘦身的进阶玩法。1. 为什么我最终把基础镜像从 CentOS 换成了 Rocky Linux1.1 CentOS 停更后的替代路径对比CentOS 7 已经在 2024 年 6 月终止维护CentOS 8 停得更早2021年底就 EOL 了。市场上替代方案无非几条路AlmaLinux、Rocky Linux、Oracle Linux、或者直接上 RHEL 开发者订阅。我用一张表把关键差异列出来这也是当时选型的核心依据。发行版RHEL 兼容性更新方式商业支持社区活跃度镜像生态Rock Linux二进制兼容稳定版保守可选CIQ 提供高社区驱动Docker Hub 官方镜像完善AlmaLinux二进制兼容稳定版保守可选TuxCare 提供高CloudLinux 驱动官方镜像完善Oracle Linux兼容但有偏移稳定版 UEK 内核Oracle 支持中等存在但商业味道重RHEL 开发者订阅原生稳定版Red Hat 官方官方需订阅适合生产合规场景我当时选择的决定因素是“不偏科”Rocky Linux 由 CentOS 联合创始人 Gregory Kurtzer 主导继承 CentOS 的制镜流程和社区基因RHEL 兼容性测试覆盖广Docker Hub 上镜像更新节奏稳定。AlmaLinux 也很好但它的商业驱动来自 CloudLinux部分用户会有意回避商业公司色彩。至于 Oracle Linux除非你已经在用 Oracle 生态否则没必要引入额外的绑定感。1.2 Rocky Linux 与 RHEL 的兼容边界很多新手会问Rocky Linux 是不是可以完全当作 RHEL 用结论是应用层基本可以内核层和商业认证层有边界。Rocky Linux 基于 RHEL 源码构建用户态程序、运行库、开发工具链、系统服务都能对上号RPM 包直接拿来装基本不会出问题。这是它适合做 Docker 基础镜像的根本原因——容器里跑的本来就不是完整内核而是用户态文件系统只要 glibc、库文件、系统工具与 RHEL 兼容业务层就感受不到差异。边界在哪里一是 RHEL 有 Red Hat 官方认证的驱动和第三方生态比如某些数据库、安全加固工具只对 RHEL 做官方支持Rocky Linux 虽然能跑但厂商“不承诺兜底”二是内核补丁路径不完全一样Rocky 的 kernel 包来自 RHEL src.rpm但签名密钥和构建链路不同个别高度依赖内核特性的场景需要单独验证。放到 Docker 里默认共享宿主机内核这个问题基本被绕开了。注意如果你的业务包含专有内核模块、DSDK/DPDK 这类用户态驱动、或者强依赖特定内核版本的场景需要在 Rocky Linux 宿主机上做一次跑批验证别直接拿容器做生产。1.3 这层“兼容性”到底能省多少钱从成本角度看这层兼容性价值巨大。企业迁移到 RHEL 体系通常涉及三类成本授权订阅费、迁移改造费、运维培训费。Rocky Linux 直接砍掉第一项第二项因为二进制兼容而大幅缩减——绝大多数 RPM 包不需要重打包Dockerfile 里的 FROM centos:7 直接改成 FROM rockylinux:9 就能跑通大部分场景。运维培训基本为零因为命令体系、配置文件路径、systemd 服务管理逻辑和 RHEL 一模一样。不过要提醒一句省成本的前提是团队对 RHEL 体系本身熟悉。如果团队只懂 Debian/Ubuntu换到 Rocky 之后yum/dnf 包管理、firewalld、SELinux 这些差异项还是需要学习成本的。放到容器镜像里影响小但宿主机层面躲不开。2. 拉取官方镜像第一道关卡往往是网络和架构2.1 从 Docker Hub 拉取 Rocky Linux 镜像基础镜像搭建的第一步是拉取官方镜像。Rocky Linux 官方镜像名是rockylinux有9、9-minimal、8、8-minimal等标签。其中minimal系列体积更小适合做运行时基础层标准标签带完整包管理器工具链适合做构建层。docker pull rockylinux:9 docker pull rockylinux:9-minimal docker pull rockylinux:8拉取完成后用docker images确认镜像信息和大小docker images | grep rockylinux标准版镜像一般 170MB 左右minimal 版 90MB 左右相比 Ubuntu 24.04 的 80MB 略大但比你自己从头做一个小型发行版要省事得多。镜像内部默认用户是 root包管理器是 dnfRocky 8 里是 yum 但指令兼容 dnf。国内网络环境拉 Docker Hub 镜像容易卡在下载阶段Docker 镜像下载慢是高频问题。处理方式建议优先配置镜像加速器而不是反复重试。Docker Desktop 和 Linux 版 Docker Engine 的配置路径不同但原理一致修改 registry-mirrors把https://docker.m.daocloud.io这类加速地址挂上去。需要注意加速器只对 Docker Hub 官方仓库生效第三方仓库不受影响。2.2 验证镜像签名与完整性安全是基础镜像的第一红线。容器镜像可能被篡改或替换尤其在企业内网环境拉取镜像后建议做两层验证第一层是哈希值docker images --digests可以查看镜像在仓库中的 digest与 Docker Hub 上官方公布的 digest 比对第二层是启动容器后验证 RPM 包签名Rocky Linux 官方源里的包都有 GPG 签名执行以下命令确认签名链路完好docker run --rm -it rockylinux:9 bash容器里执行rpm -qa --qf %{NAME}-%{VERSION}-%{RELEASE} %{SIGPGP:pgpsig}\n | head如果输出中包含gpg相关的签名信息基本可以确认镜像内部的包未被篡改。这块容易被忽略但企业环境里建立“镜像供应链可信”意识还是有必要性的。2.3 验证架构匹配x86、ARM 与多架构镜像Docker Hub 上的 rockylinux 官方镜像支持多架构docker pull时会根据宿主机 CPU 架构自动拉取对应平台版本。但要注意一种场景在 Apple Silicon Mac 上开发镜像构建时默认是 arm64推到 x86 服务器上运行可能拉取到的是 x86_64 版本没问题但如果你构建的是自定义镜像FROM rockylinux:9 之后没有自动跨架构的能力需要用docker buildx做多架构构建。docker buildx build --platform linux/amd64,linux/arm64 -t myregistry/rocky-base:9 . --push如果忽略这一点最典型的问题是在 arm 机器上装了一个 x86 的私有包运行时报Exec format error。踩过一次之后我现在的做法是所有自定义基础镜像统一用 buildx 构建两份架构推送前用docker manifest inspect确认镜像平台列表。3. 定制基础镜像的三个核心动作系统源、时区、用户体系3.1 替换 dnf 源为企业内网源或国内加速源镜像拉下来以后第一件事不是写业务代码而是把默认 yum/dnf 源调整到可达的源。容器里默认的源是 Rocky 官方源国内服务器直连有时会很慢企业内网通常又要求走私有源。我习惯在 Dockerfile 里直接把源替换掉下面是 Rocky 9 的写法FROM rockylinux:9 # 替换为国内加速源也可以替换成企业内网源 RUN sed -e s|^mirrorlist|#mirrorlist|g \ -e s|^#baseurlhttp://dl.rockylinux.org/$contentdir|baseurlhttps://mirrors.aliyun.com/rockylinux|g \ -i.bak \ /etc/yum.repos.d/rocky.repo \ /etc/yum.repos.d/rocky-extras.repo \ /etc/yum.repos.d/rocky-devel.repo \ dnf clean all \ dnf makecache这段 sed 命令的作用是把默认的 mirrorlist 注释掉启用 baseurl 并指到镜像站。不同站点的 URL 结构略有差异替换前建议先 curl 一下确认路径可访问。做完之后立刻dnf clean all dnf makecache验证源是否可用的最快方式是安装一个小包docker run --rm -it --name rocky-source-test rockylinux:9 bash -c dnf install -y vim-minimal能顺利装完就说明源正常。很多团队把这一步漏掉直到构建业务镜像时反复超时才回头补配置浪费时间。3.2 时区、语言、编码这些看似不起眼的配置时区和语言配置在容器里看似无关紧要实际影响很大。日志时间差 8 小时、Python 脚本处理中文乱码、Java 应用编码错误全是这些基础配置没做导致的。Dockerfile 里建议统一配置ENV TZAsia/Shanghai \ LANGen_US.UTF-8 \ LANGUAGEen_US.UTF-8 \ LC_ALLen_US.UTF-8 RUN dnf install -y tzdata glibc-langpack-en \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ echo $TZ /etc/timezone \ dnf clean allglibc-langpack-en这个包很关键Rocky 9 默认不带完整英文语言包没有它即使设置了 LANGlocale 也不生效。这块踩过坑的人应该懂容器里跑locale命令永远报 warning应用日志里各种 “couldnt set locale”。3.3 创建非 root 用户容器安全的底线操作企业级镜像里长期用 root 跑业务是安全大忌。容器虽然有一定隔离性但 root 用户配合 Docker 的默认配置一旦被攻破就可能影响宿主机。RHEL 系镜像里创建一个业务用户很简单RUN groupadd --gid 10001 app \ useradd --uid 10001 --gid app --shell /sbin/nologin --create-home appuid/gid 建议固定写死好处是跨环境保持一致卷挂载时文件权限不会出现“开发环境正常、生产环境读取失败”的怪异问题。.ssh目录、日志目录、数据目录都归这个用户业务容器跑起来以后进程权限被限制在普通用户级别。最佳实践是用户创建之后业务入口指定USER app同时给需要写入的目录提前 chown。注意端口 1024 以下是特权端口普通用户绑定需要额外权限所以容器内部业务端口尽量用 8080、8443 这类高位端口或者额外用NET_BIND_SERVICEcapability但这种情况生产环境较少见。还需要留意nologin用户和sbin/nologin的区别。如果把/sbin/nologin设置为 shell用户无法通过交互 shell 登录但容器内docker exec仍然可以以该用户执行命令通过docker exec -u app指定这是符合安全预期的。如果你希望某些场景能临时进入容器排查可以额外建一个debug用户或者保留app用户的 shell 为/bin/bash但锁定 SSH 登录。根据我的经验基础镜像里用nologin 必要的docker exec -u组合是安全和可运维性的不错平衡点。4. 容器网络的静态 IP 问题从 bridge 到自定义网络4.1 为什么容器 IP 会一直变怎么固定下来Docker 容器默认接入bridge网络每次重建容器都会重新分配 IP。这在开发环境无所谓但涉及服务间互相发现、白名单配置、固定域名解析时IP 漂移就是事故源头。企业里跑多个容器组成内部平台如果服务 A 依赖服务 B 的 IP配置里写死 IP一旦 B 重建A 就失联。我早期的解决思路是给每个容器指定--ip在自定义 bridge 网络上设置子网和固定 IPdocker network create \ --driver bridge \ --subnet172.20.0.0/16 \ --gateway172.20.0.1 \ app_net docker run -d \ --name rocky-app \ --network app_net \ --ip 172.20.0.10 \ rockylinux:9 \ sleep infinity这样容器重启、重建之后 IP 基本不会变。但注意--ip只能在自定义网络使用默认bridge网络不支持指定 IP。还要避免 IP 冲突所以子网要大一些生产环境建议每个项目独立子网。4.2 Docker Compose 场景下的静态 IP 配置实际生产里很少只有一个容器通常用 docker compose 编排服务。Compose 里配置静态 IP 的方式更简洁services: rocky-app: image: local/rocky-base:9 networks: app_net: ipv4_address: 172.20.0.10 networks: app_net: ipam: config: - subnet: 172.20.0.0/24 gateway: 172.20.0.1这种写法的好处是 IP 规划一目了然运维接手时看配置文件就能理解网络拓扑。主机名可以直接用服务名互访Compose 网络内置 DNS 解析所以代码里尽量用rocky-app这类服务名而不是把 IP 写死在应用配置里。4.3 静态 IP 在重启和跨宿主机场景下的边界静态 IP 不是万能的。单宿主机上用自定义 bridge 固定 IP能做到容器重建后 IP 稳定一旦涉及跨宿主机容器迁移IP 可能冲突。生产环境如果有很多机器建议不要过度依赖静态 IP直接引入 overlay 网络Swarm 模式或者 K8s做服务发现业务代码里全部走服务名或 DNS 解析。另外Rocky 镜像本身是“无 systemd”运行模式的容器容器里的网卡配置、network-scripts 这些是用不上的网络配置完全由 Docker 宿主机侧接管。有个细节如果你在容器内执行ip addr看到的 eth0 就是 Docker 分配的虚拟网卡修改容器内网卡配置没有实际意义重启即恢复。这一点和物理机/虚拟机里 Rocky Linux 设置静态 IP 完全是两回事别把传统 Linux 的网络管理经验直接套进容器。如果你是在 Docker DesktopMac/Windows上跑 Rocky 容器静态 IP 会有一个更隐蔽的限制Docker Desktop 自带一层轻量虚拟机自定义 bridge 子网要和宿主机的网段错开否则可能出现容器 IP 出不去或者宿主机访问不到容器的问题。我这边遇到过172.18.0.0/16和公司 WiFi 网段冲突的案例最后把容器子网换到172.30.0.0/16才解决。5. 实战组合用 Rocky 基础镜像搭建 MySQL8.0、Redis 和 LibreOffice 服务5.1 基于 Rocky 9 的 MySQL 8.0 容器镜像定制MySQL 官方镜像已经做得很好但有些企业因为安全审计、合规要求或者离线交付必须从基础镜像自建 MySQL 镜像。我在 Rocky 9 基础镜像上安装 MySQL 8.0 时踩过几个坑这里直接给出可复用的 Dockerfile 思路。Rocky 9 默认源仓库自带的 MySQL 版本比较旧推荐用 MySQL 官方 Yum 源安装指定版本。安装后最麻烦的问题是容器内没有 systemdmysqld不会自动启动。所以基础镜像里不能只装包还要写一个入口脚本来初始化数据目录。FROM local/rocky-base:9 RUN dnf install -y https://dev.mysql.com/get/mysql80-community-release-el9-1.noarch.rpm \ dnf install -y mysql-community-server \ dnf clean all ENV MYSQL_ROOT_PASSWORDChangeMe123 \ MYSQL_DATABASEappdb COPY entrypoint.sh /usr/local/bin/ RUN chmod x /usr/local/bin/entrypoint.sh EXPOSE 3306 ENTRYPOINT [/usr/local/bin/entrypoint.sh]entrypoint.sh 里的核心逻辑就三件事如果/var/lib/mysql目录为空就初始化、启动 mysqld、执行用户传入的初始化 SQL。#!/bin/bash if [ ! -d /var/lib/mysql/mysql ]; then mysqld --initialize-insecure --usermysql fi if [ -n $MYSQL_DATABASE ]; then mysqld --usermysql --daemonize mysql -uroot -e CREATE DATABASE IF NOT EXISTS ${MYSQL_DATABASE}; mysql -uroot -e ALTER USER rootlocalhost IDENTIFIED BY ${MYSQL_ROOT_PASSWORD}; mysql -uroot -p${MYSQL_ROOT_PASSWORD} -e FLUSH PRIVILEGES; # 保持前台运行 exec mysqld --usermysql fi这里--initialize-insecure是为了在无密码环境下初始化数据目录生产环境建议换成--initialize并配合初始密码的管理。这种自建 MySQL 镜像适合离线交付、定制化配置较多的场景如果只是跑常规业务直接用官方 mysql 镜像会省很多事。5.2 Redis 主从配置在 Rocky 容器里的实践Redis 在容器里跑主从直接用官方 redis 镜像已经够爽。但如果你需要 Redis 跑在 Rocky/RHEL 体系里方便统一安全基线、统一日志收集可以从 Rocky 基础镜像构建。官方redis镜像基于 Debian日志路径、配置文件路径、权限模型都和 RHEL 系不一样。全部统一到 Rocky 之后监控脚本、日志采集、yn 配置文件可以复用同一套运维体系。我这边搭建一个小规模 Redis 主从集通常用 docker compose 直接编排services: redis-master: image: local/rocky-redis:7 command: [redis-server, /etc/redis/redis.conf] networks: app_net: ipv4_address: 172.20.0.21 volumes: - redis-data:/data redis-replica: image: local/rocky-redis:7 command: [redis-server, /etc/redis/redis.conf, --replicaof, redis-master, 6379] networks: app_net: ipv4_address: 172.20.0.22 depends_on: - redis-master注意 RHEL 系的 redis 包默认配置里protected-mode yes如果跨容器访问要显式配置bind 0.0.0.0或者指定容器网段。生产环境别图省事绑定所有网卡直接在 redis.conf 里写bind redis-master 127.0.0.1单机或bind 172.20.0.21绑定容器 IP配合密码认证和 namespace 隔离效果更好。5.3 离线装 LibreOffice 到 Rocky 容器的避坑记录有一类需求比较特别在容器里跑文档转换服务比如用 LibreOffice 把 office 文件转 PDF供业务系统在线预览。很多团队直接用libreoffice官方 Linux 安装包一步到位但在 Rocky 容器里会遇到缺依赖的问题。我这边尝试安装LibreOffice 7.4.7.2的 RPM 版本时常规做法是下载官方 RPM 包然后dnf install -y ./LibreOffice_7.4.7.2_Linux_x86-64_rpm/RPMS/*.rpm结果报依赖错误缺少一堆库比如libcups.so.2、libfontconfig.so.1、libXinerama.so.1等。这些库在 Docker 最小化镜像里默认不会安装。排查链路是这样的先看具体缺哪些依赖用ldd检查 LibreOffice 主程序逐步补齐基础库。我的经验是直接安装以下包组可以覆盖大部分场景dnf install -y \ cups-libs fontconfig freetype libXinerama libXext libXrender \ libXrandr libXi libX11 libXau libxcb libXdmcp libXfixes \ dbus-libs glib2 libICE libSM libuuid libffi \ poppler-data urw-base35-fonts \ wqy-zenhei-fonts wqy-microhei-fontsurw-base35-fonts是标准 PDF 字体集wqy-*是中文字体。做文档转换服务的镜像如果缺中文字体中文全部显示成方块线上被投诉到飞起。这个问题排查报告专门写了一段先确认fc-list :langzh是否输出中文字体没有就装wqy-zenhei-fonts和wqy-microhei-fonts。装完库和字体后用libreoffice --headless --convert-to pdf做一次转换测试。我实测下来在 Rocky 9 容器里跑 LibreOffice 7.4.7.2转换速度比 Debian 基础镜像里快一点应该和 glibc 版本差异有关。这个组合比较适合做企业内部文档服务。5.4 构建、推送、使用的完整流水线基础镜像只有在被业务镜像引用时有价值。我会把基础镜像推到私有镜像仓库业务镜像 Dockerfile 里直接写FROM registry.internal.local/rocky-base:9构建流水线里把“源替换、用户创建、时区设置”放在基础镜像阶段完成业务镜像只负责叠加代码和依赖。这样业务开发不需要了解基础环境细节基础镜像团队统一升级安全补丁后下游重建镜像即可。流水线建议用 GitHub Actions 或 GitLab CI 触发构建完自动跑一组冒烟测试容器是否能启动、是否能安装业务包、时区是否正确、非 root 用户是否能正常写数据目录。这些测试不过就直接阻断镜像推送避免脏镜像污染环境。6. Windows 宿主机上的 Docker DesktopRocky 容器运行时的几个硬伤6.1 Virtualization support 检测失败的排查Docker Desktop 在 Windows 上最经典的问题就是启动时报Virtualization support not detected. Docker Desktop failed to start because virtualization support wasnt detected.这个报错的原因通常有三类Windows 功能里没开启 Hyper-V 或 WSL 2、BIOS/UEFI 里 CPU 虚拟化被关闭、或者杀毒软件/安全策略拦截了 Hyper-V 服务。排查链路分三步走。第一步确认 Windows 虚拟化功能是否启用。管理员 PowerShell 执行Get-WindowsOptionalFeature -Online | Where-Object {$_.FeatureName -like *Hyper* -or $_.FeatureName -like *VirtualMachine*}如果Microsoft-Hyper-V-Hypervisor没启用执行Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -All -NoRestart。第二步确认 CPU 虚拟化是否开启。任务管理器-性能-CPU查看“虚拟化”状态。如果显示“已禁用”需要进 BIOS/UEFI找 Intel VT-x / AMD-V 选项打开。这一步对绝大多数办公电脑都适用但部分轻薄本 BIOS 隐藏了虚拟化开关需要更新 BIOS 才出现。第三步确认 WSL 2 是否正常。Docker Desktop 现在的后端默认走 WSL 2直接执行wsl --status看版本。如果版本是 1执行wsl --set-version distro 2。很多时候报错不是虚拟化没开而是 WSL 内核太旧去 Microsoft Store 更新 WSL 即可。注意如果你的 Windows 开了内核隔离基于虚拟化的安全性个别驱动冲突可能导致 Docker Desktop 反复起不来。可以临时关闭内核隔离测试确认之后再决定是否保留。6.2 WSL 2 模式下 Rocky 容器文件性能和挂载问题Windows 上跑 Rocky 容器默认是放在 WSL 2 的轻量虚拟机里。WSL 2 的跨文件系统性能是真的拉胯容器里写/var/lib/mysql这类数据目录没问题但挂载 Windows 目录/mnt/c/...做文件读写时磁盘 IO 和 inotify 事件都有明显延迟。如果你在 Windows 上开发 Node/Python 项目挂载 Windows 目录跑测试可能会遇到文件变更监听失效、写文件慢、MySQL 初始化超时之类的问题。我在 WSL 2 里跑 Rocky 容器做 LibreOffice 文档转换时发现挂载 Windows 目录比挂载 WSL 内部目录慢 5 倍以上。建议实践开发阶段不要把仓库放在/mnt/c下而是放 WSL 2 内部比如~/projects再把工作目录挂载进容器。数据类容器比如 MySQL、Redis直接用 Docker Volume 而不是 bind mount。这样能大幅避开 WSL 2 的性能坑。6.3 Docker Desktop 的镜像加速配置其实比命令行更简单Docker Desktop 设置里直接有 Docker Engine 配置 Json在这里加 registry-mirrors 即可。很多教程让初学者去该配置文件但 Docker Desktop 的 GUI 配置更安全不会因为 Json 格式错误导致整个 Docker 引擎崩掉。{ registry-mirrors: [ https://docker.m.daocloud.io ] }改完点击 Apply Restart拉镜像速度会明显改善。如果你的公司有内网镜像仓库把内网地址放到列表第一位Docker 会优先尝试失败了再走后面的加速器。Windows 宿主机的另一个常见问题是大镜像构建时磁盘占用暴涨C 盘红了 Docker 就卡死。Docker Desktop 设置里可以改虚拟磁盘位置把它挪到 D 盘同时定期执行docker system prune清理悬空镜像和构建缓存。企业里给开发机装 Docker Desktop 时建议在首次启动前就把磁盘路径改到空间充裕的盘符否则用到一半再迁移很麻烦。7. 面向生产的镜像瘦身与安全加固基础镜像能走多远7.1 把镜像从 500MB 压到 180MB 的实操手法Rocky Linux 基础镜像本身不大但装完各种业务依赖之后很容易膨胀。镜像瘦身的核心不是追求小而是“删掉永远不会用到的文件”。第一招是多阶段构建。构建阶段用带完整工具链的rockylinux:9运行阶段切到rockylinux:9-minimal只复制必要的二进制。以 LibreOffice 为例构建镜像里编译/安装 RPM运行镜像里只保留安装后的文件。第二招是清理 dnf 缓存和临时文件。Dockerfile 里每执行一次dnf install都会产生/var/cache/dnf缓存最终垃圾全被 commit 进镜像层。正确的做法是在同一 RUN 指令里安装和清理RUN dnf install -y --setopttsflagsnodocs \ tzdata glibc-langpack-en \ dnf clean all \ rm -rf /var/cache/dnf /tmp/*--setopttsflagsnodocs是 RHEL 系镜像瘦身的惯用招数它跳过 man page 和 doc 文件安装。单这一项能减少 10% 左右体积而且对运行没有任何影响。第三招是删除不必要的语言包和 locale。Rocky 里glibc-langpack-*包体积不小如果只需要英文和中文就只装对应语言包dnf install -y glibc-langpack-en glibc-langpack-zh还有coreutils里带的一堆辅助命令如果有洁癖可以裁剪但生产环境不建议动系统包风险收益不成正比。7.2 基础镜像的安全基线用户、补丁、包签名、运行时权限安全加固是基础镜像区别于“能用”和“可交付”的分水岭。我在搭 Rocky 基础镜像时固定了一组安全基线这里列举直接能抄的几条。一是禁用 root 远程登录如果容器内跑了 SSH并把 SSHD 端口改掉。但容器里跑 SSH 本来就是少数场景多数业务容器不需要 SSHD直接从镜像层面不安装 openssh-server攻击面更小。二是及时打安全补丁。基础镜像每次重建时dnf update -y这一步看起来简单但很多团队的 Dockerfile 里没有写导致镜像里的 glibc、openssl 长期停留在旧版本。CVE 扫描器一旦上线问题会集中暴露。建议 CI 里加docker scout cves或者 Trivy 扫描扫描结果不通过不推送。三是文件权限收敛。业务镜像里/etc/passwd、/etc/shadow、密钥目录、日志目录的属主和权限要做统一约束。特别是密钥类文件容器里经常出现 644 权限的私钥文件SSH 直接用不了还是小事泄露风险才是大事。四是用cap-drop和security-opt控制容器运行时权限。如果业务不需要NET_RAW、SYS_ADMIN这类高危 capability在 docker compose 或 k8s 里显式 drop 掉security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE这套能力和基础镜像本身配合即使容器被攻破横向移动的能力也受到极大限制。7.3 镜像仓库的 tag 规范与不可变 tag 策略基础镜像的 tag 管理直接决定线上可追溯性。最糟糕的实践是给每一个镜像打latest然后依赖“最新的永远是好的”。发布策略稳定之后线上事故有一半是这种 tag 混乱导致的。我现在的规范是基础镜像 tag 用《Rocky 大版本-日期-构建号》格式比如9-20250616-1。业务镜像是应用名-环境-日期-构建号每次推镜像都生成唯一 tag线上部署记录里引用具体 tag回滚时直接切到上一个 tag。latest只允许出现在开发环境生产环境不允许拉取latest。数据类容器不要随便重建标签。比如 MySQL 镜像一旦用了9-20250616-1这个 tag后续修复漏洞也只允许创建新 tag不允许覆盖旧 tag。这样任何时间点的部署镜像内容和几天前验证过的内容完全一致不会出现“同样的 tag不同的镜像”这种隐蔽事故。8. 完整排查链路一次从“容器起不来”到“静态 IP 冲突”的实战定位最后分享一次真实排障过程这个问题混合了 Rocky 基础镜像、数据目录权限、静态 IP 冲突多个因素排查链路有代表性。现象某天客户报障redis-master容器重建后一直处于Restarting状态redis-replica连接超时。当时第一反应是 Redis 配置文件写坏了直接进去看日志docker logs redis-master --tail 50日志显示 Redis 正常加载配置但紧接着FATAL CONFIG FILE ERROR报错指向bind配置的 IP 地址不可用。再手动启动一个临时容器检查网卡docker run --rm --network app_net redis-test ip addr发现临时容器拿到的 IP 是 172.20.0.33而 redis-master 配置里写死 bind 172.20.0.21。此时我意识到问题不在 Redis而在 IP 分配之前给 redis-master 指定了静态 IP 172.20.0.21但 Docker 网络的 IPAM 不知道这个地址已经被手动分配之后某个其他容器被自动分配到了 172.20.0.21导致 redis-master 启动时 bind 不上去。排查确认docker network inspect app_net | grep -A10 Containers果然看到两个容器都在 172.20.0.21 网段信息下一个手动指定一个自动分配。解决方式把所有使用静态 IP 的容器显式声明同时给 compose 文件里的服务统一添加固定 IP 声明确保 IPAM 池里没有和手写 IP 重叠的自动分配范围。简单说就是要么全手动管理 IP要么全自动分配不要混用。为了避免这类问题后续我把 compose 文件里每个服务的 IP 都显式列出并且子网规划时预留出静态段networks: app_net: ipam: config: - subnet: 172.20.0.0/24 gateway: 172.20.0.1 ip_range: 172.20.0.100/28ip_range限制 Docker 自动分配的范围只在 172.20.0.100-172.20.0.111 之间手动指定的 IP 都放在 172.20.0.2-172.20.0.99 段。这样一个很小的改动就从根上排除了 IP 冲突的可能。这起事故的复盘价值在于容器网络、静态 IP、配置绑定这三件事是互相耦合的任何一层出问题表面症状都可能一样容器起不来、服务连不上。排查时不要只盯着应用层配置先docker logs看系统层原因再查docker network inspect最后回去翻 compose 配置这个路径基本能覆盖八成同类问题。 ## 9. 多架构与离线交付企业内网部署 Rocky 镜像的最后一公里9.1 在离线环境构建和迁移 Rocky 镜像企业内网环境往往不能直接访问 Docker Hub特别是涉密或金融场景镜像导入导出是日常操作。docker save和docker load是基础技能但真正的坑在于“跨架构”和“跨 Docker 版本”。# 在能联网的构建机上打镜像 docker build -t registry.internal/rocky-base:9 . # 导出为 tar 包 docker save registry.internal/rocky-base:9 -o rocky-base-9.tar # 拷贝到离线服务器 scp rocky-base-9.tar useroffline-server:/opt/images/ # 离线服务器导入 docker load -i rocky-base-9.tar这一步看起来很简单但实际操作里有几个隐性坑。第一docker save会导出镜像的所有层如果中间层包含敏感信息比如 Dockerfile ARG 里传递的密码导出包也可能包含这些信息转移时需要用加密传输或转移前清理构建 ARG。第二离线环境里docker pull不行但镜像内部的dnf源如果在构建时没替换为内网源离线容器里执行dnf install依然会失败。所以离线交付的基础镜像一定要在构建阶段就把源替换成内网镜像源或者把所有需要的 RPM 包提前打成离线包。第三如果离线服务器 Docker 版本比较老docker load可能遇到 image 格式不兼容的问题。老版本对mediaType和manifest list的支持不完整多架构镜像会加载失败。遇到这种情况要么升级 Docker要么在构建时输出单架构镜像--platform linux/amd64。9.2 用 Skopeo 和 Registry 做镜像中转如果你的构建机和离线服务器之间网络隔离而且镜像数量大我推荐用 Skopeo 而不是 docker save/load。Skopeo 可以直接从 Registry 拉镜像再推到另一个 Registry中间不经过本地 Docker daemon还能复制多架构镜像速度也快。# 安装 skopeo在能联网的机器上 dnf install -y skopeo # 从 Docker Hub 复制到内网 Harbor skopeo copy docker://docker.io/rockylinux:9 \ docker://registry.internal.local/rockylinux:9 \ --dest-creds username:password # 只复制单架构 skopeo copy --override-arch amd64 \ docker://docker.io/rockylinux:9 \ docker://registry.internal.local/rockylinux:9在企业内部采用 Harbor 作为镜像仓库然后让所有生产机器从 Harbor 拉取镜像这是比较稳妥的交付链路。Harbor 自带镜像复制功能可以把上游镜像自动同步到内网配合漏洞扫描和镜像签名整个供应链就闭环了。Harbor 的安装部署本身也可以基于我之前做过的 Rocky Linux Docker Compose 方式安全可控。好回到核心问题我们需要在离线服务器上怎么处理。很多时候离线环境还没有 DNS镜像仓库地址只能写死 IP。这种情况建议把 Harbor 的地址固定为一个稳定的内网 IP并在所有机器上配置/etc/hosts或者使用内网 DNS 保证解析一致性。否则不同机器上写的仓库地址千奇百怪后面排查镜像拉取问题会非常痛苦。9.3 离线 yum 源的搭建思路除了镜像本身企业内网还要解决“容器里跑 dnf install 装包”的问题。如果所有操作都在构建阶段完成离线节点只需要一个预装好所有依赖的镜像那离线 yum 源可以不用部署。但现实是业务迭代快经常有临时装包需求所以一个内网 yum 源仓库相当有必要。最简单的方案是dnf reposync把 Rocky 官方源同步到内网 Nginx 或 HTTP 服务器dnf install -y createrepo_c dnf-utils # 同步 BaseOS 和 AppStream 仓库 reposync --repobaseos --repoidbaseos --download-path/data/yum/rocky/9/BaseOS reposync --repoappstream --repoidappstream --download-path/data/yum/rocky/9/AppStream # 生成仓库元数据 createrepo_c /data/yum/rocky/9/BaseOS createrepo_c /data/yum/rocky/9/AppStream然后在 Rocky 容器里把/etc/yum.repos.d/指向内网地址即可。这个方案的好处是容器和宿主机共用一套源构建机、测试机、生产机全部指向同一个内网源依赖版本完全一致。如果只是想离线安装某个特定 RPM 包也可以用yumdownloader --resolve把包和依赖全部下载成一个目录拷贝到内网后dnf install ./rpm/*.rpm本地安装。这种方式在容器内临时补包时最省事。10. 从基础镜像到完整平台的落地经验和路线图10.1 落地遇到过的几个共性问题整个“Docker 部署 Rocky Linux 构建企业级基础镜像平台”的过程最终不是输在一两个技术难点上而是输在一些容易被忽略的细节上。我整理了一下踩坑最狠的几个点给后来者当参考。第一容器内用 dnf 装包时不要用--assumeyes以外的选项但也不要过度依赖它。有些包安装时会弹交互确认比如 TZData-y并不能完全跳过。我的建议是构建阶段尽量从已知源安装避免交互选项否则 CI 时间会卡在奇怪的提示上。第二注意 Dockerfile 的层缓存。dnf install这行如果写在COPY . .之后代码变更会导致整个 dnf 缓存失效每次构建都重新下载所有包。合理的顺序是先 COPY 仓库配置和缓存文件再 dnf install最后 COPY 业务代码。这种调整能显著提升团队 CI 效率。第三容器里的 cron 或 systemd timer 不要直接用因为没有 systemd。计划任务要么宿主机 crontab 调docker exec要么容器内单独跑一个 cron 守护进程或者直接引入 K8s CronJob。Rocky 容器里默认不安装 cronie装起来也跑得别扭不如从架构上绕开。第四日志处理。容器里应用直接写/var/log/app.log这个文件会在容器重建时丢失而且docker logs看不到。规范做法是让应用直接把日志打到 stdout/stderr由 Docker 日志驱动统一收集或者至少把日志目录挂到宿主机卷上再加 Filebeat 之类采集。很多老业务是直接写文件的迁移时要注意这层适配。10.2 一份可以直接抄作业的基础镜像 Dockerfile把前面所有要点汇总成一份可直接使用的 Dockerfile标好注释适合作为企业标准化基础镜像的起点。# syntaxdocker/dockerfile:1 FROM rockylinux:9 # 1. 替换为内网/国内源 RUN sed -e s|^mirrorlist|#mirrorlist|g \ -e s|^#baseurlhttp://dl.rockylinux.org/$contentdir|baseurlhttps://mirrors.aliyun.com/rockylinux|g \ -i.bak \ /etc/yum.repos.d/rocky.repo /etc/yum.repos.d/rocky-extras.repo /etc/yum.repos.d/rocky-devel.repo \ dnf clean all \ dnf makecache # 2. 基础补丁和常用工具 RUN dnf update -y \ dnf install -y --setopttsflagsnodocs \ tzdata glibc-langpack-en glibc-langpack-zh \ vim-minimal curl wget tar gzip findutils \ ca-certificates \ dnf clean all \ rm -rf /var/cache/dnf /tmp/* # 3. 时区和语言 ENV TZAsia/Shanghai \ LANGen_US.UTF-8 \ LANGUAGEen_US.UTF-8 \ LC_ALLen_US.UTF-8 RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ echo $TZ /etc/timezone # 4. 创建应用用户 RUN groupadd --gid 10001 app \ useradd --uid 10001 --gid app --shell /sbin/nologin --create-home app # 5. 基础安全收尾 RUN dnf autoremove -y || true \ dnf clean all \ rm -rf /var/log/* /root/.bash_history CMD [/bin/bash]需要说明的是这份 Dockerfile 是“基础镜像”而非“业务镜像”所以 CMD 只是占位。业务镜像会在FROM registry.internal/rocky-base:9之后叠加 JRE、Python 运行时等应用依赖。基础镜像里不预装 JDK、Python 这类重依赖否则所有下游业务镜像都会背上不必要的体积和攻击面。10.3 平台化之后的运维重点镜像做出来只是第一步平台化要考虑的是这个镜像如何被团队长期稳定地使用。我的建议是建立一个“基础镜像发布节奏”每个季度重建一次基础镜像同步 Rocky 安全补丁同步后跑全量冒烟测试覆盖团队内的典型业务Java 服务、Python 服务、文档转换、数据缓存等测试通过后推送到内网 Harbor 的 new 版本 tag并通知下游团队在一个周期内完成验证切换。不要频繁重建也不要一年不重建季度节奏比较均衡。监控和审计层面容器镜像的 CVE 扫描应该纳入 CI每次构建后自动扫描高危漏洞阻断上线。Harbor 自带 Trivy 扫描器用起来不复杂。再配合镜像签名Cosign和策略即代码OPA等于是给基础镜像上了多层保险。小团队从 CVE 扫描开始就够了。最后人员层面。基础镜像平台这类工作最难的不是技术选型而是说服团队“从今天开始统一基础镜像不再各自拉不同发行版”。我这边推动统一的经验是先挑一个对镜像体积最敏感的业务场景做试点比如文档转换服务从 Ubuntu 镜像切到 Rocky 基础镜像后体积减小 30%启动速度也略有提升实测数据摆在面前团队自动就愿意迁移了。技术方案再成熟没有落地场景和收益数据后面推广都是空的。