简介:ZLMediaKit(zlm)的 Docker 离线安装资源,面向需要在无外网环境部署流媒体服务的技术人员,适合机房、内网服务器及离线交付场景,也适用于需要掌握私有化部署的运维工程师、开发者和项目交付人员。该方案解决了内网无法通过 docker pull 拉取镜像的痛点,无需额外配置本地镜像仓库即可完成部署。打包好的 gz 压缩包共包含 2 个文件,一个 tar 格式的 Docker 镜像文件用于本地导入,一个 sh 格式的安装脚本用于加载镜像并启动容器,整体体积约 208.73MB,目录结构清晰,下载解压后按脚本执行即可。对具备基础 Docker 概念的读者来说,这套资源能省去手工导入镜像与启动命令的时间,降低出错概率,让 ZLM 推拉流服务在离线环境下快速落地。目前已有 375 人浏览学习,是内网部署流媒体服务时可以直接参考的实用工具包。
1. zlm docker 离线安装:从联网机到内网机的一次镜像搬运
接到 zlm docker 离线安装 的需求,通常意味着现场机器在内网,甚至整个机房没有外网出口。ZLM(ZLMediaKit)是这轮项目里的流媒体服务,负责把摄像头 RTSP 源转成 RTMP、HTTP-FLV 或 WebRTC 供网页端播放。如果直接往生产机丢编译好的二进制,运行库版本、配置目录、自启动脚本都得自己打理一遍;用 Docker 容器把这堆东西包进去是更省事的做法,但镜像本身又变成了需要“运输”的制品。这篇文章按一次真实交付的顺序:先用 rpm/deb 包把 Docker 引擎装进离线机,再在联网机 pull 并 save 出 ZLM 镜像,拿到内网 load 后一把拉起容器,最后列出最容易翻车的几个现场。适合需要自己搞定服务上线的运维、音视频实施工程师,也适合 C++ 后端想少踩环境坑的开发者。
2. 离线装 Docker 引擎:ZLM 容器需要一套干净的运行时
离线部署不是只把 ZLM 镜像拷过去就完事。镜像要能跑起来,宿主机上必须有一个健康的 Docker 守护进程。很多内网服务器是 CentOS 7 或 Ubuntu 20.04,两者的离线安装方式完全不同,而且 Docker 版本直接影响后面 ZLM 容器是否稳定,我习惯先把这一步走踏实再碰镜像。
2.1 先定版本:CentOS 7 装 20.10.24 的组合为什么稳
Docker 24 以上对内核和 iptables 的默认行为改了不少,在 CentOS 7 这种 3.10 老内核上容易出现容器网络初始化失败。ZLM 是用户态进程,对宿主机内核依赖不大,但容器运行时 runc 和网络插件对内核有底线要求。CentOS 7 上我一般选 docker-ce 19.03 或 20.10 系列,其中 20.10.24 是 20.10 里修复比较充分的版本,也是内网环境里被验证最多的组合。Ubuntu 20.04 内核 5.4,选择面宽一些,20.10 或 24.0 都能跑,但考虑到离线包要连带 containerd 一起搬运,我仍优先 20.10。
| 目标系统 | 推荐 docker 版本 | 离线包格式 | 主要注意点 |
|---|---|---|---|
| CentOS 7 | docker-ce 20.10.24 | rpm | 内核 3.10,避免 docker 24+ |
| Ubuntu 20.04 | docker-ce 20.10 / 24.0 | deb | apt 缓存目录搬运 |
| 国产化 ARM 机 | 按 CPU 架构选镜像 | deb / rpm | 下面第 5 章单独讲架构坑 |
这里有个容易忽略的点:docker-ce-cli、containerd.io、docker-ce必须一起离线带上,缺哪个现场都会卡住。如果后面想用 docker compose 管理 ZLM 容器,docker-compose-plugin的离线包也要提前准备,否则内网根本装不上 compose。
2.2 CentOS 7 用 rpm 离线装 docker:先下载,再按依赖顺序安装
在一台能联网的同版本 CentOS 7 机器上,先把 rpm 包集中下载到目录。yum 的--downloadonly选项只下载不安装,是离线交付最常用的准备手段。
mkdir -p /root/docker-rpms yum install -y yum-utils yum install --downloadonly --downloaddir=/root/docker-rpms \ docker-ce-20.10.24 docker-ce-cli-20.10.24 \ containerd.io docker-compose-plugin ls -lh /root/docker-rpms这段命令的逻辑是:指定--downloadonly后,yum 只把依赖解析完并下载,不会改动系统;--downloaddir指定落地目录。docker-ce-cli-20.10.24显式写版本,是为了保证客户端和守护进程版本一致。containerd.io不写死版本时,默认会拉到仓库里和 docker-ce 20.10 配套的版本,先这样下载,到内网机器上再统一安装。
把/root/docker-rpms打包传到内网机后,解压并按顺序安装。顺序不能乱:先客户端,再容器运行时,最后是守护进程本体。
tar czf docker-rpms.tar.gz /root/docker-rpms scp docker-rpms.tar.gz root@<内网机IP>:/opt/offline/内网机上执行:
cd /opt/offline && tar xf docker-rpms.tar.gz rpm -ivh docker-rpms/docker-ce-cli-*.rpm rpm -ivh docker-rpms/containerd.io-*.rpm rpm -ivh docker-rpms/docker-ce-rootless-extras-*.rpm rpm -ivh docker-rpms/docker-ce-*.rpm systemctl enable --now docker docker version --format '{{.Server.Version}}'用通配符*是为了不纠结具体 rpm 尾缀,只要包在目录里就能匹配。rootless-extras是非 root 用户运行 docker 的辅助包,装上无副作用,不装也能跑。最后docker version能同时打印客户端和服务端版本,如果只显示 Client 不显示 Server,说明守护进程没起来,先journalctl -u docker看日志。rpm 安装时如果报依赖缺失,缺哪个就从联网机上用同样的--downloadonly把包拉下来补上,内网机上不要尝试加--nodeps绕过,后面容器网络会出怪问题。
2.3 Ubuntu 20.04 用 deb 离线装 docker:apt 缓存一刀切
Ubuntu 的离线思路更直接:在某台联网的 Ubuntu 20.04 上先把 deb 包下载到本地,然后整目录搬运。apt 的--download-only参数会跳过安装步骤,把包留在/var/cache/apt/archives。
apt-get update apt-get install -y --download-only \ docker-ce docker-ce-cli containerd.io docker-compose-plugin mkdir -p /root/docker-debs cp /var/cache/apt/archives/docker-*.deb /root/docker-debs/ ls -lh /root/docker-debs注意docker-*.deb的匹配范围,docker-scan-plugin、docker-buildx-plugin 这些也会被一起拷出来,没有坏处。把整个目录传到内网机后,执行 dpkg 安装。
cd /root/docker-debs dpkg -i docker-ce-cli-*.deb containerd.io-*.deb docker-compose-plugin-*.deb docker-ce-*.deb systemctl enable --now dockerdpkg 对依赖顺序比较敏感,这几个包一起传入后挨个安装通常一次过。如果报缺libseccomp2或iptables这类系统库,说明离线机系统本身太旧,需要单独下载对应的 deb 包补装,不要硬跳过。Ubuntu 上装完同样建议用docker version验证,并且顺手执行systemctl is-active docker确认服务状态。
装 Docker 引擎这步我没有用一键脚本,原因很简单:内网环境千差万别,自动化脚本一旦遇到缺依赖或旧版本残留,排查起来反而比手动装更耗时间。版本定死、包齐了、按顺序装,现场十分钟内就能把 Docker 引擎准备好。
3. 准备 ZLM 镜像:docker search、pull、save 三步导出离线包
Docker 引擎就绪后,回到联网机准备 ZLM 镜像。这里要解决两件事:选一个可靠的镜像源,以及把镜像完整导出成可搬运的文件。很多人在“镜像下载慢”这件事上耗掉半天,其实离线场景根本不要求 pull 多快,只拉一次、导出正确就够了。
3.1 先从“镜像下载慢”说起:docker search 里怎么筛 ZLM 镜像
在联网机器上,拉镜像慢是常态。Docker Hub 的连通性受网络环境影响很大,尤其在公司出口带宽紧张的时候,一个几百 MB 的镜像可能要反复重试。离线方案的优势就在这里:外网机器只承担一次拉取和导出,之后内网所有机器都用同一份 tar 包,不再依赖外网。
筛选镜像我习惯用 docker search 先看候选列表:
docker search zlmediakit --limit 20输出里重点看 STARS 和 OFFICIAL 两列。ZLM 的官方镜像通常以 zlmediakit 相关命名,社区维护的镜像在文档里会标注对应的配置文件路径。宁可选文档齐全的镜像,也不要图体积小选一个精简版,后面端口和配置目录对不上会非常痛苦。
3.2 docker pull 前用 manifest inspect 确认架构,别等现场才翻车
这一步最容易偷懒,但恰恰是现场翻车率最高的点。内网机器如果是飞腾、鲲鹏或 RK3588 这类 ARM 架构,而你在联网机随意 pull 了个 x86 镜像,load 过去后容器必然起不来。确认架构用 docker manifest inspect。
# 先搜索确认官方镜像名,示例里的 zlmediakit/zlmediakit 需要换成你确认到的名字 docker manifest inspect zlmediakit/zlmediakit:latest输出是一个 JSON,里面platform字段列出architecture和os。如果列表里有arm64并且你的目标机是 ARM,再执行 pull 时加上平台参数:
export ZLM_IMAGE=zlmediakit/zlmediakit:latest docker pull --platform linux/arm64 "$ZLM_IMAGE"--platform参数会让 docker 拉取指定架构的镜像层,但前提是镜像本身发布过多架构版本。如果 manifest 里只有amd64,说明这个镜像没做 ARM 版,这时候要么换一个维护者镜像,要么在 ARM 机器上用源码镜像构建。生产环境用latest标签不如固定到一个 release 版本,master是开发分支,离线交付后没人帮你盯更新,固定版本才是可控的。
3.3 docker save 导出离线包:tar、gzip、md5 一套带走
镜像拉好后,导出用 docker save,千万别用 docker export。这两者的区别很关键:save保留镜像的层结构、历史记录和 CMD/ENTRYPOINT 元数据,load回去后能直接docker run;export把容器文件系统摊平成一层,丢失镜像属性,load 回去只是一个不完整的文件系统快照。
mkdir -p /data/zlm-offline cd /data/zlm-offline docker save -o zlm.tar "$ZLM_IMAGE" gzip -k zlm.tar md5sum zlm.tar.gz > zlm.tar.gz.md5 ls -lh /data/zlm-offline-o指定输出文件名,不写-o时 docker save 会把内容打到标准输出,可以配合管道。gzip -k保留原始 tar,避免压缩失败时又要重新 save。md5 文件体积很小,但它在现场能帮你确认传输过程是否损坏,建议养成习惯。ZLM 镜像压缩后通常在几百 MB 量级,用移动硬盘或直接网络传都能接受。
3.4 送进内网:rsync、scp 与移动介质的选择
跨网传输方式取决于内网的物理隔离程度。有管理网就跑 rsync,支持断点续传,比 scp 更稳。
rsync -avP /data/zlm-offline/zlm.tar.gz root@10.20.30.40:/opt/zlm-offline/ rsync -avP /data/zlm-offline/zlm.tar.gz.md5 root@10.20.30.40:/opt/zlm-offline/-a归档模式保留文件属性,-v显示明细,-P等于--partial --progress,传输中断后再次执行会接着传。如果现场是物理隔离,用 U盘或移动硬盘拷贝时,务必把 md5 文件一起拷过去。介质拷贝存在静默损坏的可能,不校验直接 load,报错后你分不清是镜像问题还是拷贝问题。送达内网机后先执行md5sum -c zlm.tar.gz.md5,输出 OK 再继续。
4. 导入并启动 ZLM 容器:从 docker load 到 RTSP/HTTP 验证
镜像包到了内网,Docker 引擎也装好了,接下来就是把 tar 导入、把容器跑起来。这一步的关键不是“能 start”,而是容器起来后配置目录、端口映射和日志都处于可控状态,方便现场继续调整。
4.1 docker load 导入前先做完整性校验
内网机上进入离线包目录,先校验再导入。docker load 支持直接读取 gzip 压缩的 tar,不需要先解压。
cd /opt/zlm-offline md5sum -c zlm.tar.gz.md5 docker load -i zlm.tar.gz docker images | grep -i zlmmd5sum -c会读取.md5文件里的期望值,计算当前文件实际值并比对,输出OK才说明传输和拷贝过程没问题。docker load -i zlm.tar.gz导入镜像,输出里通常包含Loaded image: xxx或Loaded image ID: sha256:xxx。docker images | grep -i zlm用来确认镜像已经出现在本地仓库。
这里经常出现一个现象:镜像 ID 在,但 REPOSITORY 和 TAG 显示<none>。原因后面第 5 章详细说,解决办法是用镜像 ID 补一个本地标签:
docker tag <IMAGE_ID> zlm-local:latest export ZLM_IMAGE=zlm-local:latest补完标签后,后续所有docker run和 compose 文件都用zlm-local:latest这个稳定名称,不再依赖原始仓库地址。
4.2 用 docker inspect 摸清端口与配置目录,别靠记忆
ZLM 镜像在不同维护者手里,启动目录和配置路径不一样。与其翻文档回忆,不如直接问镜像本身。docker inspect 能给出工作目录、暴露端口和声明的挂载点。
docker inspect "$ZLM_IMAGE" \ --format '工作目录:{{.Config.WorkingDir}}' docker inspect "$ZLM_IMAGE" \ --format '暴露端口:{{json .Config.ExposedPorts}}' docker inspect "$ZLM_IMAGE" \ --format '挂载点:{{json .Config.Volumes}}'这几条命令分别输出镜像的工作目录、端口声明和卷声明。如果ExposedPorts为空,不代表没有端口,只说明镜像构建时没做显式 EXPOSE,实际的端口要看 ZLM 的 config.ini。常见 ZLM 默认端口是 RTSP 554、RTMP 1935、HTTP 80,但这个结论请用你手里的 config 去验证。挂载路径一般会显示/opt/media/config一类目录,实际以 inspect 输出为准。
4.3 docker run 最小配置:端口映射、配置挂载、开机自启
启动 ZLM 容器的命令里,配置目录挂载和日志限制是两个容易被忽略的点。先建好宿主机目录,再启动:
mkdir -p /opt/zlm/conf /opt/zlm/record docker run -d --name zlm --restart always \ -p 554:554 \ -p 1935:1935 \ -p 8080:80 \ -v /opt/zlm/conf:/opt/media/config \ -v /opt/zlm/record:/opt/media/record \ -e TZ=Asia/Shanghai \ "$ZLM_IMAGE"| 参数 | 作用 |
|---|---|
-d | 后台运行容器 |
--restart always | 宿主机重启后容器自动拉起 |
-p 宿主机端口:容器端口 | 端口映射,左边是外部访问端口 |
-v 宿主机目录:容器目录 | 配置和录像数据持久化 |
-e TZ=Asia/Shanghai | 容器内时区,影响日志时间 |
-p 8080:80表示把宿主机 8080 映射到容器内 80。容器内 HTTP 端口可能被 config.ini 改过,如果改成 8080,映射方向就是-p 8080:8080,现场以实际配置为准。第一次启动我不建议立即挂载配置目录:容器内往往自带一份默认 config.ini,直接挂载空目录会把它遮住,导致启动异常。更稳的做法是先不带-v启动一次,用docker cp zlm:/opt/media/config /opt/zlm/conf把默认配置拖出来,再重新挂载启动。
4.4 用 HTTP 接口和 ffplay 验证拉流链路
启动后先看日志,再打 HTTP 接口。ZLM 自带一套 HTTP 管理接口,默认路径通常以/index/api/开头。
docker logs --tail 50 zlm curl -s http://127.0.0.1:8080/index/api/getServerConfig | head -c 400日志里会打印各端口的监听情况,比如HttpServer listen: 0.0.0.0:80,这能帮你确认容器内端口到底是什么。curl 请求 HTTP 接口,返回一段 JSON 说明服务活着。如果这条命令卡住或拒连,先检查docker ps里容器是否在运行,再用docker logs看有没有配置文件缺失的报错。
验证外网拉流,在另一台内网机器上执行:
ffplay -rtsp_transport tcp -i rtsp://<离线机IP>/live/test/live/test是 ZLM 默认的测试流地址,流不存在时会立刻返回 404,这本身就是端口已通的证明。如果 ffplay 一直卡在连接阶段,多半是防火墙。放行端口用 firewalld:
firewall-cmd --permanent --add-port=554/tcp --add-port=1935/tcp --add-port=8080/tcp firewall-cmd --reloadRTSP 默认走 TCP 554,但 ZLM 的 RTSP 也可以配置成 UDP 模式,现场拉流失败时不要只放行 TCP,确认客户端用的 transport 是 TCP 还是 UDP。这一步做完,容器和网络基本就通了。
5. 离线部署避坑指南:镜像标签、运行库与网络三类高频翻车
离线安装本身不复杂,流程是固定的。真正消耗时间的永远是那几个隐蔽问题:标签丢失、目录被遮、架构不匹配、网络策略冲突、磁盘打满。我按现场踩坑的顺序列一遍,每条都是现象、原因、解决三段式。
5.1 load 之后镜像变成<none>:标签丢失怎么补
现象:docker load -i zlm.tar.gz成功,镜像也能看到,但 REPOSITORY 和 TAG 都是<none>:<none>,docker run没法直接用名字启动。
原因:docker save 导出时,如果镜像本身没有 tag,或者你在 save 时传入的是IMAGE ID,导出包里就只包含镜像层和 ID,不会附带仓库名和标签信息。离线机 load 后自然显示<none>。还有一种情况:pull 的时候用的是 digest 而不是 tag,结果类似。
解决:用docker tag手动补一个本地标签。先docker images查出 IMAGE ID 那一列,然后执行docker tag <IMAGE_ID> zlm-local:latest。之后 run 和 compose 都用zlm-local:latest,避免每次都要翻 ID。这件事最好在联网机 save 之前就做好,用固定的镜像名:版本save,load 出来标签往往是完整的。
5.2 容器启动秒退:配置目录被空挂载遮住的连锁反应
现象:docker run执行完,docker ps里看不到容器,docker ps -a显示容器已退出,docker logs zlm里只有开头几行甚至没内容。
原因:最常见的是挂载了一个空的宿主机目录到容器配置路径,把镜像里自带的 config.ini 或默认文件“遮住”了。ZLM 启动脚本读不到配置文件时直接退出,而不是帮你生成一个默认配置。另一个常见原因是宿主机目录权限不对,容器内进程以非 root 用户运行,没有写权限。
解决:先不挂载配置目录,只用docker run --name zlm-test "$ZLM_IMAGE"启动一次,让它用镜像内置的默认配置跑起来。然后docker cp zlm-test:/opt/media/config /opt/zlm/conf把配置目录完整拖出来。再按 4.3 的完整命令重启,挂载就不会是空目录了。权限问题用docker inspect '{{.Config.User}}'查看容器内运行用户,把宿主机目录 chown 成对应的 UID。
5.3 exec format error:ARM 机器拿了 x86 镜像
现象:容器运行立刻报错,docker logs或docker run前台输出提示exec format error。在飞腾、鲲鹏或 RK3588 平台上尤其常见。
原因:镜像架构和宿主机 CPU 架构不匹配。x86_64 的二进制文件在 ARM64 内核上无法执行,runc 创建容器进程后 exec 直接失败。杀毒软件或虚拟化平台有时候也会导致类似报错,但先怀疑架构。
解决:回到联网机上执行docker manifest inspect <镜像名>,看支持哪些平台,然后docker pull --platform linux/arm64 <镜像名>重新拉取,save 成新的 tar 包。这一步应该放在第 3 章,但如果现场已经翻车,就按这个路径重新走一遍。现场没有外网时,这批镜像没法换,只能从源头重出。所以我在 3.2 强调 manifest 检查,这不是多此一举。
5.4 外面机器拉不了流:docker 网络不通先查这三处
现象:容器在跑,curl 127.0.0.1:8080也通,但另一台机器访问rtsp://离线机IP/live/test超时或拒绝。
原因:不是 ZLM 的问题,是宿主机防火墙或端口映射的问题。第一处查 firewalld/ufw 是否放行了 554、1935、8080。第二处查docker ps里端口映射方向,-p 8080:80表示外部访问 8080 才到容器 80,外部访问 80 会失败。第三处查宿主机的默认路由和 docker0 网段是否和公司内网冲突,如果有重叠,容器网络可能路由异常。
解决:按顺序执行:firewall-cmd --permanent --add-port=554/tcp --add-port=1935/tcp --add-port=8080/tcp && firewall-cmd --reload放行端口;docker ps检查映射是否按预期;ip addr show docker0看一下 docker0 网段,如果与内网冲突,在/etc/docker/daemon.json里设置bip改掉段。改完重启 docker 后,docker run的容器名称和挂载参数会保留,但容器本身需要重新启动,有--restart always的话 docker 服务恢复后会自动拉起。
5.5 load 半路报 no space left:data-root 迁移与日志限额
现象:docker load -i zlm.tar.gz跑了一多半,突然报no space left on device,df -h显示根分区快满了。
原因:docker 的默认数据目录是/var/lib/docker,如果系统盘本身就小,镜像导入会瞬间占掉几个 GB。ZLM 的录像文件如果映射到容器内默认路径,也会不断累积在 docker 数据目录里。这是内网机器最常见的空间事故。
解决:在导入大镜像之前,先改daemon.json把>{ "data-root": "/data/docker", "log-opts": { "max-size": "10m", "max-file": "3" } }
>services: zlm: image: zlm-local:latest container_name: zlm restart: always ports: - "554:554" - "1935:1935" - "8080:80" volumes: - /opt/zlm/conf:/opt/media/config - /opt/zlm/record:/opt/media/record environment: - TZ=Asia/Shanghai
image 用 5.1 补过的本地标签zlm-local:latest,compose 文件不依赖任何外部仓库。install.sh 的内容就是校验、导入、启停三件事:
#!/bin/bash md5sum -c zlm.tar.gz.md5 && \ docker load -i zlm.tar.gz && \ docker compose up -d在离线机上执行bash install.sh,脚本跑完容器就是运行状态。这里要求装 Docker 时带上了docker-compose-plugin,如果没有,就用同等参数的docker run脚本替代,效果一致。
6.2 把 compose 当后悔药,交付前先完整走一遍
我现在的习惯是每次给别人交付前,先在本地一模一样的环境里过一遍 install.sh,从空机器到拉流成功也就十分钟。版本换过之后,不再手动敲 docker run 了,哪个版本改了什么参数记不住,compose 文件就是后悔药。现场如果出现端口冲突或者机器配置不同,第一步就是打开 compose 文件,别急着打命令,从文件里找问题比乱试要快得多。希望帮到你。
本文还有配套的精品资源,点击获取