Linux离线安装Docker:选型、备料与内网落地实战

发布时间:2026/10/27 11:16:18
Linux离线安装Docker:选型、备料与内网落地实战

1. 先把思路捋清楚:Linux 离线安装 Docker 到底难在哪

前段时间接到一个活,给一台完全封闭的内网机器装 Docker。这台机器不通外网,连内网的软件源都没有,只有一个管理员账号和一张光盘刻录出来的 U 盘。类似的场景我这些年遇到过好几次:工厂产线上的工控机、涉密环境的部署服务器、边缘机房的迷你主机,甚至有些云上机器的安全组策略把出站流量卡死,本质上和物理隔离没区别。Linux 离线安装 Docker这件事,听起来只是把安装包拷过去敲几条命令,真上手你会发现坑几乎全在"拷什么"和"拷过去之后它认不认"这两步上。

这篇内容我想讲清楚三件事:一是离线安装的几条技术路线各自适合什么机器,二是从零备料到装完跑通的完整操作链条,三是那些文档里不会写、只有踩过才知道的细节。适合的读者是已经会用 Linux 基本命令、但没系统性做过离线部署的同学,也适合手上有一批隔离环境要统一纳管的运维同学。全文讲的都是通用做法,具体版本号和包名请以你手头机器的实际情况为准。

写这篇文章的出发点很朴素:网上讲离线安装的文章不少,但大部分只给一段命令,不提为什么这么选,也不提装完之后的各种异常。我见过最典型的失败案例是,有人把联网机器上的 rpm 包一股脑拷过去用rpm -ivh --nodeps强装,装是装上了,systemctl start docker起来三秒钟就退出,日志里一堆依赖缺失的抱怨。所以这篇我尽量把"为什么"讲透,让你在遇到变种问题时能自己判断。

2. 三条技术路线怎么选:包管理、静态二进制、镜像搬运

2.1 先认清离线环境的真实约束

离线安装的难度,取决于目标机器的"贫瘠程度"。我习惯先问四个问题来定档:目标机是什么发行版、什么大版本、什么 CPU 架构、内核是多少。这四个信息缺一个,后面选包就可能全盘错。x86_64 的包拷到 ARM 机器上是绝对装不上的,CentOS 7 的 rpm 拿到 Ubuntu 22.04 上也一样白搭,这些听起来像废话,但实际项目里因为架构搞错返工的例子一点不少。

第二个约束是包管理器的完整性。有些精简版系统把yum或apt都裁掉了,只剩rpm或dpkg裸命令,甚至createrepo这种工具都没有。这种情况下,走本地仓库那条路会变得很别扭,反而静态二进制包更省事。所以我在动手前一定会先登录目标机看一眼:cat /etc/os-release、uname -m、uname -r,三条命令跑完心里就有数了。

第三个约束往往被忽略:目标机有没有可用的本地磁盘空间,以及/var/lib/docker打算放在哪。Docker 本体装完大概占几百兆,但真正吃空间的是镜像和容器层。我遇到过一台机器的根分区只剩 3G,装完 Docker 再导入两个业务镜像就满了,容器起不来还报一堆莫名其妙的错。这个事情必须在安装前就规划好,要么提前挂数据盘,要么在配置里改># 1. 装工具 yum install -y yum-utils createrepo # 2. 添加 Docker 官方软件源 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # 3. 建目录并下载全套包 mkdir -p /opt/docker-offline/packages yumdownloader --resolve --destdir=/opt/docker-offline/packages \ docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin # 4. 生成仓库元数据 createrepo /opt/docker-offline/packages

--resolve这个参数是关键,它会递归解析依赖并把所有需要的包一并下载。有人图省事不加这个参数,结果只下到五个主包,拷过去一装就报缺container-selinux、libcgroup、libseccomp之类。这几个依赖在 RHEL 系上几乎必现,尤其是container-selinux,版本要求还挺挑,低了不行。

createrepo那一步也别省。它会在目录里生成repodata子目录,目标机上的yum靠这个才能把这个目录当成一个合法的软件源来用。如果不生成,你配了 repo 文件,yum会报"无法检索仓库元数据"。

下载完可以顺手统计一下体积和文件数,做个记录,方便后续核对:

