Docker 19.03.9离线部署实战:内网环境下的安装与避坑指南

发布时间:2026/10/7 10:35:03
Docker 19.03.9离线部署实战:内网环境下的安装与避坑指南 简介面向需要在内网或无外网环境中安装 Docker 的运维工程师和开发人员这份「docker19.03.9离线部署工具」提供了完整的 Docker 19.03.9 离线安装包与启动配置。压缩包共 4 个文件包含 docker-19.03.9.tar.gz 镜像包、op.sh 部署脚本、docker.service 系统服务单元以及 env.conf.tpl 环境变量模板覆盖从导入镜像、编写服务配置到启动并验证 Docker 的核心流程。包体约 57.69MB结构精简适合服务器无法访问公网时快速完成部署。目前已有 9041 人学习或下载适用于 CentOS 7.x 等常见 Linux 发行版也可作为制作离线安装包、梳理 systemd 服务部署与排错思路的参考。拿到资源后可直接按脚本化流程操作节省手工下载和配置时间降低因版本差异带来的部署风险助力在隔离网络环境下一站式完成 Docker 环境搭建。1. 老版本 Docker 离线部署为什么我还在用 19.03.9手头有一台连内网都算不上的机器——没外网、没有 yum 源、连装个 curl 都得拿 U 盘拷这种环境里要跑容器docker 19.03.9 离线部署工具就是最省事的解法。这个工具打包好了 docker 主程序、cli、containerd 以及整套启动脚本拷进去解压执行就能把 docker 拉起来不用编译不用配源。适合的场景很具体等保环境、机房隔离网、生产内网、以及那些被安全策略锁死不让碰外网的服务器。docker 19.03.9 这个版本在 CentOS 7 / Ubuntu 16.04 上兼容性都不差虽然是 2020 年的版本但胜在稳而且这套离线包帮你把依赖关系全处理好了——少去无数个「缺 libltdl.so.7」的深夜。资源不大胜在完整下文我把部署流程、参数配置和踩过的坑一次讲清楚。2. 离线部署的技术选型docker 19.03.9 的优势与包结构2.1 为什么是 19.03.9 而不是镜像里的最新版选版本这事其实不是拍脑袋。docker 19.03.9 属于 19.03 系列的最后一个小版本而 19.03 这个版本线在 docker 的历史里地位很特别它是 containerd 独立拆分之前 docker 自己捆绑 containerd 的成熟期版本也是支持docker engine API在 1.40 协议上非常完整的一代。对离线部署来说版本越新依赖越重glibc、systemd 版本要求越苛刻而 19.03.9 对内核的要求相对宽松——CentOS 7 的内核 3.10 可以直接跑Ubuntu 18.04 的 4.15 更没压力。从工具包的角度看这套离线部署工具把 docker 的二进制拆成了几个核心部分组件作用版本对应关系docker主程序负责 CLI 与服务端19.03.9dockerd守护进程入口与主程序同版本containerd容器运行时管理1.2.x配套版本docker-init容器内 PID 1 初始化内置docker-proxy端口映射辅助内置你可能会问为什么要拆这么清楚因为离线部署的难点从来不是拷贝文件而是依赖管理和启动顺序。docker 的二进制虽然是静态编译的但 containerd、iptables、cgroups 这些底层组件的版本搭配是有讲究的。这套工具把对应版本绑定好了你不需要自己去找「docker 19.03.9 该配哪个 containerd」它配的就是官方同期的 1.2.13。2.2 离线包的目录结构与启动链解压工具包之后典型的目录结构是下面这样docker-offline/ ├── bin/ │ ├── docker │ ├── dockerd │ ├── containerd │ ├── containerd-shim │ ├── ctr │ ├── docker-init │ └── docker-proxy ├── service/ │ └── docker.service ├── install.sh └── uninstall.sh这里面的docker.service是 systemd 的 unit 文件它定义的是 dockerd 怎么被系统拉起以及启动时带哪些参数。install.sh 脚本干的事核心就是把 bin 下的二进制拷到/usr/bin/再把 service 文件放到/etc/systemd/system/然后执行systemctl daemon-reload和systemctl enable docker。部署时直接执行chmod x install.sh ./install.sh脚本内部核心逻辑其实就三步# 拷贝主程序到系统路径 cp -p bin/docker /usr/bin/docker cp -p bin/dockerd /usr/bin/dockerd cp -p bin/containerd /usr/bin/containerd # 安装 systemd 服务配置 cp service/docker.service /etc/systemd/system/docker.service # 重新加载并启用服务 systemctl daemon-reload systemctl enable docker systemctl start docker逻辑说明三步走是按「可执行文件 → 服务配置 → 系统级启用」的标准操作顺序来设计的。-p参数保留了二进制文件的权限和时间戳这在实际排查问题时很有用——比如你可以通过ls -l /usr/bin/dockerd看二进制时间来判断它是不是来自这套包。systemctl enable保证了重启后 docker 会自动拉起这是生产环境的基本要求。参数说明如果你的机器不是 CentOS 7 而是 Ubuntu 18.04systemctl一样可用如果是 SysVinit 的旧系统比如 CentOS 6这套脚本就不适用了需要手动写/etc/init.d/docker脚本但 19.03.9 官方对 CentOS 6 已经不做支持劝你放弃。2.3 校验安装是否真的成功装完不等于能用需要确认几个关键点。先看版本号对不对docker --version docker version第一个命令输出Docker version 19.03.9, build 9d988398是正常的第二个命令会分别显示 Client 和 Server 两段信息——注意看好 Server 段的Version是不是也显示 19.03.9。如果 Client 有版本而 Server 报Cannot connect to the Docker daemon说明 dockerd 没起来多半是 service 文件有问题或者 cgroup 驱动不匹配。这我在后面避坑章节会详细讲。再验证服务状态systemctl status docker正常会看到Active: active (running)而且Main PID那一行有具体的进程号。用ps -ef | grep dockerd可以直接看到 dockerd 进程带的完整参数这个参数就会告诉你它用的是overlay2还是devicemapper、iptables有没有开。到这里你手里的离线工具包已经能拉起一个可用的 docker 环境。但你离「生产可用的编排」还差最后一步——daemon 参数调优和镜像管理这是下一章的内容。3. 部署后的关键配置daemon.json、存储驱动与镜像导入3.1 配置私有仓库与数据目录把 docker 拉到能跑只是第一步内网机器不可能每次docker pull都从外网拉——图省事的办法是在工具包旁边放一个自己导出的镜像 tar 包这叫「镜像离线导入」讲究一点的做法是搭个内网 registry然后把/etc/docker/daemon.json里的insecure-registries指向内网仓库地址。我一般安装完 docker 后第一件事就是写 daemon.jsonmkdir -p /etc/docker cat /etc/docker/daemon.json EOF { data-root: /data/docker, storage-driver: overlay2, exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 }, insecure-registries: [registry.inner.local:5000], registry-mirrors: [] } EOF逻辑说明>mkdir -p /data/docker如果目录不存在dockerd 启动时会自动创建但权限和挂载点可能不是你预期的建议先手动建好。改完 daemon.json 必须重启才生效systemctl daemon-reload systemctl restart docker注意顺序不能反。先 daemon-reload 重新加载 systemd 配置再 restart docker。如果你手滑先 restart可能会出现「docker 起来以后参数还是旧的」这种玄学问题。3.2 离线导入镜像从 tar 到可运行容器配置完之后你手里的镜像 tar 包怎么导入不能docker pull只能docker loaddocker load -i images/nginx-1.19.0.tar docker load -i images/redis-5.0.10.tar执行完可以用docker images确认镜像是否加载成功。注意看输出里的REPOSITORY和TAG两列——如果镜像名是nginx而 tag 是none说明打包的时候用了docker save但没打 tag导入后就是悬空镜像。解决方法是手动打 tagdocker tag IMAGE_ID nginx:1.19.0逻辑说明docker load做的事是把你机器上docker save出来的 tar 流读进本地镜像层它不做任何网络请求所以离线环境完全可用docker save和docker load是对应的一个导出一个导入中间走的是 tar 文件。docker tag则是给镜像刷一个可读的名字不然每次docker run都得记一长串镜像 ID——当然你可以用 ID 直接跑但生产维护角度这是给自己挖坑。参数说明-i指定输入 tar 文件。如果镜像文件是 gzip 压缩过的image.tar.gzdocker load也能直接识别不需要手动解压。如果你想看这个 tar 里到底有哪些镜像tar -xf nginx-1.19.0.tar manifest.json cat manifest.jsonmanifest.json里会列出所有镜像的RepoTags和Layers能让你在导入之前就确认包里有没有你想要的那个镜像 tag。3.3 用 docker-compose 编排多容器工具包里如果带了 docker-compose 可执行文件你可以把它丢到/usr/local/bin/docker-compose并加执行权限。19.03.9 时代对应的 docker-compose 版本是 1.25.x 附近——别用 2.x 的 compose语法和兼容性跟 19.03 的 API 有微小差异尤其涉及docker-compose up -d的服务编排时1.25 更省心。我一般会准备一个 compose 文件来一次性拉起内网基础服务version: 3 services: mysql: image: mysql:8.0.20 container_name: mysql8 restart: always environment: MYSQL_ROOT_PASSWORD: Admin123 volumes: - /data/mysql:/var/lib/mysql ports: - 3306:3306 redis: image: redis:5.0.10 container_name: redis5 restart: always volumes: - /data/redis:/data ports: - 6379:6379注意mysql:8.0.20和redis:5.0.10必须是已经在本地docker images里存在的否则 compose 会尝试去仓库拉取——离线环境会卡在 pull 阶段直到超时。所以每次写 compose 文件之前先docker images确认一遍镜像存在这是我的习惯。启动编排docker-compose up -dup -d是后台模式-d保证不占用终端。第一次启动拉起的容器会打印日志可以通过docker-compose logs -f跟踪。这里面的关键点是restart: always——容器挂了或者机器重启后会自动拉起这对内网无人值守服务极其重要。3.4 容器网络模式的选择离线环境最容易忽略的是容器网络。19.03.9 默认创建bridge网络容器之间可以通过容器名互相访问但主机访问容器要靠端口映射。如果你有两个容器要互相通信比如 nginx 反代后端 Java 服务建议直接把他们放在同一个自定义网络中docker network create backend-net docker network connect backend-net nginx docker network connect backend-net java-app逻辑说明默认的 bridge 网络不提供 DNS 解析容器之间只能靠 IP 访问而容器重启后 IP 会变——这是个经典坑。自定义 network 有内置 DNSnginx 配置里写proxy_pass http://java-app:8080;就能解析到目标容器省去手工改 IP 的苦力。参数说明docker network create后面的backend-net是网络名你可以随意起connect是把已存在的容器挂到网络上不用重启容器。如果你用的是 docker-composecompose 文件里networks字段自动创建一个项目级网络同样提供 DNS 解析不需要手动 connect。到这里为止你的 docker 环境已经能正常跑容器、编排服务了。但真正的生产环境永远会在某个深夜给你来一记闷棍——下一章专门讲我在这套离线部署上踩过的坑。4. 避坑指南离线部署 docker 最常见的五个故障4.1 dockerd 起不来failed to start docker application container engine现象执行systemctl start docker后systemctl status docker显示failed to start docker application container engine.journalctl 里能看到 dockerd 启动报错。原因这条报错太泛了真实原因一般藏在日志后几行。我遇到最多的是两种情况一是/etc/docker/daemon.json写坏了JSON 语法错误导致 dockerd 直接退出二是内核版本太老overlay2 模块没加载。解决先看 daemon.json 是否合法python -m json.tool /etc/docker/daemon.json如果输出报错说Expecting property name enclosed in double quotes那就是 JSON 格式错——常见的是键名写成了单引号。修正后重启。如果 JSON 没问题检查 overlay 模块lsmod | grep overlay modprobe overlay没有输出就手动modprobe overlay如果是 CentOS 7 精简安装可能连内核模块都没装全——那就得换一个带完整内核模块的机器或者是用工具的--force参数换成vfs存储驱动。4.2 拉镜像或连容器时报 permission denied现象执行docker ps报permission denied while trying to connect to the docker daemon socket at unix:///var/run/docker.sock普通用户必现root 用户正常。原因docker.sock 的属组是root用户不在 docker 组里。docker 官方习惯把docker组作为授权入口但离线部署工具装的 docker 不一定帮你创建这个组。解决groupadd docker usermod -aG docker $USER newgrp docker关键一步是newgrp docker——它刷新当前 shell 的组权限。如果你不执行这步必然要重新登录才生效。执行完再看docker ps就正常了。注意有些环境是 sudo 权限不够sudo docker ps反而能跑——这种情况说明用户根本不在 docker 组解决方式同上。4.3 容器网络不通容器之间 ping 不通现象两个容器在同一台机器上docker exec进容器 A ping 容器 B 的 IP不通或者从宿主机访问容器内端口也连不上。原因最常见的是宿主机开了 firewalldiptables 规则把 FORWARD 链默认 DROP 了。docker 的 bridge 网络依赖 iptables 做 NAT 和转发firewalld 的策略会跟 docker 的 iptables 规则发生冲突。解决先看 FORWARD 链策略iptables -L FORWARD -n | head -20如果看到policy DROP那就把默认策略改掉iptables -P FORWARD ACCEPT但这只对当前生效重启机器就没了。持久化做法是写一个 systemd 服务或者在/etc/sysctl.conf加net.ipv4.ip_forward 1同时关掉 firewalld 或者把 docker 的网段加入白名单。我个人习惯是直接systemctl disable firewalld——内网环境里 firewalld 的价值本来就不大而它跟 docker 的 iptables 之争是头号网络故障源。另一种情况是容器内 DNS 配的是宿主机地址导致容器之间用名字访问失败——解决方案已经在 3.4 讲过自定义网络会覆盖这个配置。4.4 docker 启动超时timeout 但 status 还是 active现象systemctl start docker命令卡住十几秒然后报 timeout但systemctl status docker又显示 active。原因dockerd 启动时如果遇到存储驱动加载慢或者 daemon.json 配置的>systemctl show docker -p TimeoutStartUSEC默认是 90 秒。如果确认是启动慢可以在 service 文件里加TimeoutStartSec180然后 daemon-reload 重启。但治标不治本——真正的病根是存储路径不对别偷懒换路径。4.5 磁盘被日志撑爆container 日志无限增长现象docker logs能看到几天的日志但df -h显示/var/lib/docker/containers所在分区 100% 满了。原因默认log-driver是 json-file 且没有 max-size 限制容器滚动打印日志会无限增长。这个问题在离线环境里更致命——你没有一个日志采集系统帮你转储日志全落在本地。解决靠 3.1 节里的log-opts配置max-size: 100m和max-file: 3会滚动写日志文件超过就自动轮转。但对已存在的容器修改 daemon.json 重启后不会立即清理旧日志你需要手动清truncate -s 0 /var/lib/docker/containers/*/*-json.log执行这个的时候 docker 正在运行直接截断文件是安全的不需要停容器。如果你连log-driver都不想让 docker 管可以换成local驱动但那样docker logs就看不见历史日志了各有取舍——我建议 json-file 滚动就够。避坑章节就写这五条。前三条是安装部署期大概率遇到的后两条是运行期常见的照着排查基本都能活过来。下一章讲部署好之后的验证流程和进阶操作这样你交付给别人的时候不至于心里没底。5. 部署后的验证与进阶从「能跑」到「交付给别人不打脸」5.1 五分钟环境自检脚本把 docker 部署完不算完你得能证明它「是好的」。我每次给客户交付这套离线工具之前都会跑一遍自检脚本把关键项目全部测一遍#!/bin/bash # quick-docker-check.sh echo 版本检查 docker --version echo 服务状态 systemctl is-active docker echo 存储驱动 docker info --format {{.Driver}} {{.DockerRootDir}} echo 网络连通 docker run --rm alpine:3.10 ping -c 1 8.8.8.8 21 || echo PING 不通离线正常 echo 容器启动 docker run --rm -d --name check-nginx nginx:1.19.0 sleep 2 curl -s http://127.0.0.1:80 /dev/null echo HTTP OK || echo HTTP 失败 docker rm -f check-nginx逻辑说明docker info --format用 Go template 语法直接抽出驱动和数据目录省得十行输出里自己找curl -s http://127.0.0.1:80是为了覆盖端口映射链路——能证明容器运行、端口绑定、iptables 转发这整条通路是通的。alpine镜像如果本地没有这行会报No such image实际作用就变成「验证本地镜像库是否有该镜像」。参数说明alpine:3.10和nginx:1.19.0这两个镜像在 19.03.9 时代都很通用。如果你本地没这两个镜像换成你真正部署用的镜像即可。另外自检脚本里如果有容器映射了 80 端口注意先确认宿主机 80 端口没被占用。5.2 内网镜像仓库的搭建与推送如果你管理的机器不止一台那就需要在其中一台做 registry其他机器通过insecure-registries指向它拉镜像。在已经装好 docker 的机器上一条命令起 registrydocker run -d --name registry \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ --restartalways \ registry:2然后把你手里那个nginx-1.19.0.tar导入、打 tag、推送docker load -i nginx-1.19.0.tar docker tag nginx:1.19.0 registry.inner.local:5000/nginx:1.19.0 docker push registry.inner.local:5000/nginx:1.19.0逻辑说明registry:2是 docker 官方的镜像仓库实现纯 Go 写的内网部署没有版权问题。推送成功后其他机器只要在 daemon.json 里写了insecure-registries: [registry.inner.local:5000]就可以直接docker pull registry.inner.local:5000/nginx:1.19.0——这一步打通之后整个内网集群都有了统一的镜像分发入口不再需要每台机器手动 load tar 包。参数说明-p 5000:5000是 registry 默认端口你改成 8443 之类也可以但要同步改所有客户机的 insecure-registries。/data/registry是持久化目录收好如果这台机器挂了镜像数据全在这里面。这里有个细节registry 容器本身要先把registry:2镜像 load 进来才能跑。所以离线部署最典型的路径是第一台机器手动 load 基础镜像和 registry 镜像 → 起 registry → 再 load 业务镜像 → push 上去 → 后续机器从 registry pull。5.3 内核参数与 limit 调优19.03.9 对文件描述符、线程数等默认值放得比较宽但生产内网的高并发容器场景还是要主动调一波。这部分不是工具包带的功能但属于「部署完必须顺手做的事」写在 /etc/sysctl.d/99-docker.confnet.ipv4.ip_forward 1 net.bridge.bridge-nf-call-iptables 1 vm.max_map_count 262144 fs.file-max 1000000生效方式sysctl -p /etc/sysctl.d/99-docker.conf逻辑说明net.bridge.bridge-nf-call-iptables必须为 1否则容器跨宿主机通信时防火墙规则会漏掉 bridge 流量vm.max_map_count在跑 Elasticsearch 一类容器时尤其关键默认值 65530 会被 ES 直接拒绝启动fs.file-max提升到百万级是为了扛大量容器并发时的文件句柄压力。参数说明这些值不是拍脑袋定的vm.max_map_count是 ES 官方文档明确要求262144fs.file-max按 64GB 内存的机器设 1000000 够用了更大内存可以等比上调。另外记得同时改 ulimit——在 daemon.json 里加default-ulimits: {nofile: {Name: nofile, Hard: 65535, Soft: 65535}}否则容器里进程的文件句柄限制还是默认 1024。5.4 交付前的最终验证清单我现在每交付一套离线 docker 环境都会强制走一遍下面的清单不通过就不签字检查项期望结果失败动作docker versionServer 段版本19.03.9检查 containerd 版本docker info存储驱动overlay2检查内核模块systemctl is-enabled dockerenabled执行 enable否则重启丢服务curl 127.0.0.1:映射端口返回业务页面检查 iptables 与端口占用重启一次机器再检查 docker 状态active (running)查 systemd 依赖、data-root 是否存在docker ps -a容器状态全部 Up 或 Exited(0)有 Exited(非0) 的要看日志这套清单是我从一次惨痛教训里总结出来的有次我把环境装好业务方说没问题就签字了结果第二天机器因机房断电重启docker 起是起来了但里面的 MySQL 容器没跟着起来——原因是当时 compose 文件里没写restart: always。从那以后我每次交付时最后一项一定是「重启一次再回来查」不经历一次断电模拟就不敢说交付完成。这个工具包本身解决的是「把 docker 装上去」这个阶段的问题但从我的经验看真正决定项目成败的往往是部署完之后你针对内网环境做了多少适配——目录迁移、日志轮转、镜像分发、自检脚本这些配套动作一个都不能少。希望这份笔记帮你在内网里少走几个弯路。本文还有配套的精品资源点击获取