干运维这些年,我碰到过太多次这种场面:内网机房一台刚上架的新服务器,安全基线和系统加固全部做完,就差装一个 Docker,结果发现这台机器根本摸不到公网。yum 源连不上,apt 源超时,手动去下个安装包,一连串依赖补完这个缺那个,“拆东墙补西墙”补到怀疑人生。更扎心的是,这种场景往往还伴随一句“今天就要上线”。
这篇文章就讲一件事:怎么在一台没有外网、连软件源都不齐全的机器上,用一套 docker 离线一键安装包,把 Docker 干净利落装上。我会把离线包的制作过程、install.sh 脚本里每个关键步骤的用意、以及生产环境里真正会踩的坑全部摊开讲。不管你是刚入门的新手,还是被内网环境反复摩擦过的老运维,按这套思路走,基本能做到“拷过去、跑起来、能验证”三步到位。
1. 为什么必须搞离线安装包:三条技术路线的真实取舍
1.1 内网环境的真实困境
先放下技术方案,把场景说透。平时在线装 Docker,官方一键脚本或者发行版的包管理命令几分钟就能搞定,所有依赖自动解决。但离线环境完全是另一回事:没有外网,默认软件源不可用,机器上可能连编译环境都没有。你面对的不只是“缺一个包”,而是一整条依赖链全部缺失。
我见过不少同事在这种环境里试图用安装包逐个搞定,结果装到一半开始报依赖错误,最后只能删掉重来。线下环境里时间比什么都值钱,这种碰运气式的装法,第一轮就该排除掉。
1.2 三条技术路线怎么选
在动手之前,我把常见做法拉出来对比过一轮:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 自建软件源 | 在有网的机器上同步 Docker 官方仓库,做成内网 yum/apt 源 | 和系统包管理深度集成,装完就是“正规军” | 包多体积大,源结构脆,换发行版要重新同步 |
| 静态二进制包 | 官方提供编译好的 dockerd、docker、containerd、runc,解压即可用 | 跨发行版通用,体积小,行为完全可控 | systemd 服务、配置、内核参数全部要自己补齐 |
| 镜像仓库缓存 | 在内网跑一个 registry,解决的是“装完以后镜像怎么拉”的问题 | 适合长期持续交付 | 救不了“Docker 还没装上”的第一步 |
最终我选了静态二进制包这条路线。原因很实在:同一份包,CentOS 7、CentOS 8、Ubuntu 20.04、Debian 11 上都跑过,不用为每个发行版维护一套源;整个安装包压缩下来不到 100MB,U 盘上一拷就能走;最关键的是,所有的服务文件、配置文件都是自己写的,任何一条执行了什么、为什么要执行,心里都门儿清。
1.3 这套方案的边界也讲清楚
静态二进制包适用于 Linux x86_64 和 aarch64 服务器,尤其适合隔离网络、批量交付、临时机房扩容这类场景。但它不是万能的:Windows 和 macOS 上运行的 Docker Desktop 是另一套图形化方案,依赖宿主机虚拟化功能,后面我单独列一节说明;另外官方静态包依赖 glibc,所以宿主系统太精简(比如纯 musl 的 Alpine)的时候不一定跑得起来,服务器环境一般不会有这个问题。
2. 制包机准备:版本、架构与文件清单
2.1 版本和架构怎么选
第一步是在一台能正常访问外网的“制包机”上把素材准备好。版本选择只有一个原则:生产环境不追新。新版本功能多,但引入的坑也多。我一般选当前时间点已经发布两三个月以上、社区反馈比较成熟的次版本号,比如 26.1.x。2024 年我用的是 docker-26.1.4,这个版本在存量系统上的表现比较踏实,容错也做得好。
架构上先确认目标机器是 x86_64 还是 aarch64。现在很多机房都有 ARM 服务器,下载包的时候必须选对应的架构目录,拿 x86_64 的包硬装到 ARM 机器上,一启动就是 “Exec format error”,这个错提示得还挺直白。
2.2 下载官方静态包的正确姿势
官方静态包的地址有固定规律,在制包机上这样操作:
mkdir -p /data/docker-pkg cd /data/docker-pkg wget https://download.docker.com/linux/static/stable/x86_64/docker-26.1.4.tgz sha256sum docker-26.1.4.tgz下载完先做两件事:一是校验 sha256,官方页面会给出对应值,别省这一步,传输损坏的包装到一半才报错才是最耽误事的;二是解开看一眼内容,确认里面的二进制都在:
tar -tzf docker-26.1.4.tgz正常会看到 docker 目录下包含这些文件:dockerd、docker、containerd、containerd-shim-runc-v2、ctr、runc、docker-init、docker-proxy。一个都不缺就可以进入下一步了。
2.3 离线包目录结构怎么设计
我习惯把整个交付物做成一个自包含目录,目标机上不管放到哪个路径都能跑:
docker-offline/ ├── install.sh ├── uninstall.sh ├── daemon.json ├── systemd/ │ ├── docker.service │ ├── docker.socket │ └── containerd.service ├── docker-26.1.4.tgz └── images/ └── READMEinstall.sh、uninstall.sh、配置文件、systemd 服务文件全部打进这一个目录。设计原则很简单:目标机不需要联网,不需要额外依赖,拷过去就能装。images 目录留空或者放提前导出的镜像包,我会在第四节讲怎么用。
3. 一键安装脚本的实现细节拆解
3.1 install.sh:先把每一步的意图说清楚
写安装脚本之前,我心里列了五件必须处理的事:权限检查、旧环境清理、二进制部署、内核准备、systemd 集成。下面是完整脚本,我用了 set -euo pipefail,任何一个命令失败立刻退出,避免装到一半留下一个半残环境:
#!/bin/bash set -euo pipefail BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOCKER_VERSION="26.1.4" DATA_ROOT="/data/docker" # 1. 权限检查 if [ "$(id -u)" -ne 0 ]; then echo "错误:请使用 root 用户执行 install.sh" exit 1 fi # 2. 停掉可能存在的旧 Docker 进程 systemctl stop docker docker.socket containerd 2>/dev/null || true pkill -9 dockerd 2>/dev/null || true pkill -9 containerd 2>/dev/null || true # 3. 解压并拷贝二进制 if [ ! -x "$BASE_DIR/docker/dockerd" ]; then tar -xzf "$BASE_DIR/docker-${DOCKER_VERSION}.tgz" -C "$BASE_DIR" fi cp -f "$BASE_DIR"/docker/* /usr/bin/ # 4. 准备目录与用户 groupadd docker 2>/dev/null || true mkdir -p "$DATA_ROOT" /etc/docker [ -f /etc/docker/daemon.json ] || cp "$BASE_DIR/daemon.json" /etc/docker/daemon.json # 5. 安装 systemd 服务 cp -f "$BASE_DIR/systemd/docker.service" /etc/systemd/system/ cp -f "$BASE_DIR/systemd/docker.socket" /etc/systemd/system/ cp -f "$BASE_DIR/systemd/containerd.service" /etc/systemd/system/ # 6. 内核模块与网络参数 modprobe overlay 2>/dev/null || true modprobe br_netfilter 2>/dev/null || true cat > /etc/sysctl.d/docker.conf <<'EOF' net.ipv4.ip_forward = 1 net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 EOF sysctl --system >/dev/null 2>&1 || sysctl -p /etc/sysctl.d/docker.conf # 7. 启动并验证 systemctl daemon-reload systemctl enable containerd docker.socket >/dev/null 2>&1 || true systemctl enable --now docker >/dev/null 2>&1 || true systemctl start docker sleep 2 docker version echo "Docker 离线安装完成"几个我认为不能删的细节,单独拆开说。
第一,新旧环境兼容。很多机器上可能曾经装过旧版 Docker,或者装到一半失败过。所以第 2 步先调用 systemctl stop,失败也不报错,再用 pkill 兜底把残留的 dockerd、containerd 干掉。这里用 -9 是故意的,安装阶段任何“优雅退出”都可能卡住,不如直接清场,前提是你确认这台机器上没有正在跑的重要容器。
第二,二进制拷贝路径。有人喜欢放 /usr/local/bin,但官方 systemd 服务文件和不少安全策略默认找 /usr/bin/dockerd,所以我统一装到 /usr/bin,和发行版包管理安装的位置保持一致,少踩一类因 PATH 不一致引发的玄学问题。
第三,内核网络参数。Docker 的网络模式依赖 net.ipv4.ip_forward 和 bridge-nf-call-iptables。很多系统默认 ip_forward=0,不写进去的话,容器起来以后只能看着容器 ping 不通外网干着急。第 6 步里先用 modprobe 加载 br_netfilter 再写 sysctl,顺序别反。
3.2 为什么必须自带 systemd 服务文件
官方静态包里只有二进制,没有 systemd 服务文件,这是它和发行版安装包最大的差别。所以我们在离线包里自带三个文件:docker.service、docker.socket、containerd.service。核心内容这样写:
[Unit] Description=Docker Application Container Engine After=network-online.target firewalld.service containerd.service Wants=network-online.target Requires=docker.socket [Service] Type=notify ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock ExecReload=/bin/kill -s HUP $MAINPID LimitNOFILE=infinity LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity TimeoutStartSec=0 Delegate=yes KillMode=process Restart=always RestartSec=2 [Install] WantedBy=multi-user.target[Unit] Description=Docker Socket for the API [Socket] ListenStream=/var/run/docker.sock SocketMode=0660 SocketUser=root SocketGroup=docker [Install] WantedBy=sockets.target说几个容易被忽略的“为什么”。Type=notify 表示 dockerd 启动完成会主动通知 systemd,这样 systemctl 不会在服务还没准备好时就返回成功;LimitNOFILE=infinity 很关键,高并发场景下容器会打开大量文件描述符,默认限制根本不够用;docker.socket 启用 socket 激活,Docker API 监听文件可以在 dockerd 完全起来之前就创建好,配合 ExecStart 里的 -H fd:// 使用是官方推荐组合;SocketGroup=docker 配合 install.sh 里创建的 docker 用户组,决定了普通用户能不能免 sudo 执行 docker 命令。
containerd.service 的原理类似,这里不贴全文了,注意它的 ExecStart 只指定 /usr/bin/containerd,并且同样使用 Type=notify、Delegate=yes。如果你从别的渠道抄到不带 socket 激活的旧版服务文件也别慌,关键是 ExecStart 里的 dockerd 参数、容器运行时 socket 路径要和 containerd 一致,否则启动 docker 时会报 “failed to dial gRPC: connection refused” 这类错。
3.3 daemon.json 里的生产环境参数
/etc/docker/daemon.json 是 Docker 守护进程的主配置,我给的默认文件如下。首次安装时 install.sh 会判断文件不存在才拷贝,不会覆盖运维后续手动调整过的配置:
{ "data-root": "/data/docker", "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" }, "storage-driver": "overlay2", "iptables": true, "ip-forward": true, "live-restore": true }逐条解释一下为什么这么配。data-root 把 Docker 的数据目录从默认的 /var/lib/docker 挪到 /data/docker,这是给系统盘减压的常用做法,很多机器根分区只有 50G,镜像一多就报警;exec-opts 里指定 cgroupdriver=systemd,是为了将来如果要接 Kubernetes,kubelet 和 Docker 的 cgroup 驱动保持一致;json-file 日志加上 max-size 限制,防止某个容器疯狂打日志把磁盘塞满;storage-driver 明确用 overlay2,这是目前 Linux 上最常用的联合文件系统驱动;live-restore 开启后,升级或重启 dockerd 时已运行的容器不会被停掉,生产环境里这个参数能避免很多“误伤”。
3.4 卸载脚本与重复执行兼容
有装就有卸,uninstall.sh 我同样放在离线包里,内容不复杂但有几处细节要盯住:
#!/bin/bash set -euo pipefail # 卸载 Docker 服务,数据目录默认保留 systemctl stop docker docker.socket containerd 2>/dev/null || true rm -f /etc/systemd/system/docker.service rm -f /etc/systemd/system/docker.socket rm -f /etc/systemd/system/containerd.service rm -f /var/run/docker.sock rm -f /usr/bin/docker /usr/bin/dockerd /usr/bin/containerd rm -f /usr/bin/containerd-shim-runc-v2 /usr/bin/ctr rm -f /usr/bin/runc /usr/bin/docker-init /usr/bin/docker-proxy rm -rf /etc/docker /etc/sysctl.d/docker.conf systemctl daemon-reload echo "Docker 已卸载,/data/docker 下的镜像数据尚未删除"我特意不在卸载脚本里删 /data/docker。镜像、容器数据是重资产,卸载引擎不等于要数据陪葬,真要清盘就手动 rm -rf /data/docker,让操作者自己确认。install.sh 重跑一遍也不会有问题,核心原因在于:cp -f 覆盖二进制,服务文件重新落盘,daemon.json 只在不存在时写入。这套幂等设计保证“装错了重来”的成本很低。
4. 目标机实操:离线安装、镜像搬运与基础使用
4.1 从拷包到验证的全流程
安装包做好后,把它传到目标机器。内网割裂的环境经常连 scp 通道都不给,U 盘、移动硬盘是常见介质,所以离线包目录要尽量小巧,这也是当初选静态二进制而不是全套软件源的原因之一。到达目标机后按下面的顺序操作:
mkdir -p /opt/docker-offline # 把 install.sh、docker-26.1.4.tgz、systemd/、daemon.json 等全部拷入 /opt/docker-offline cd /opt/docker-offline bash install.sh安装脚本执行完,用三个命令做基础验证:
docker version docker info | grep -E "Server Version|Storage Driver|Docker Root Dir" systemctl status docker --no-pagerdocker version 能看到客户端和服务端两部分版本,如果只显示 Client 不显示 Server,说明守护进程还没起来,优先看 systemctl status docker 的报错。docker info 里我习惯重点看三行:Server Version 确认版本号、Storage Driver 确认是 overlay2、Docker Root Dir 确认数据目录已经切到了 /data/docker。
这一套走完,Docker 本体就算装好了。但注意,此时机器上还没有任何镜像,直接 docker pull 会失败。离线环境的镜像搬运是另一步,我单独说。
4.2 离线环境里镜像怎么带进去
镜像搬运有两种姿势,按交付规模选。
一次性交付、机器数量少的时候,用 docker save 和 docker load。在制包机上:
docker pull mysql:8.0 docker save -o mysql-8.0.tar mysql:8.0导出的 tar 拷到目标机后:
docker load -i mysql-8.0.tar多个镜像可以一次打包:docker save 后面跟多个名字,一个 tar 全带走。大镜像要注意单个文件体积,U 盘如果是 FAT32 格式,单文件超过 4G 会直接报错,可以用 split 拆包:
split -b 3G -d mysql-8.0.tar mysql-8.0.tar.part # 目标机上合并 cat mysql-8.0.tar.part* > mysql-8.0.tar docker load -i mysql-8.0.tar机器超过十台的时候,save/load 这种方式就太累了。更推荐在制包机之外再准备一台内网仓库机:先把 registry:2 这种仓库镜像本身也 save 出来,load 进仓库机跑起来,然后所有目标机的 daemon.json 里配置:
{ "insecure-registries": ["harbor.intra:80"] }之后各节点正常 docker pull 内网地址的镜像,速度和可靠性都远胜于在线慢速拉取,也彻底绕开了“镜像下载慢”这个常见痛点。离线安装只是第一步,镜像仓库才是内网长期运行的底座,这笔投入值。
顺带提一句 compose。需要编排的时候,在制包机上下载对应版本的 docker-compose 二进制,拷到目标机的 /usr/local/lib/docker/cli-plugins/docker-compose,chmod +x 之后就能用 docker compose 子命令了,这个过程同样不需要联网。
4.3 装完顺手开个 MySQL 8 验证一把
假设你已经在镜像包里存了 mysql:8.0,装完 Docker 后最直接的冒烟测试就是起一个 MySQL。我最常写的是下面的命令:
mkdir -p /data/mysql docker run -d --name mysql8 \ --restart=always \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD='OfFl!ne@2024' \ -e TZ=Asia/Shanghai \ -v /data/mysql:/var/lib/mysql \ mysql:8.0容器起来后,docker ps 看状态,docker logs mysql8 看初始化日志,最后在另一台机器用 mysql 客户端连一下 3306 端口。这里有个高频翻车点,很多人只给端口映射不给 MYSQL_ROOT_PASSWORD,MySQL 官方镜像直接拒绝初始化,日志里报 “database is uninitialized and password option is not specified”,翻译过来就是“你想初始化数据库,却不给密码,我不干”。执行命令前把环境变量写全,比事后查日志省心得多。
另外,-v /data/mysql:/var/lib/mysql 这步别偷懒。容器删了、机器重启了,数据还留在宿主机上,这是数据库类容器最基本的保命配置。
5. 现场问题排查与避坑实录
5.1 systemd 不存在的时候怎么兜底
部分精简系统或老版本机器根本不带 systemd,install.sh 跑到 systemctl 就报 command not found。先确认是不是 PATH 问题:which systemctl 找不到就该换方案。我的做法是用 nohup 直接拉起守护进程,并把手动启动写进 /etc/rc.local:
nohup /usr/bin/containerd >/var/log/containerd.log 2>&1 & nohup /usr/bin/dockerd --containerd=/run/containerd/containerd.sock >/var/log/dockerd.log 2>&1 &这种方式拿不到 Type=notify 的优雅通知,但作为兜底能跑。能装 systemd 的环境我还是建议装 systemd,毕竟服务托管、开机自启、日志归集都靠它。
5.2 容器网络起不来的三板斧
装是装成功了,容器也创建了,但容器里 ping 不通外网。这种问题我遇到得最多,原因高度集中在三处。第一看转发开关,cat /proc/sys/net/ipv4/ip_forward 输出 0,说明 install.sh 第 6 步的 sysctl 没生效,手动执行 sysctl -p /etc/sysctl.d/docker.conf;第二看内核模块,lsmod | grep br_netfilter 没有输出就 modprobe br_netfilter;第三看防火墙策略,firewalld 或 ufw 有可能在 Docker 建好 NAT 规则之后把链清掉,最直接的验证是 iptables -L -n -t nat 看看有没有 DOCKER 链。
内网环境里如果业务之间用的是扁平网络,实在不想被防火墙干扰,可以关掉 firewalld,但代价是宿主机暴露面变大。我的习惯是优先保留 Docker 的 iptables 管理,只在确认网络模型不需要端口映射的极简场景里把 iptables 关掉,别为了省事把后门敞开。
5.3 overlay 存储驱动与老内核的兼容
启动 dockerd 时报 “error creating overlay mount to /var/lib/docker/overlay2: invalid argument”,十有八九是文件系统对 overlay 支持不够。老内核或者 XFS 在格式化时没带 ftype=1 参数的机器,Docker 无法在 overlay2 上正常工作。排查顺序:mount | grep ' /data ' 看挂载类型,如果是 xfs,确认格式化参数;如果机器里数据不多,换成 ext4 分区是最省心的解法;内核版本过旧实在没法换的,daemon.json 里把 storage-driver 改成 vfs 能跑起来,但镜像占用空间巨大,只建议临时用。
5.4 socket 权限与 Desktop 虚拟化问题
连接 Docker 报 permission denied 是新手高频问题。原因是你当前用户不在 docker 组。执行 usermod -aG docker 用户名,然后重新登录,再执行 docker ps 就不会被拒了。注意这个操作只对加入组之后的新会话生效,开了一个终端不退出直接试是没用的。
顺带把 Docker Desktop 的问题也归拢到这里。Windows 或 macOS 上装 Docker Desktop,启动时报 “Docker Desktop failed to start because virtualisation support wasn't detected”,这是宿主机虚拟化没开或者 Hyper-V/WSL2 组件冲突。BIOS 里把 Intel VT-x 或 AMD-V 打开,Windows 侧确认“虚拟机监控程序平台”和 WSL2 相关功能已经启用,基本能解决。这和本文的服务器离线安装不是一条线,但很多同事把两者混为一谈,我在这统一提一句。
5.5 常见问题速查表
| 现象 | 常见原因 | 处理建议 |
|---|---|---|
| docker version 只显示 Client | 守护进程未启动 | systemctl status docker 看日志,必要时开 debug 日志 |
| 报错 iptables failed | 系统是 nftables 后端 | 安装 iptables-nft 兼容层,或关闭 firewalld |
| 容器 ping 不通外网 | ip_forward=0 或 br_netfilter 未加载 | 执行 sysctl -p 与 modprobe br_netfilter |
| Docker 目录空间不足 | 镜像累积撑满根分区 | 正确配置>
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设
2026/10/3 9:37:11
OpenShell 可编程命令行外壳框架:策略引擎与命令管控实战1. OpenShell 是什么,为什么值得你花时间了解第一次听到 OpenShell 这个名字,很多人会下意识以为它又是一个“终端美化工具”或者“换皮命令行”。我最初也是这么想的,直到真正把它拉下来跑了一遍,才发现这东西的定位比想象中要硬…
网站建设
2026/10/3 9:37:11
OpenShell 命令编排框架:命令单元与自动化流水线实践1. OpenShell 是什么:从命名到定位的完整拆解第一次看到 OpenShell 这个名字,我脑子里蹦出来的第一反应是"又一个终端工具"。毕竟"Shell"这个词在技术圈太深入人心了,几乎所有人第一反应都会往命令行解释器上靠。但真正上…
网站建设
2026/10/3 9:36:22
Husky 与 lint-staged 实战:从 Git Hooks 到高效提交规范在不少前端团队待过,我发现真正决定代码质量与提交效率的分水岭,往往不在代码评审,而在 git commit 之前那一下。Husky 负责把 Git Hooks 变成团队共享的工程规范,lint-staged 则把 lint 和格式化限定在暂存区文件上,保…
网站建设
2026/10/3 9:34:34
PostgreSQL 16 安装 pgvector 全指南:从编译到 HNSW 索引调优从 RAG 应用落地到向量检索,pgvector 几乎是我见过的最省心的方案。它把向量能力直接塞进 PostgreSQL,不需要额外引入 Elasticsearch、Milvus 或 Redis 向量模块,一套数据库同时管业务数据和 embedding,事务、备份、权限全部复用原…
网站建设
2026/10/3 9:34:33
IDEA正常打包成exe就OOM?JVM参数配置与传递详解有段时间我一直在处理一个让人挠头的问题:项目在IDEA里点运行,一切正常,数据跑得飞快;一旦用Maven打成可执行jar再包装成exe发给测试同事,运行不到十分钟,控制台就冒出Exception in thread "main"…
网站建设
2026/10/3 9:34:32
SQLite撑起中小型社交项目:从Schema设计到并发与迁移实践做了几年的中小型社交类项目,我发现一个有意思的现象:很多团队一提到社交网络,立刻默认要上 MySQL 或者 PostgreSQL,再不济也得是 MongoDB。但实际做下来,对于早期项目、内部工具、垂直社群类应用,SQLite 反… |