
先从结论说起Rocky Linux 9 的离线 Docker 部署难的不是那一两个安装命令而是整条链路上每一个“看不见就翻车”的环节——依赖包到底怎么捞、yum 源怎么搭、服务起不来怎么定位、镜像怎么在没有外网的机器上跑起来。我前后在 VMware 虚拟机、物理机内网环境各踩过一轮完整的坑这篇就把从零到能跑 MySQL 容器的全过程拆开讲。1. 为什么离线部署比想象中麻烦不只是“装个软件”这么简单很多人第一次接到内网部署任务时脑子里想的还是线上环境那套玩法dnf install -y docker-ce一把梭装完systemctl start dockerpull 镜像直接跑。等到了离线的机器上才发现每一步都被卡住软件源连不上、依赖缺失报错、服务起不来、镜像根本拉不下来。我在实际项目里的体会是离线部署本质上是一条由四个环节串起来的链路系统基础环境、Docker 引擎安装、容器镜像分发、业务容器启动。任何一环缺了准备后面全被卡死。而且麻烦的地方在于这四个环节的错误往往不在当场暴露而是隔一层才炸出来——比如你千辛万苦把 Docker 装好了一启动服务发现存储驱动不支持或者容器起来了但 MySQL 数据目录权限不对直接退出这时候再回头补就非常被动。所以做离线部署不能把它当成“装个软件”而要当成一次完整的供应链搬运。你要搬的不仅是 Docker 那几百 MB 的二进制还包括系统层面的依赖 rpm 包数量可能远超你想象所有业务需要的容器镜像包括镜像背后的基础镜像层配置文件和启动脚本可能用到的 docker-compose 二进制或插件包另一个新手容易忽略的点是版本匹配。Rocky Linux 9 用的是 dnf 体系和 8 的 yum 命令虽然兼容但底层仓库结构不同。从 9 开始PowerTools 改名成了 CRB 仓库很多依赖要从这里拉。这些细节在线的机器上dnf会自动处理离线的机器上全得靠你手动搞清楚。我在第 3 到第 9 部分会把每个环节的做法和踩坑点完整展开直接把能用的命令和步骤给你。2. 基础准备Rocky Linux 9 minimal 安装后的初始化设置离线部署的第一步其实在装系统时就开始了。我的建议是无论如何都要装 minimal 版本不要装 GUI——服务器上图形界面纯粹浪费 CPU 和内存而且会引入一堆无关依赖后续做离线依赖清单时干扰非常大。装完系统的第一件事不是装 Docker而是把网络环境和基础工具先收拾利索。内网环境最常见的问题有两个一是 DHCP 分配的 IP 在重启后变了导致你通过 SSH 远程操作的会话直接断掉二是网卡没自动激活开机后根本没有 IP。这两个问题在物理机上尤其常见VMware 里通常默认 NAT 没问题但到了客户机房交换机端口配置乱七八糟什么情况都能遇到。2.1 先配静态 IP内网环境的生存底线我习惯第一步就配静态 IP。在 Rocky Linux 9 上建议直接用nmcli不要再去改/etc/sysconfig/network-scripts/ifcfg-*那套老古董了——虽然文件还在但 NetworkManager 的优先级更高你改了文件它不一定认。先用ip link看一下网卡名称常见的叫ens160或ens32nmcli con show假设上面显示连接名也叫ens160那么这样配置静态地址nmcli con mod ens160 ipv4.addresses 192.168.10.50/24 nmcli con mod ens160 ipv4.gateway 192.168.10.1 nmcli con mod ens160 ipv4.dns 192.168.10.2 nmcli con mod ens160 ipv4.method manual nmcli con up ens160这三条mod命令分别设置地址、网关、DNSmethod manual告诉 NetworkManager 别再用 DHCP。改完后con up激活。这里有个容易翻车的细节如果你当时正在用 SSH 连接这台机器执行con up的瞬间网卡会重新拉起连接会短暂中断然后通过新 IP 重新连。如果新 IP 配错了可能就再也连不上了只能去控制台改。所以我建议第一步只配静态 IP 并确保 SSH 恢复再继续后面的操作。另外顺手把主机名设置一下内网环境尤其重要否则容器日志里机器名全是localhost.localdomain排查问题很难区分hostnamectl set-hostname docker-node12.2 防火墙和 SELinux先别急着关但要知道怎么关Rocky Linux 9 默认 firewall 是开着的SELinux 是 enforcing 模式。这两个东西在 Docker 部署时都可能成为阻碍但我不建议一上来就无脑关闭而是分情况处理。防火墙在纯内网环境而且没有专职安全要求时可以先把 Docker 需要用的端口放通比如 2375不建议开、3306、5700 这类业务端口firewall-cmd --permanent --add-port3306/tcp firewall-cmd --reload如果你的离线环境是一台专用的容器服务器后面要跑的容器端口非常多我个人建议这种场景直接systemctl disable --now firewalld。等真上了生产内网再做端口级管控单独容器网络环境里开防火墙的收益和运维成本不成比例。SELinux 是个更微妙的坑。Docker 在 RHEL 系上对 SELinux 是支持的但前提是你得安装container-selinux这个包而且这个包恰恰是离线安装时最容易缺的依赖之一。我后来的做法是在离线环境里如果没把握把 SELinux 相关的容器策略包一次搞齐就直接在/etc/selinux/config里把SELINUXenforcing改成SELINUXpermissive重启后生效。permissive 模式只记录警告不拦截比 disabled 稳妥而且 CVE 审计时也好交代。2.3 离线依赖从哪来在有网环境准备一个“百宝箱”这步虽然是在有网的机器上做但它是整个离线部署能否成功的关键。我一开始犯过的错误是只拷了几个核心 rpm 过去结果装到一半报缺这个缺那个现场又没网只能干瞪眼再去远程折腾。正确做法是准备一台和离线机器同大版本、同架构的能上网的 Rocky Linux 9 机器架构不对非常致命x86_64 的包不能用在 ARM 上在这台机器上把后面要用到的所有 rpm 包一次性下载好。具体方法和仓库搭建我在下一部分细说。这里先提一个思维层面的原则离线环境的所有操作都要有“先备份、后搬运、再校验”的意识。镜像、rpm 包、配置文件的完整性都要盯紧搬运过程经常出现文件损坏的问题建议所有打好的包顺手生成一个 md5sum 校验清单过去之后先校验一遍。3. 依赖打包的完整实操用 dnf 一次性把 RPM 捞全离线安装 Docker 最核心的一个准备动作就是把 rpm 依赖捞全。这步做不好后面就是无休止的“缺一个装一个”循环。在一个没有外网的机器上每缺一个包就意味着你要脱离现场重新找包、拷贝、传进去耗时极长而且容易乱。3.1 先配置 Docker 官方仓库下载的大前提在有网的 Rocky Linux 9 机器上先装 Docker 官方仓库。注意网上很多教程还在用老命令yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repoRocky Linux 9 兼容 CentOS 9 的源路径这个没问题。但yum-config-manager在 minimal 环境默认没有需要先装yum-utilsdnf install -y yum-utils装完再加仓库dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repoRHEL 9 系列的 docker-ce 仓库地址用的是 centos 目录这不是写错了而是因为 RHEL 系共享同一个发行版路径官方 repo 文件里会自动替换变量。3.2 用 dnf download 而不是网上的脚本我最早是在网上找了一段 rpm 清单一个个下后来发现这样根本下不全。正确做法是用dnf download把依赖递归解析出来。先看一下 Docker 相关包的完整列表dnf list --showduplicates | grep docker-ce一般主包有这几个docker-ce、docker-ce-cli、containerd.io、docker-buildx-plugin、docker-compose-plugin。前三个是核心后两个建议一起带上否则后面用docker compose时会很尴尬。下载全部依赖到指定目录推荐用这个命令dnf download --resolve --alldeps docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin--resolve表示解析并下载依赖--alldeps是把所有依赖包括弱依赖都带上。这两个参数缺一不可。执行完后当前目录会生成一堆 rpm数量通常在 30 到 60 个之间取决于你基础环境里已经装了哪些包。这里有一个很关键的坑dnf download只会下载 rpm 本身不会检查这台机器上已有的包。也就是说你在这台有网机器上下载的依赖集合是基于它已有的软件环境生成的。如果离线目标机器的软件环境更干净、缺失的包更多那你下载的依赖可能还是不全。保险起见可以在离线机器上先执行dnf list installed把已装包列表导出来拿到有网机器上做比对。如果嫌麻烦用 minimal 系统作为两台机器的基准绝大部分问题都能避免。3.3 用 createrepo 做成本地 yum 源rpm 包下完后如果直接rpm -ivh一个个装遇到复杂依赖顺序会让你怀疑人生。正确做法是把这些 rpm 做成一个本地 yum 源然后用dnf去安装让 dnf 处理依赖顺序。在有网机器上执行dnf install -y createrepo_c mkdir -p /data/docker-rpms把 3.2 下载的 rpm 全部放到/data/docker-rpms目录然后生成仓库元数据createrepo /data/docker-rpms执行完后目录里会多一个repodata文件夹这就是 dnf 识别仓库的依据。把整个/data/docker-rpms目录打成压缩包cd /data tar czf docker-rpms.tar.gz docker-rpms这个 tar 包通常也就是几十 MB 到一两百 MB用 U 盘、内网共享、刻盘、带外管理口都行拷到离线机器上。到了离线机器上解压后写一个 repo 文件tar xzf docker-rpms.tar.gz -C /opt cat /etc/yum.repos.d/docker-local.repo EOF [docker-local] nameDocker Local Repo baseurlfile:///opt/docker-rpms enabled1 gpgcheck0 EOFgpgcheck0是因为本地源的 rpm 没有做 GPG 签名不关掉 dnf 会一直报错。把官方 docker-ce.repo 里指向远程源的部分禁用掉或者干脆删掉/etc/yum.repos.d/docker-ce.repo避免 dnf 去尝试连接外网超时等待。3.4 依赖清单的排序技巧如果你非要手动 rpm 安装记住一个顺序原则先装依赖包再装 containerd.io再装 docker-ce-cli最后装 docker-ce。因为 docker-ce 主包的 postinstall 脚本会要求 dockerd 和 containerd 已经就位。不过强烈不建议手动排序用dnf localinstall, 也就是加本地源的dnf install来装下面给命令。离线机器上验证本地源是否生效dnf repolist能看到docker-local字样就说明源没问题。4. 离线安装 Docker 引擎rpm 链路与常见报错准备工作做完安装环节反而简单了。但要提醒一句安装不等于能用装完后的服务启动才是重头戏。4.1 从本地源安装 Docker离线机器上执行dnf clean all dnf makecache dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugindnf 会自动从本地源解析依赖顺序按拓扑排序逐个安装。这一步如果本地源配置正确一般是几分钟内搞定不会报错。安装完成后先不要急着启动确认一下安装结果rpm -qa | grep docker docker --versiondocker --version能看到版本号就说明 CLI 装好了。这一版的版本号对后续镜像兼容性很重要我用的版本是 24.0.x 和 25.0.x 都没问题建议别装太老的版本cgroup v2 和新的 containerd 兼容性更好。4.2 配置 daemon.json提前规避存储驱动和镜像加速问题在启动服务之前先看一眼/etc/docker/目录是否存在不存在就创建mkdir -p /etc/docker然后写一个最小但关键的daemon.json{ data-root: /var/lib/docker, storage-driver: overlay2, exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }这里每个配置都有实际意义storage-driver: overlay2是 RHEL 9 上推荐的存储驱动性能最好。但前提是文件系统支持后面启动章节专门讲这个问题。native.cgroupdriversystemd是为了和宿主机 systemd 保持一致。Docker 和 Kubernetes 混用时强烈建议用 systemd cgroup 驱动否则节点不稳定。log-opts限制单容器日志大小内网环境磁盘通常不富裕一个不写日志的容器可以把磁盘撑爆这行配置能救命。4.3 启动 docker 服务的第一道坎首次启动systemctl enable --now docker正常输出只有一条返回没有任何提示。验证systemctl status docker docker infodocker info能看到Server Version说明 daemon 已经起来了。但如果你看到Job for docker.service failed because the control process exited with error code, 那说明启动失败了。这个错误本身只是“服务进程退出”的通用提示原因千奇百怪下一章专门排查。4.4 修改 containerd 配置内网环境另一个隐藏坑containerd 是 Docker 底层的容器运行时它有自己的配置文件/etc/containerd/config.toml。默认安装后这个文件可能不存在Docker 会自动用内置默认值。但在 RHEL 9 上如果后面用docker compose或 Kubernetes 时遇到奇怪的网络问题可能就和 containerd 的SystemdCgroup配置有关。建议显式生成配置并确认containerd config default /etc/containerd/config.toml然后用编辑器把文件里的SystemdCgroup false改成SystemdCgroup true新版版本号可能叫systemd_cgroup以实际为准改完后重启 containerdsystemctl restart containerd systemctl restart docker这一步不是必须的但如果你在容器里跑 Java 服务遇到 CPU 和内存限制不生效的问题回来查这行配置。5. docker 服务启动失败排查从 systemd 日志一路查到底服务启动失败是离线部署里最让人头大的问题因为它有无数种可能。我把在实际环境中碰到过的高频原因按出现概率排个序排查时按这个顺序走效率最高。5.1 先看日志再动手journalctl 是第一个排查工具启动失败后第一件事永远是把 systemd 日志拉出来看journalctl -u docker --no-pager | tail -50不要只看systemctl status docker那几行很多时候真正的错误藏在日志后半段。我最常见到的几个关键报错报错关键字实际原因解决方向overlayfs not supported内核模块 overlay 未加载或文件系统不支持加载 overlay 模块或换 vfs 驱动Failed to listen on docker.sock - bind: address already in use端口或 socket 被占用查看是谁占用或者改监听配置Error starting daemon: error initializing graphdriver: operation not permittedSELinux 或权限问题检查 SELinux 状态、目录权限cgroups: cannot find cgroup mount destinationcgroup v1/v2 切换问题检查内核启动参数5.2 存储驱动报错离线环境翻车率第一报错里出现mount或overlay关键字时基本就是存储驱动炸了。RHEL 9 默认文件系统是 XFS理论上支持 overlay2但有两个前提内核里overlay模块要加载。执行lsmod | grep overlay如果没输出就执行modprobe overlay。为了让重启后依然生效写进/etc/modules-load.d/overlay.conf内容就一行overlay。XFS 的 inode 数不能为 0。创建 XFS 分区时如果没特别指定默认 inode 数是够的。但如果你的数据盘是用某些工具格式化的xfs_info /var/lib/docker看到nlinks0会导致 overlay2 无法工作。如果两个条件都满足还是报错可以用 vfs 驱动应急{ storage-driver: vfs }vfs 性能差一些但兼容性最好。内网环境如果只是跑一两个业务容器影响不大。但如果跑几十个容器磁盘占用会翻几倍慎用。5.3 cgroup v2 与兼容性RHEL 9 上最容易模糊的角落Rocky Linux 9 默认启用 cgroup v2这是内核启动参数systemd.unified_cgroup_hierarchy1控制的。Docker 20.10 及以上版本对 cgroup v2 支持已经很好但如果发现容器里的 CPU、内存限制完全不生效或者启动多个容器时资源互相干扰就要查 cgroup 版本是否和 Docker 预期的一致。查看当前 cgroup 版本stat -fc %T /sys/fs/cgroup/看到cgroup2fs说明是 v2看到tmpfs就是 v1。Docker 24 以上对 v2 已经非常成熟不用太担心。真正让我踩过坑的是老版本 Docker 配 cgroup v2容器能启动但docker stats看到的数值全不对排查半天才想到是版本问题。所以如果你要在 RHEL 9 上离线部署Docker 版本务必高于 23.0低于这个版本的很多网络和 cgroup 细节会让人抓狂。5.4 SELinux 导致的启动失败从 “permission denied” 到放行启动失败日志里出现avc: denied或Permission denied时先看 SELinuxgetenforce如果输出是Enforcing可以先临时切到 permissive 看一下是不是 SELinux 的锅setenforce 0 systemctl restart docker如果服务能起来了基本确认是 SELinux 策略问题。有两个解决路径一是正经地处理装container-selinux包并确保容器文件目录的 SELinux context 正确chcon -Rt container_file_t /data/docker二是干脆改SELINUXpermissive持久化。我个人在内网离线场景强烈推荐第二种省心而且不易引入新问题——毕竟内网环境安全隐患更多来自不可控的镜像来源而不是 SELinux。5.5 验证服务真正可用启动后不要只看守护进程状态跑一个真正的容器验证全链路。离线环境下没有现成镜像先用 hello-world 肯定不行那玩意拉不回来。用本地已有的基础镜像跑一下docker run --rm -it --entrypoint sh alpine:latest -c echo docker-ok如果没镜像可以从后面第 6 部分的镜像打包流程导入一个最小镜像后再验证。6. 容器镜像离线搬运save / load / registry 的取舍Docker 引擎装好、服务正常这只是前半场。后半场是把业务镜像弄进内网。这块有两条技术路线适用范围完全不同。6.1 小规模场景docker save 和 docker load 的完整用法如果内网只有一两台机器、镜像数量也不多docker savedocker load是最直接的方式。在有网的镜像仓库机器上docker pull mysql:8.0 docker save -o /data/images/mysql-8.0.tar mysql:8.0这里有三个细节save保存的是镜像不是容器。如果你在容器里装了不少东西想让容器也迁移过去必须先docker commit把容器固化成镜像再 save。多个镜像可以打成一个 tar节省传输次数docker save -o /data/images/all.tar mysql:8.0 redis:7.2 alpine:3.20生成的 tar 可能非常大。MySQL 8.0 官方精简镜像已经超过 500 MB一个 Redis 也有 150 MB。建议 save 后用gzip压缩gzip /data/images/mysql-8.0.tar到了离线机器上docker load -i /data/images/mysql-8.0.tar.gzload 过程会显示加载了哪些镜像层。这里有个坑load 之前不要手工解压 gzdocker load 本身支持直接读取 gzip 压缩的 tar但你解压后再 load 也没问题只是多一步。load 完成后跑docker images确认镜像列表是对的。最怕的是 tar 在传输过程中损坏load 时会报invalid tar header之类的错误。所以我每次都会先对 tar.gz 做一次 md5 校验两边比对一致再 load。6.2 内网团队协作自建 registry 镜像仓库如果是一个团队共用一套内网环境多台机器都要拉同样的镜像那docker load的方案效率太低。这时应该用 registry 镜像做内网私有仓库。在有网的机器上docker run -d --name registry --restartalways \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ registry:2然后给需要内网分发的镜像打 tag 并 pushdocker tag mysql:8.0 192.168.10.50:5000/mysql:8.0 docker push 192.168.10.50:5000/mysql:8.0内网机器上只要在/etc/docker/daemon.json里加一行{ insecure-registries: [192.168.10.50:5000] }然后重启 docker就能直接docker pull 192.168.10.50:5000/mysql:8.0注意这个方案里 registry 容器本身也是镜像你同样需要用 save/load 的方式把 registry:2 镜像先导入到这台机器上。所以它不是代替 save/load而是建立在 save/load 之上的进阶方案。如果你的环境连 HTTP 私服都接受不了要求 HTTPS那还得自己生成证书配 nginx 反向代理这个复杂度就上了一个量级不是内网小团队场景一般用不到。6.3 registry 数据目录的整体搬迁还有一种更暴力的玩法把 registry 容器的数据目录/var/lib/registry整个打包带走。只要版本一致搬过去后跑相同的 registry 容器并挂载这个数据目录里面的镜像就全都“复活”了。这个方案适合一次性把几十 GB 的镜像库搬到另一个隔离网络里比一台台 save 快得多。但要注意docker registry 数据目录的格式是固定的不同 registry 大版本的存储结构可能不兼容。搬过去之前先确认两边 registry 容器镜像版本差不多否则可能读到一半报错。7. 最典型的离线业务MySQL 8.0 容器化部署实操Docker 装好了镜像也导入了接下来看一个行业内最常用到的业务——MySQL 8.0。这个案例把容器启动、数据持久化、权限、日志排查全串起来你把它跑通了其他有状态服务Redis、PostgreSQL、Nginx都是一样的套路。7.1 数据目录规划和 MySQL 镜像的导入部署之前先把存储规划好。MySQL 的数据必须挂在宿主机上否则容器一删数据全没。我的习惯是在数据盘上建目录mkdir -p /data/mysql/{data,conf,logs}然后在有网机器上拉镜像并导出这里用我个人推荐的 mysql:8.0 官方镜像docker pull mysql:8.0 docker save -o /data/images/mysql-8.0.tar mysql:8.0拷到离线机器后导入docker load -i /data/images/mysql-8.0.tar7.2 启动 MySQL 容器参数逐个解释启动命令如下docker run -d \ --name mysql8 \ --restartalways \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDYourStrongPassw0rd \ -v /data/mysql/data:/var/lib/mysql \ -v /data/mysql/conf:/etc/mysql/conf.d \ -v /data/mysql/logs:/var/log/mysql \ mysql:8.0 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci逐个说下每个参数的意图--restartalways容器异常退出后 dockerd 自动拉起离线环境没人全天守着这个参数极其重要。-e MYSQL_ROOT_PASSWORD首次初始化数据目录时使用的 root 密码。注意只有第一次启动时这个环境变量才生效如果你删掉数据目录重新初始化它才会再次生效。如果数据目录已存在改密码要另想办法。-v /data/mysql/data:/var/lib/mysql数据持久化的核心里面是 MySQL 的ibdata1、mysql.ibd这类文件。-v /data/mysql/conf:/etc/mysql/conf.d自建配置文件的挂载位置。MySQL 会读取这个目录下的.cnf文件你不需要进入容器改配置。--character-set-serverutf8mb4字符集必须显式指定否则默认 latin1中文存储会乱。7.3 容器启动后立刻退出的排查思路这是热词里提到的高频问题“安装 mysql 启动服务报错”。容器docker ps -a看到状态是Exited (1)先不要慌用日志说话docker logs mysql8 --tail 50实际运维中我遇到过几类典型日志日志内容原因处理[ERROR] [MY-010244] [Server] Failed to initialize DD Storage Engine数据目录权限不对chown -R 3306:3306 /data/mysql/data[ERROR] [MY-011011] [Server] Failed to initialize DD Storage Engine配置文件和别的参数冲突删掉 conf 目录里冲突的配置再重启initialize specified but the data directory has files in it数据目录非空但还没初始化过清空/data/mysql/data后重启mbind: Operation not permittedNUMA 内存绑定警告无害可以忽略其中数据目录权限问题翻车率最高。MySQL 官方镜像内是以mysql用户运行UID/GID 是 3306。宿主机的/data/mysql/data如果默认是 root 所有容器内写不进去启动就失败。处理方式chown -R 3306:3306 /data/mysql/data chown -R 3306:3306 /data/mysql/logs这里不得不吐槽一下新手经常在日志里看到Permission denied但不知道是谁的权限问题实际上就往数据目录和日志目录上套 chown 准没错。7.4 验证 MySQL 服务服务起来后验证一下docker ps | grep mysql8状态是Up就说明容器活着。然后进容器docker exec -it mysql8 mysql -uroot -p输入密码能进 MySQL 命令行就 OK。这里建议顺手建一个业务专用账号别让业务代码直接用 rootCREATE USER app% IDENTIFIED BY AppPassw0rd; GRANT ALL PRIVILEGES ON *.* TO app%; FLUSH PRIVILEGES;7.5 有状态容器备份的附加建议对有状态容器我后来养成了一个习惯每天凌晨在宿主机上用docker exec做一次逻辑备份0 2 * * * docker exec mysql8 sh -c exec mysqldump -uroot -p$MYSQL_ROOT_PASSWORD --all-databases /data/backup/mysql-$(date \%F).sql容器能持续跑是基本要求数据能随时捞回来才是保障。这部分和离线环境无关但放这儿一起提出来省得你后面再踩一遍“容器活了但数据没了”的坑。8. 容器编排在离线环境下的玩法docker-compose 与青龙面板组合实际的内网部署很少只跑一个 MySQL往往是 MySQL、Redis、Nginx、业务接口、定时任务一堆服务一起上。这时候就用 docker-compose 把它们编排起来。离线环境下 compose 的部署有两个关键点一个是怎么把 compose 插件弄进去另一个是镜像整体打包策略。8.1 把 docker-compose 插件装到离线机器上如果你在安装 Docker 时按第 4 部分的命令装了docker-compose-plugin那docker compose就已经可用了直接验证docker compose version如果当时没装补救也不难。在有网机器上下载dnf download docker-compose-plugin把得到的 rpm 拷到离线机器上dnf install -y ./docker-compose-plugin-*.rpm注意如果离线机器上还没有 docker-compose-plugin 依赖的基础包可能还要一并处理依赖。这也是我为什么一直强调安装 Docker 时就把所有插件装齐后面补装很麻烦。如果你需要用docker-compose带横线的旧命令不是docker compose子命令那就得单独下载二进制文件放到/usr/local/bin/docker-compose并加上执行权限。新版 Docker 建议直接用docker compose子命令少维护一个二进制。8.2 青龙面板离线部署依赖管理的衍生问题青龙面板在很多自动化场景里都会用到属于典型的需要长期运行、依赖复杂的容器应用。离线部署的步骤和其他镜像没有任何区别有网机器上docker pull whyour/qinglong:latest docker save -o /data/images/qinglong.tar whyour/qinglong:latest离线机器上docker load -i /data/images/qinglong.tar启动docker run -d \ --name qinglong \ --restartalways \ -p 5700:5700 \ -v /data/qinglong:/ql/data \ whyour/qinglong:latest启动成功后访问http://内网IP:5700就能看到配置页面。这里要特别提醒一个热词里提到的“青龙依赖管理”问题青龙面板界面里安装依赖时实际上是容器内执行npm install、pip install、apk add等命令依赖源默认指向公共仓库。离线环境下这些命令会超时失败面板里会一直转圈或报错。解决思路有两个都建议尝试在有网机器上把常见的依赖列表通过青龙的 shell 命令在容器里预先装好然后docker commit把安装好依赖的容器固化成新镜像再 save 走。在离线机器上给容器配置内网 npm/pip/apk 镜像源比如-e NPM_REGISTRYhttp://内网nexus/repository/npm/这类环境变量但前提是你内网有一个软件源服务。方案一增量小、见效快适合一次性打包方案二维护性更好但需要额外的内网基础设施。个人在内网起步阶段推荐方案一后面真要大规模铺开再考虑方案二。8.3 多容器编排compose 文件的离线要点写一个简单的docker-compose.yml把 MySQL 和 Redis 编排起来services: mysql: image: mysql:8.0 container_name: mysql8 restart: always environment: MYSQL_ROOT_PASSWORD: YourStrongPassw0rd volumes: - /data/mysql/data:/var/lib/mysql - /data/mysql/conf:/etc/mysql/conf.d ports: - 3306:3306 redis: image: redis:7.2 container_name: redis7 restart: always volumes: - /data/redis/data:/data command: [redis-server, --appendonly, yes] ports: - 6379:6379离线环境下用 compose 启动docker compose up -d这里有个容易忽略的坑compose 文件里引用的所有镜像都必须在离线机器上docker load过。如果某个镜像没导入compose 会尝试去公共仓库拉取然后超时失败。建议在离线机器上先docker images确认所有镜像都在再执行 up。为了避免一个个镜像手动 load 太麻烦可以写个小脚本一次性加载所有 tar.gz 包#!/bin/bash for f in /data/images/*.tar.gz; do echo Loading $f ... docker load -i $f done这是我每次到新环境第一个执行的脚本简单但救命。9. 一次完整的内网部署流程回顾从零到业务可用最后把这篇文章的完整执行流程压缩成一张可复制的操作清单。这里没有新的知识点但是把这些步骤按顺序罗列出来能帮你在实际部署时不漏环节、不跳步。在有网机器上准备离线介质配置 docker-ce 官方仓库dnf download --resolve --alldeps下载全部 rpmcreaterepo生成仓库元数据打 tar 包生成 md5 校验清单在离线机器上初始化系统配置静态 IP设置主机名按需关闭防火墙调整 SELinux 为 permissive解压 rpm 包创建/etc/yum.repos.d/docker-local.repo离线安装 Dockerdnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin写/etc/docker/daemon.json加载 overlay 模块systemctl enable --now docker用本地基础镜像验证docker run可执行导入业务镜像从有网机器docker save并传输到离线机器md5 校验后docker loaddocker images确认镜像列表启动业务容器规划宿主机数据目录docker run或docker compose up -ddocker logsdocker exec验证服务真实可用这套流程我实际执行下来效率最高的一次是 40 分钟全部搞定包括系统初始化和 MySQL、Redis 两个容器上线。最关键的是第 1 步和第 4 步的“搬运完整性”只要依赖包和镜像都带全了后面的安装步骤几乎不会出意外。根据我个人的经验最后再补一个建议离线部署完成之后一定不要立刻收工找时间把整套环境冷重启一次确认 Docker 是随系统自启的、容器是随 Docker 自启的。我遇到过不止一次systemctl enable docker虽然执行了但重启后因为某个单元依赖没起来导致 Docker 没拉起来的情况。内网环境没人 7x24 盯现场能在重启后自己恢复的容器化服务才是真正合格的部署。