du -sh /opt/docker-offline/packages ls /opt/docker-offline/packages/*.rpm | wc -l

3.3 Debian 系:把 apt 缓存目录当转运站

Debian 系(Ubuntu、Debian 及其衍生版)没有yumdownloader这样的对口工具,但apt本身支持只下载不安装。核心思路是把下载缓存定向到一个独立目录,下完打包带走。

# 1. 添加 Docker 官方源(示例用 Ubuntu) apt-get update apt-get install -y ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \ > /etc/apt/sources.list.d/docker.list apt-get update # 2. 只下载不安装,定向到独立目录 mkdir -p /opt/docker-offline/debs/partial apt-get install -y --download-only \ -o Dir::Cache::archives="/opt/docker-offline/debs" \ docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin

这里有个细节值得单独说:Dir::Cache::archives指向的目录里必须先有partial子目录,否则apt可能直接报错退出。我第一次做的时候没建,apt 报了个"无法写入缓存"的错误,找了半天才发现是目录结构不全。这个坑我建议你直接在脚本里写死mkdir -p,省得每次都忘。

还有一个常见问题是依赖没下全。apt --download-only默认只会下载确实需要安装的包,如果目标机上已经有某个依赖的高版本,apt 就会跳过。所以要确保备料机器和目标机的软件包状态尽量接近,或者等待它下载完成后,用dpkg -I挨个看一下清单,对着目标机的dpkg -l比对一遍。

3.4 静态二进制包:最省心但也最需要手工收尾

如果你打算走静态二进制路线,备料就简单多了,只需要一个压缩包:

# 从官方静态包地址下载,注意替换架构和版本号 wget https://download.docker.com/linux/static/stable/x86_64/docker-24.0.7.tgz

这个包大概 70MB 左右,解压出来是一个docker/目录,里面包含了运行所需的全部可执行文件。它的好处是跨发行版通用,我在一些不常见的国产化平台上就是用这个方式装起来的。坏处是装完要自己写 systemd unit、自己创建docker用户组、自己建/etc/docker目录,工作量比包管理器路线多出不少。

选版本的时候有个经验:尽量不要选刚发布一两个月的大版本,等它出几个补丁版本再上。离线环境回滚成本高,稳定比新功能重要得多。

3.5 把材料打成一个自包含的离线包

料备齐后,我习惯把整个目录连同安装脚本、校验文件、说明文档一起打包成一个自包含的压缩包,通过介质带到目标机。目录结构大概长这样:

docker-offline/ ├── packages/ # rpm 或 deb 包目录 ├── images/ # 业务镜像 save 出来的 tar ├── config/ │ └── daemon.json # 预设的守护进程配置 ├── scripts/ │ ├── install.sh │ └── verify.sh ├── SHA256SUMS └── README.txt

打包前生成一份校验文件,这一步很多人省,但我觉得很有必要。介质拷贝过程中文件损坏不是玄学,我确实遇到过 U 盘拷到一半拔掉、文件大小对但内容不完整的案例,docker load时报格式错误,排查了半天。

cd /opt/docker-offline && find . -type f ! -name SHA256SUMS -exec sha256sum {} \; > SHA256SUMS tar -czf docker-offline.tar.gz -C /opt docker-offline

4. 落地实操:目标机器上的完整安装流程

4.1 装之前的系统体检与旧版本清理

拿到目标机第一件事不是装,是查。先确认系统信息和你备料时记录的一致:

cat /etc/os-release uname -m uname -r df -h /var free -g

然后是清理旧版本。很多机器上其实装过老版本的 Docker,或者是发行版自带的docker-io、podman-docker之类的包,这些东西会和新版本抢/usr/bin/docker。RHEL 系这么清:

yum remove -y docker docker-client docker-client-latest docker-common \ docker-latest docker-latest-logrotate docker-logrotate docker-engine \ podman-docker containerd runc

Debian 系这么清:

apt-get remove -y docker docker-engine docker.io containerd runc

注意:清理旧版本前一定要确认这台机器上没有正在运行的容器。yum remove或apt remove会连带删掉/var/lib/docker之外的一些东西,如果之前有业务在跑,容器数据可能就没了。稳妥做法是先systemctl stop docker,把需要的容器 commit 成镜像再卸载。

还有一个体检项容易被跳过:内核版本。Docker 对内核的最低要求是 3.10,但那是很宽泛的说法。真正影响体验的是overlay2存储驱动能不能用,它要求内核 4.0 以上,并且如果底层文件系统是 XFS,ftype必须是 1。用xfs_info /可以看,输出里ftype=0的话 overlay2 就用不了,只能退到vfs,性能会差一大截。这个检查我在 CentOS 7 的老机器上做过好几次,确实有默认ftype=0的情况。

4.2 走本地仓库安装 rpm 包

把离线包解压到/opt/docker-offline,然后配一个指向本地目录的 repo 文件。这一步的关键是让yum完全不去碰任何远程源,避免它在离网环境下卡在超时上干等。

tar -xzf docker-offline.tar.gz -C /opt cat > /etc/yum.repos.d/local-docker.repo <<'EOF' [local-docker] name=Local Docker Repository baseurl=file:///opt/docker-offline/packages enabled=1 gpgcheck=0 EOF

gpgcheck=0在这里是必要的妥协。官方源有签名校验,本地目录没有配置公钥就会报错。如果你的安全规范要求签名校验,就需要把 Docker 官方的 GPG 公钥一并带过去并rpm --import导入,然后改成gpgcheck=1。这个取舍看你们的环境要求,我个人在内部环境里为了省事一般关掉。

安装命令要带上--disablerepo='*',把其他所有源都禁掉,只留本地这一个:

yum clean all yum --disablerepo='*' --enablerepo=local-docker install -y \ docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin

yum clean all这一步别省。有些机器上残留了之前配置过的远程源元数据,不清掉的话yum可能会拿旧缓存来做依赖解析,出现"包已安装但版本不对"的诡异情况。

装完立刻查版本,确认和你备料时选的一致:

docker version containerd --version

4.3 走二进制方式的手工部署

如果目标机用不了包管理器,那就走手工部署。整个流程大概分四步:铺二进制、写 unit、建目录、起服务。

# 1. 解压并铺开可执行文件 tar -xzvf docker-24.0.7.tgz cp docker/* /usr/bin/ chmod +x /usr/bin/docker* /usr/bin/containerd* /usr/bin/runc /usr/bin/ctr # 2. 建配置目录 mkdir -p /etc/docker /var/lib/docker # 3. 写 systemd unit cat > /etc/systemd/system/docker.service <<'EOF' [Unit] Description=Docker Application Container Engine Documentation=https://docs.docker.com After=network-online.target firewalld.service Wants=network-online.target [Service] Type=notify ExecStart=/usr/bin/dockerd ExecReload=/bin/kill -s HUP $MAINPID LimitNOFILE=infinity LimitNPROC=infinity LimitCORE=infinity TimeoutStartSec=0 Delegate=yes KillMode=process Restart=on-failure StartLimitBurst=3 StartLimitInterval=60s [Install] WantedBy=multi-user.target EOF # 4. 启用并启动 systemctl daemon-reload systemctl enable docker --now

这个 unit 文件里有几个参数值得解释。Type=notify表示dockerd启动完成后会主动通知 systemd,这样systemctl start的返回才准确,否则 systemd 会以为它秒退。Delegate=yes是让 systemd 把 cgroup 的控制权交给 Docker,容器自己管理资源限制时需要它。LimitNOFILE=infinity是放开文件描述符上限,容器多的时候默认的 1024 完全不够用,会报 "too many open files"。

还有一个容易漏的:手动创建docker用户组。包管理器安装时会自动建,二进制方式不会。如果你要给普通用户免 sudo 用 docker,就得自己来:

groupadd -f docker usermod -aG docker your_user

改完组之后用户要重新登录才生效,newgrp docker也能临时生效,但只对当前 shell 有效。

4.4 daemon.json:几个必须提前定好的参数

/etc/docker/daemon.json这个文件决定了 Docker 守护进程的行为,装完第一时间就该配好,别等出了问题再回头改。我常用的模板如下:

{ "data-root": "/data/docker", "exec-opts": ["native.cgroupdriver=systemd"], "storage-driver": "overlay2", "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" }, "insecure-registries": ["10.0.0.10:5000"], "live-restore": true }

逐条说一下为什么这么写。># 联网机 docker pull nginx:1.25-alpine docker pull redis:7.2-alpine docker pull mysql:8.0.35 # 多个镜像可以一次性 save 到一个文件,省空间也省事 docker save -o /opt/docker-offline/images/base-images.tar \ nginx:1.25-alpine redis:7.2-alpine mysql:8.0.35 # 体积大的话顺手压缩 gzip -9 /opt/docker-offline/images/base-images.tar

目标机上导入:

docker load -i /opt/docker-offline/images/base-images.tar.gz docker images

这里有两个实操细节。一是docker save出来的 tar 保留了完整的镜像标签和层结构,load回去标签不会丢,这点比导出容器快照再导入可靠得多。二是如果镜像多、体积大,save过程会占用大量磁盘,建议save和gzip用管道串起来,避免中间文件占两倍空间:

docker save nginx:1.25-alpine | gzip -9 > nginx.tar.gz

如果内网有私有仓库,导入后可以打个指向内网的 tag 再 push 进去,后续其他机器直接从内网拉,就不用每台都搬 tar 包了。

5. 验证、排障与长期维护的实战经验

5.1 装完必跑的六项自检

装完 Docker 别急着交差,按下面这个清单跑一遍,能提前发现九成的问题。

检查项命令期望结果
版本是否匹配docker version客户端与服务端版本一致
服务是否自启systemctl is-enabled docker输出enabled
守护进程配置docker info各参数与你写入的一致
网络是否可用docker run --rm alpine ping -c1 8.8.8.8能通(内网需换目标)
存储驱动docker info | grep Storage显示overlay2
日志轮转docker info | grep -A2 "Log"max-size 与 max-file 生效

这几项里我最看重的是存储驱动和网络。存储驱动退化成vfs的话,容器的每一次写操作都会复制整个层,性能能差十倍以上,但因为不报错,很多人装了之后一直没发现。网络问题则通常在docker run时才暴露,尤其是防火墙开着的情况下,容器内访问外部大概率失败。

5.2 典型报错的排查路径

离线安装环境里高发的报错就那么几类,我整理成一张速查表,遇到时按顺序排查能省不少时间。

报错现象可能原因处理办法
container-selinux >= 2:2.9 is needed依赖版本过低把高版本 selinux 包一并备好装上
Job for docker.service failedunit 文件路径或参数错journalctl -u docker -n 50看具体原因
iptables: No chain/target/match防火墙规则冲突或内核模块缺失重载iptables模块,或临时关掉防火墙验证
failed to start daemon: pid file exists上次异常退出残留删/var/run/docker.pid后重启
no space left on device磁盘满查docker system df,清理无用镜像和日志
overlay2 not supported内核太旧或 XFS ftype=0升级内核或改用其他存储驱动
docker load报格式错误压缩包在传输中损坏用 SHA256SUMS 校验后重新拷贝

网络类问题值得多讲两句。离线环境最常见的现象是容器能启动但访问不通外部。排查顺序建议是:先在宿主机上curl目标地址确认宿主机网络本身正常,再看iptables -t nat -L -n里有没有 Docker 生成的规则链,最后看sysctl net.ipv4.ip_forward是否等于 1。转发没开是高频原因,改完sysctl -w net.ipv4.ip_forward=1并写进/etc/sysctl.conf持久化。

还有一个特别隐蔽的问题:SELinux 处于 enforcing 模式时,容器挂载宿主机目录会报 Permission denied。离线环境里没法临时去查文档,我的处理办法是先在宿主机上执行getenforce确认状态,如果确实是 enforcing 且业务不需要,可以临时设成 permissive 验证问题是否由此引起,确认后再决定是加:z挂载标签还是彻底调整策略。这个操作有安全影响,改之前务必和负责安全的同事确认。

5.3 升级与回滚:离线环境里更要留后路

离线环境升级 Docker 比在线麻烦,因为没有yum update这种一键操作。我的做法是每次升级都把旧版本的全套包和镜像保留在一个单独目录里,命名带上版本号和日期,回滚时直接切本地 repo 的baseurl指回旧目录再装一遍。

# 备份当前状态 mkdir -p /opt/docker-rollback/$(docker version --format '{{.Server.Version}}') cp -r /opt/docker-offline/packages/* /opt/docker-rollback/$(docker version --format '{{.Server.Version}}')/

升级前记得先docker save一份重要镜像,并且确认/etc/docker/daemon.json里有没有已经不兼容的旧参数。有一类升级事故是存储驱动或 cgroup 驱动在新版本里改了默认值,重启后容器全起不来,这种就只能靠备份和回滚兜底。

5.4 几条踩出来的经验

最后分享几个我认为价值最高的实操心得,都是文档里不会写的。

第一,备料清单要按"目标机缺什么"来定,不是按"官方装什么"来定。官方文档给的是标准安装路径,但目标机上很可能已经装了某个高版本的依赖,反而引起冲突。我现在的做法是先把目标机的rpm -qa或dpkg -l全量导出,拿到备料机器上比对,只下真正缺的包,这样能避免版本打架。

第二,离线包一定要带上createrepo生成的元数据和一份纯手工安装的备选脚本。本地 repo 有时候会因为yum版本差异读不了元数据,这时候rpm -ivh --nodeps --force虽然不优雅,但能救场。当然这只是兜底手段,装完必须手工确认所有依赖都在。

第三,镜像的save和load环节,标签管理要统一规范。我见过太多环境里镜像是latest、v1、test混着用,过两个月谁也说不清哪个是线上版本。我的规矩是只用带明确版本号的标签,latest一律不用,离线包里附一份镜像清单文本,记录每个镜像的完整名称、大小和引入日期。这个习惯在离线环境里尤其重要,因为你没法随时去公共仓库查某个标签现在指向什么。

第四,时间同步这件事容易被忽略。离线机器的时钟可能不准,而 Docker 拉镜像、TLS 握手都依赖时间正确。虽然离线场景不拉镜像,但如果你后面接了内网私有仓库,证书校验就会因为时间偏差失败。装完顺手确认一下date输出,误差大就配个内网时间源。

第五,把安装过程写成脚本,哪怕只装一台。我一开始也觉得自己记住就行,后来发现同一套流程隔几个月再做一次,细节总会漏那么一两个。现在我的离线包里固定带一个install.sh,参数化处理发行版判断、解压路径、repo 配置和服务启动,跑完再做人工核对。脚本本身也是一份可执行的文档,交接给别人时省下大量沟通成本。