1. 离线安装前,先想明白这三件事
不少同学第一次接触“离线安装docker”这个概念,第一反应是找一个 docker 的安装包丢到服务器上解压就能用。如果你也这么想,建议先停一下——离线安装这件事,真正麻烦的不是“怎么拷文件”,而是“怎么把一套完整可用的运行时环境,从零开始在一台没有外网的机器上搭起来”。
这里说的“完整可用”,至少包含三层含义:一是 docker 引擎本身能启动;二是 docker-compose 能配合工作;三是容器跑起来之后,镜像从哪儿来。第三点往往是被忽略的,后面我会单独拿一小节来聊。
开始动手之前,请确认你的目标机器是什么操作系统和 CPU 架构,这是所有离线方案的地基。最简单的确认命令:
cat /etc/os-release uname -m拿最典型的 CentOS 7 来说,我需要知道它的内核是不是 3.10(CentOS 7 默认内核),因为内核版本直接决定后面存储驱动怎么选,也决定容器运行是否有隐患;同时确认是 x86_64 还是 arm64 架构,因为不同架构的 rpm 包、二进制包完全不能混用。
还有一件事要先掂量清楚:你手头是否有一台可以上网、且和目标机器操作系统版本完全一致的机器。这是做离线包的最佳“加工车间”。如果没有,就得走纯二进制包路线,我把两条路都列出来,你按条件选。
另外提醒一下,离线安装不是装完就结束的事。Docker 的版本更新很快,离线环境尤其要提前规划好版本。我个人的习惯是:尽量选稳定的主版本里最新的小版本,比如 docker-ce 24.x 或 26.x,而不是随便抓一个版本就装。版本太老,后面拉新镜像、跑新 compose 文件(比如用到 depends_on 新语法、profiles 功能)很可能遇到兼容问题,那时候再升级,在内网环境下会非常痛苦。
2. 最顺手的路:用 yumdownloader 在联网机器打包 rpm
如果你的条件是“有一台同系统的联网机器”,那这条路是最省心的。核心思路是在联网机器上用包管理器把 docker 及其所有依赖包下载到本地,再把它们打包传到内网机器上离线安装。
2.1 在联网机器上准备打包环境
我先准备一台 CentOS 7.9 的联网机器,然后安装 yum-utils 工具集,里面包含了接下来要用的 yumdownloader 命令:
yum install -y yum-utils然后把 Docker 官方仓库配置好:
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo这里解释一下为什么要配官方仓库而不是直接用系统自带的源:CentOS 自带的源里虽然也有 docker(老版本的 docker 1.13),但版本过于陈旧,而且 docker-compose 插件的支持方式完全不同。官方仓库能拿到当前最新的 docker-ce、docker-ce-cli、containerd.io 等组件,并且会处理好版本配套关系。
2.2 下载指定版本及全部依赖
如果你对版本没有特殊要求,直接下载最新稳定版就可以。但我建议先看下仓库里有哪几个大版本可选:
yum list docker-ce --showduplicates | sort -r确定版本号之后,用下面的命令下载 docker 引擎和命令行工具。注意我用了--resolve参数,它的作用是分析依赖关系并把所有依赖包一起下载下来。这是离线安装中最容易遗漏的环节——很多人只下载了 docker-ce 一个包,拷到内网一装才发现缺一堆依赖,比如 container-selinux、libcgroup、iptables 等。
cd /root/docker-offline yumdownloader --resolve docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin如果你需要指定版本,下载时要带上完整的版本号:
yumdownloader --resolve docker-ce-24.0.9-1.el7 docker-ce-cli-24.0.9-1.el7 containerd.io docker-buildx-plugin docker-compose-plugin关于docker-buildx-plugin和docker-compose-plugin:这两个是 docker 官方推荐的新一代构建与编排插件。docker-compose-plugin 对应的是docker compose子命令(带空格),而老的独立版 docker-compose 以后面章节提到的二进制方式安装。如果你只需要docker compose插件,那这里下载它就够了,不需要再额外下载独立的 docker-compose。
下载完成后确认一下目录里的文件:
ls -lh /root/docker-offline你会看到类似这样的输出(版本号可能不同):
-rw-r--r-- 1 root root 25M 6月 18 10:00 containerd.io-1.6.28-3.1.el7.x86_64.rpm -rw-r--r-- 1 root root 21M 6月 18 10:00 docker-buildx-plugin-0.13.1-1.el7.x86_64.rpm -rw-r--r-- 1 root root 40M 6月 18 10:00 docker-ce-24.0.7-1.el7.x86_64.rpm -rw-r--r-- 1 root root 14M 6月 18 10:00 docker-ce-cli-24.0.7-1.el7.x86_64.rpm -rw-r--r-- 1 root root 14M 6月 18 10:00 docker-compose-plugin-2.24.2-1.el7.x86_64.rpm2.3 把 rpm 拷进内网并离线安装
打包传到内网机器上之后,安装不要用rpm -ivh *.rpm,因为 rpm 命令不处理依赖顺序,遇到依赖问题只会报错退出,而不会自动按顺序装。建议用yum localinstall:
cd /root/docker-offline yum localinstall -y *.rpmyum localinstall会读取当前目录下的 rpm 包,自动分析并解决包与包之间的依赖关系。对于离线环境来说,只要目录里的包是全的,它就一定能装成功。
这里有个经验性建议:装完后先不要急着启动 docker,先查看一下生成的配置目录和文件:
ls -l /etc/docker/ cat /etc/docker/daemon.json如果你的系统里没有 /etc/docker/daemon.json,这是正常的。启动 docker 之前,我建议先手动创建这个配置文件,把基础参数定下来。具体配置我在第 3 章和第 4 章里结合场景详细说明。
所有 rpm 装完、配置写好后,再执行:
systemctl daemon-reload systemctl enable --now docker systemctl status docker到这里,docker 引擎就起来了。如果systemctl status docker显示 active (running),可以顺手跑一条验证命令:
docker info --format '{{.ServerVersion}}' docker compose version后面这条能顺便确认 docker compose 插件是否装好。
2.4 这个方案里最容易踩的三个坑
坑一:联网机和内网机的操作系统小版本不一致。
比如联网机是 CentOS 7.9,内网机是 CentOS 7.6,依赖包的兼容性通常没有问题,但是内核版本差异会影响容器运行时的行为,比如 overlay 驱动的兼容性判断。建议把内网机的内核和系统补丁级别尽量向联网机靠拢,或者干脆在做离线包的内网机上继续后续所有操作。
坑二:下载时没有加--resolve或--downloadonly导致缺依赖。
如果你已经因为缺依赖装到一半失败了,也不用慌,回联网机上执行提示里的 yum download 补充下载对应包,重新拷过来再yum localinstall即可。
坑三:docker-ce 的 repo 在 CentOS 7 上默认启用的是 stable 源,但 SELinux 策略包(container-selinux)不在官方源里。
容器启动时报 SELinux 拒绝的错,很容易误判成 Docker 问题。解决办法是提前把 container-selinux 也下载过来:
yumdownloader --resolve container-selinux或者更省事的方式:如果内网机器(尤其是生产环境)对 SELinux 依赖不严格,可以在离线机器上临时把 SELinux 设为 permissive 模式,先把 Docker 跑通,后续再根据安全策略进行调整。但这是取舍问题,如果你所在的安全团队要求 SELinux 强制开启,那就必须把 container-selinux 一并装好。
3. 另一条路:官方静态二进制包直接部署
没有同系统联网机器怎么办?比如只有一台 Windows 电脑,或者联网机器是 Ubuntu,但内网机器是 CentOS 7——这时候 rpm 路线就断了,因为跨发行版下载的 rpm 包不能相互安装。此时我会选择 Docker 官方发布的静态二进制包(static binary),这条路不依赖任何包管理器,把压缩包解开就能用。
3.1 下载 docker 静态包并解压
访问 Docker 官方 release 页面,下载docker-<版本>.tgz即可。比如下载 24.0.9 版本:
wget https://download.docker.com/linux/static/stable/x86_64/docker-24.0.9.tgz里面有这些组件:
containerd/ containerd-shim-runc-v2 ctr docker dockerd docker-init docker-proxy runc注意,这个静态包里不包含 docker compose 插件,需要单独下载 docker-compose(独立二进制),我后面会讲。
解压并放到 /usr/bin 目录:
tar -xzf docker-24.0.9.tgz cp docker/* /usr/bin/把这些二进制放在 /usr/bin 里,可以保证任何用户都能直接执行 docker 命令,也方便 systemd 服务调用。如果你想保持目录整洁,也可以放在 /usr/local/bin,但要注意 PATH 环境变量里是否包含它。
3.2 手工编写 systemd 服务单元
静态包方式不会自动注册 systemd 服务,需要自己写。创建/etc/systemd/system/docker.service:
[Unit] Description=Docker Application Container Engine Documentation=https://docs.docker.com After=network-online.target firewalld.service containerd.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这个文件里最值得注意的有两处,一是Type=notify,这是 dockerd 通过 sd_notify 协议通知 systemd “我已经准备好”的方式,如果 Type 写错(比如写成 simple),会导致 systemctl start docker 之后长时间卡住不动;二是KillMode=process,它保证在重启 docker 服务时,不会把已经运行的容器进程一并杀掉,这一点在生产环境尤其重要。
再创建/etc/systemd/system/docker.socket:
[Unit] Description=Docker Socket for the API [Socket] ListenStream=/var/run/docker.sock SocketMode=0660 SocketUser=root SocketGroup=docker [Install] WantedBy=sockets.target创建好这两个文件后,重载并启用服务:
systemctl daemon-reload systemctl enable --now docker.socket docker.service3.3 没有 docker 用户组?先建一个
注意到上面 socket 文件里写了SocketGroup=docker,如果系统里还没有 docker 用户组,docker.socket 会启动失败。需要先执行:
groupadd docker把需要免 sudo 使用 docker 的普通用户加入这个组:
usermod -aG docker yourusername这一步很多人容易漏。漏掉的结果是:docker 服务虽然能启动,但非 root 用户执行 docker 命令会报Got permission denied while trying to connect to the Docker daemon socket,排查时容易绕弯路。
3.4 初始化 daemon.json,避免根目录写满
不管用哪种方式安装,我都建议启动 docker 之前先创建好/etc/docker/daemon.json。一个适合离线服务器场景的初始配置如下:
{ "data-root": "/data/docker", "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" }, "storage-driver": "overlay2", "iptables": true, "ip-masq": true, "registry-mirrors": [] }说一下每个配置项的意图:
>docker info --format '{{.DockerRootDir}}'输出应该是 /data/docker 而不是默认的 /var/lib/docker,那就对了。
4. docker-compose 的离线安装,别搞混两个版本
接下来是 docker-compose 的处理。这里有一个非常常见的混淆点:
docker-compose(带横线,独立的二进制工具)和docker compose(子命令,Docker CLI 的插件)是两种不同的东西。离线安装时要先明确自己需要哪种,不要装混了。4.1 新一代
docker compose插件如何离线部署如果你的 docker 版本是 20.10 以上,我强烈推荐直接用 docker compose 插件。它的最大好处是:随 docker 一起管理,不需要额外维护一个独立二进制,而且对 compose 文件的语法支持更全面。
rpm 方式安装时,上面的
yumdownloader命令里只要包含docker-compose-plugin包,装完就有了。二进制方式的话,需要单独下载 compose 插件二进制。注意它必须放在指定目录里才会被 docker 识别:
mkdir -p /usr/libexec/docker/cli-plugins cp docker-compose-linux-x86_64 /usr/libexec/docker/cli-plugins/docker-compose chmod +x /usr/libexec/docker/cli-plugins/docker-compose验证:
docker compose version能输出类似
Docker Compose version v2.24.2就说明配置正确。这里补充一下插件查找目录的细节:docker cli 会依次查找
/usr/local/lib/docker/cli-plugins、/usr/libexec/docker/cli-plugins、~/.docker/cli-plugins等路径。如果你把插件放到了用户目录(~/.docker/cli-plugins),那只有该用户执行 docker 命令时才能用,切换到其他用户或 root 就找不到了。部署到/usr/libexec/docker/cli-plugins是全局生效、最稳妥的路径。4.2 老的独立版
docker-compose二进制安装法有些老项目或者旧脚本里还会调用
docker-compose命令(带横线),这种情况就得安装独立版。下载地址是 GitHub Releases 页面的 docker-compose 仓库,离线环境下需要先从能上网的机器下载好,再拷贝进去:wget https://github.com/docker/compose/releases/download/v2.24.2/docker-compose-linux-x86_64 cp docker-compose-linux-x86_64 /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose注意,GitHub 国内网络环境下载可能时断时续,建议用能稳定上网的机器下载,或者找内网已有资源同步。这类工程文件的校验也很重要:
sha256sum docker-compose-linux-x86_64把输出和官方发布页提供的 checksum 对比一致后再拷入内网,防止文件损坏或被人替换。
验证安装:
docker-compose version4.3 两个版本能不能共存?
可以共存,我实际也这么干过。
docker compose插件不影响独立的docker-compose二进制。但有一个容易踩坑的地方:当你执行docker-compose(带横线)时,它不会读取/etc/docker/daemon.json里的某些配置(比如 registry 地址),它直接调用 docker CLI 的能力。如果你的离线环境配置了私有仓库地址,独立版 docker-compose 可能提示认证失败,这时候优先检查 docker CLI 的 auth 配置而不是怀疑 compose 文件写错了。还有一个小建议:写脚本或 CI/CD 配置时,统一用
docker compose命令(空格),避免两种风格混用导致维护混淆。我在一些客户环境里见过 docker-compose.yml 里同时被两种命令调用,后面排查问题时非常费劲。5. 启动 docker 后,镜像怎么办——离线环境最容易被忽略的最后一公里
安装完 docker 和 compose,你可能会觉得大功告成了。但离线环境的第二个硬骨头马上就会出现:内网机器上一条
docker pull命令会卡住不动,因为根本连不上互联网。5.1 用 docker save 和 docker load 搬运镜像
最简单的镜像是通过 tar 文件搬运。在一台有网机器上拉取需要的镜像,然后打包:
docker pull mysql:8.0 docker save mysql:8.0 | gzip > mysql-8.0.tar.gz拷贝到内网机器后导入:
docker load -i mysql-8.0.tar.gzdocker save保留镜像的全部层和元数据;docker load则会把层还原出来。要注意一点:save 时如果镜像带 tag,load 之后 tag 也会完整保留,但假如源镜像没有打 tag(显示为<none>),load 之后要手动打 tag 才能正常使用:docker tag <image-id> mysql:8.0这一块我想特别强调:如果你要搬运多个镜像,不要一个一个 save,可以把多个镜像写在一个命令里,一次性导出成一个 tar 文件:
docker save nginx:1.25 mysql:8.0 redis:7.0 | gzip > app-images.tar.gz内网机器上一次 load 全部导入:
docker load -i app-images.tar.gz这种方式比循环 save/load 单个镜像要高效得多,而且整体性更好。
5.2 在内网搭建 registry,让所有机器共享镜像
如果要部署的服务器不止一台,每次都手动 copy tar 包会非常低效。我的建议是:在内网挑一台机器搭一个私有 registry,其他机器直接从这台 registry 拉镜像,体验和公网 Docker Hub 几乎一样。
在内网这台机器上导入 registry 镜像并启动:
docker pull registry:2.8.3 docker run -d \ --name registry \ --restart=always \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ registry:2.8.3然后其他机器要改一下 docker 客户端配置(因为默认走 HTTPS,而内网 registry 通常只提供 HTTP),在
/etc/docker/daemon.json中加入:{ "insecure-registries": ["192.168.1.100:5000"] }配置完成后重启 docker:
systemctl daemon-reload systemctl restart docker推拉镜像的方式和 Docker Hub 一样:
docker tag mysql:8.0 192.168.1.100:5000/mysql:8.0 docker push 192.168.1.100:5000/mysql:8.0 docker pull 192.168.1.100:5000/mysql:8.0如果你出于安全考虑不想走 HTTP,可以在内网给 registry 配置自签证书。具体做法是用 openssl 生成自签证书,挂载到 registry 容器的
/certs目录,并设置环境变量 REGISTRY_HTTP_TLS_CERTIFICATE 和 REGISTRY_HTTP_TLS_KEY。这样每台客户端的 docker 还要信任这个 CA 证书,操作比 insecure-registry 要繁琐,但更符合企业安全要求。我在实际项目里推荐的做法是:小规模(几台机器)直接 tar 包搬运;规模超过 5 台,直接上 registry。
5.3 离线环境使用 compose 文件的注意事项
离线环境下用 docker compose 编排时,compose 文件里所有镜像都必须存在于本地或者内网 registry 中。如果你直接拿公网教程里的 compose 文件跑,大概率会因为镜像拉不下来而报错。
解决办法是:先在联网机器上把 compose 文件里需要的所有镜像一次性 pull 下来,再 save 导出:
docker compose config --images > images.txt while read img; do docker pull "$img"; done < images.txt docker save $(cat images.txt) | gzip > composed-images.tar.gz这会把你整个 compose 栈涉及的镜像全部拉下来、打包。到了内网机器上 load 完成后再
docker compose up -d就能正常拉起服务。这个技巧还有个额外好处:
docker compose config --images这个命令会解析整个 compose 文件里所有 service 的镜像引用,包括 build 块里用到的镜像依赖,比手动一个个敲要准确得多。6. 常见故障:离线装完之后的几个高频问题排查
装完之后我见过太多人卡在启动和运行阶段,这里把最常碰到的几个问题集中说一下,按出现频率排序。
6.1
Cannot connect to the Docker daemon这条报错往往不是因为 docker 没安装,而是服务没起来或 socket 文件异常。排查思路按顺序来:
systemctl status docker journalctl -u docker --no-pager -n 50 ls -l /var/run/docker.sock如果日志显示
failed to start daemon: Error initializing network controller: error creating default "bridge" network: iptables failed,通常是机器的 iptables 内核模块没加载或规则冲突。离线环境里常见原因是系统自带了 firewalld 但又被停用,docker 起网桥时操作 iptables 被拒绝。可以先检查:systemctl status firewalld iptables -L -n临时处理可重载 iptables 相关内核模块:
modprobe br_netfilter sysctl -w net.bridge.bridge-nf-call-iptables=1然后重启 docker。
6.2 网络地址转换和端口映射不通
离线环境可能使用了比较严格的主机防火墙。容器启动成功但宿主机外部访问不到容器端口时,先确认防火墙是否放行:
firewall-cmd --list-ports firewall-cmd --zone=public --add-port=8080/tcp --permanent firewall-cmd --reload如果用 iptables 服务,则需要手动添加规则。很多人在这一步反复重启容器、反复检查 compose 文件,实际上就是防火墙规则挡了。建议启动容器前先把 443/80 或业务端口在宿主机防火墙里放行,再通过
curl http://127.0.0.1:映射端口验证。6.3 compose 文件语法导致的启动失败
离线环境经常用 Docker Compose 部署多服务应用,如果
docker compose up -d报错,比如services.db.environment must be a mapping,这种大多是 YAML 缩进问题或版本语法问题。建议先把文件在本地用docker compose config验证,它能快速定位语法错误,不需要真正启容器。还有一点:compose 文件里如果指定了
build:块,docker compose 在老版本里会试图构建镜像,但离线环境往往没有构建所需的镜像层和网络,会导致卡住。如果你只是部署现成镜像,确保 compose 文件里没有 build 指令,或者提前把需要的镜像 load 好。6.4 存储驱动不被内核支持
CentOS 7 上偶尔看到这样的报错:
failed to mount overlay: no such file or directory,意思是内核不支持 overlay2 文件系统,常见于默认内核版本较老的 CentOS 7.x。临时解法是改用 vfs 驱动(不推荐,性能和磁盘占用都很难接受)或者在启动脚本层面分析;长期解法是升级内核或用官方支持的新系统(比如 Rocky Linux 9、Ubuntu 22.04)重新部署。
如果必须留在 CentOS 7 且内核版本支持 overlay,但某些模块未加载,可以先执行:
modprobe overlay再重启 docker。如果还是报错,就查看是否要增加内核启动参数,这属于系统层适配,不是 docker 本身的问题,但离线环境下确实会把你绊住。
7. 最后的经验总结:离线安装看起来是“能上网/不能上网”的问题,其实是“交付物设计”的问题
我做了这么多次离线环境部署,最大的体感是:离线安装的核心不是命令,而是“在有限条件下,把交付物最小化、最完整化”。
rpm 方式也好、静态二进制方式也好,都不是什么高深技术,真正的难点在于你把什么算作“安装包”。如果你只是拷一个 docker 的 rpm,内网环境大概率装不上,因为缺依赖;如果你做了一个包含所有依赖的离线仓库目录,后续再有新的机器要从零安装,你的交付物就非常可靠。
我个人的习惯是,一个完整的内网交付物包含四样东西:
- docker 引擎相关 rpm 包目录(或静态二进制 + systemd 文件)
- docker-compose 独立二进制 / 插件二进制
- 一份 daemon.json 样例
- 一个镜像 tar 包目录,或者一台内网 registry 地址
这四样东西放在一起,才是真正意义上的“离线安装包”。只放一个安装文件,那不叫离线安装包,那叫给你自己挖坑。
另外多说一句,离线环境下不要轻易尝鲜最新版本。Docker 软件本身更新节奏快,新版本可能对内核、systemd、iptables 行为有变化。我在长期维护的离线系统上倾向于固定一个大版本,比如 24.0 系列或 26.1 系列,然后把小版本也锁死。容器运行时不追求“最新”,追求“经得起时间检验的稳定”。
在整理镜像依赖时,也可以用
docker image inspect看镜像的配置信息,确认它依赖的架构、运行用户、暴露的端口是否符合你的内网环境。这些检查在公网环境下可以不管,但离线环境一旦部署下去,改一次配置都要来回拷文件,所以“上机前多花十分钟,上线后少折腾一整天”这句话,放在离线部署场景里再合适不过了。