Docker离线安装ZLM:内网流媒体服务部署与避坑指南
简介ZLMediaKitzlm是常用的高性能流媒体服务器支持 RTSP、RTMP、HLS、HTTP-FLV 等协议常见于视频监控、直播分发和网关接入场景。面向需要在内网、无外网或专网隔离环境完成部署的运维与研发人员这份离线 Docker 安装资源包将镜像归档和安装脚本整合在一起省去在线拉取镜像、源码编译和依赖配置等容易出错的环节。资源包共 2 个文件一个为 tar 格式的 Docker 镜像快照可直接通过 docker load 导入系统另一个为 sh 格式的自动化部署脚本能够自动执行容器创建、端口映射、启动参数配置等步骤压缩包整体约 208.73MB体积较小、便于传输。目前已有 376 人学习下载适合快速搭建流媒体网关进行推拉流验证、协议兼容测试或基于 ZLMediaKit 的二次开发。使用该包可以在离线主机上获得与官方一致的标准运行环境脚本还支持自定义参数与批量迁移显著降低部署门槛和排错成本。1. docker离线安装zlm内网部署流媒体服务的解耦思路一个完全隔离的内网环境里要跑起 zlm 流媒体服务没有外网、没有镜像源很多人第一反应是源码交叉编译结果被一大堆依赖折腾到怀疑人生。我自己的习惯是反过来走在能联网的机器上把 zlm 的 docker 镜像打好包用docker save压成 tar 塞进 U 盘再到内网docker load直接解包运行。这套流程的好处是环境完全隔离、依赖全部封在镜像里宿主上只需要有 docker 引擎剩下的全是配置和端口的事。适合正在做视频接入、摄像头拉流、或者被派到某现场机房处理流媒体服务的同学也是我推荐给 A 同学入门 zlm 容器化的第一条路径。2. 离线镜像适配与落盘先搞清楚镜像分层再save2.1 镜像 tag 选择与体积控制的取舍离线安装和在线安装最大的不同是你没有第二次拉取的机会。所以在docker save之前镜像 tag 选择这件事就必须定下来否则到了内网才发现 tag 不对、架构不匹配就只能把整块 U 盘再送回外网往返成本极高。zlm 镜像通常分为几类基础形态带完整构建工具的 debian 系镜像、精简运行的 alpine 系镜像、以及只包含二进制和运行库的 release 镜像。常见做法是选运行态镜像而不是带编译环境的镜像因为离线包走一趟内网体积直接决定 U 盘和传输时间。我一般会把编译环境留在外网构建机上内网只需要运行态。镜像分层的机制在这里很关键。docker save出来的是一个完整的镜像 tar里面包含所有历史层。假如你在外网反复基于同一个基础镜像打了多个版本的 zlm 镜像save时如果用了不同的 tagtar 包内层可能冗余。一个省事的习惯是在内网最终只保留一个需要的 tag其它一律在 save 前删掉否则一个 200MB 的镜像解包后可能磁盘占用翻倍。# 外网构建机上执行 docker pull zlmediakit:latest docker images # 检查架构确认是 amd64 还是 arm64避免内网机器不匹配 docker save zlmediakit:latest -o zlm_offline.tar ls -lh zlm_offline.tar参数说明docker pull zlmediakit:latest中的镜像名和 tag 要根据你实际使用的镜像仓库填写这里latest只是一个占位习惯严谨的做法是锁定一个明确版本号方便内网追溯。-o指定输出文件名tar 包名建议带架构后缀比如zlm_x64_v1.tar免得内网拿到后还要猜。docker images这一步是为了看 IMAGE ID 和 SIZE顺便确认当前本机没有多余的历史镜像混进来。保存完 tar 之后别急着走先原地用docker rmi把本地这个镜像删掉再重新docker load一次验证这个 tar 是自洽完整的。这一步能挡住很多玄学问题——比如某些镜像 save 后缺失了 manifest 文件在外网机器上因为本地还有镜像缓存所以没事到内网一 load 就报错。2.2 save、load 与内网 registry 两种搬运路径tar 包的搬运分两种场景机器数量少一两台U 盘或 scp 直接拷过去最省事机器数量多或者后续要滚动加节点就应该在内网起一个私有 registry把镜像推上去让每台机器自行拉取。两种路径我都走通过区别只在准备阶段多做一步。先看最朴素的离线路径# 内网目标机上执行 docker load -i zlm_offline.tar docker images # 确认镜像已经出现且 tag 和 REPOSITORY 显示正常 docker run -d --name zlm-test zlmediakit:latest docker logs zlm-test逻辑说明docker load会读取 tar 中每一层镜像数据并导入本地镜像库。执行完docker images时看到nonetag 千万要当心那多半是 save 时没带 tag 或用了docker export而不是docker save。docker run以测试容器先跑一遍是为了在正式改端口前确认镜像本身能起来看日志是否有报错。内网多机场景我一般会在某台内网服务器上起 registrydocker run -d --name registry \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ registry:2 # 把外网带进来的 tar 先 load 进这台机器然后重新打 tag docker load -i zlm_offline.tar docker tag zlmediakit:latest 内网registry地址:5000/zlm:latest docker push 内网registry地址:5000/zlm:latest参数说明-p 5000:5000是 registry 的默认端口内网环境可以直接用 HTTP不加 TLS因为流量不出内网-v /data/registry:/var/lib/registry是镜像数据持久化目录建议单独挂一块盘避免容器删了镜像全丢。docker tag里的“内网registry地址”替换成实际 IP 或主机名。push 完后其它内网机器直接docker pull 内网registry地址:5000/zlm:latest后续升级也只是重新 save 一次再 push比每台机器单独拷 tar 优雅得多。3. 容器化部署 zlm端口规划、配置挂载与链路验证3.1 用 docker run 跑起 zlm 的参数拆解镜像到内网了剩下就是docker run这一步。zlm 容器跑起来并不难难的是端口规划。它不是一个只监听单端口的服务RTSP 走 554RTMP 走 1935HTTP-FLV 和 REST API 共用 80/8080媒体传输时的 RTP 代理还占一段动态 UDP 端口。我在某次现场部署时忽略了 UDP 端口段的映射结果服务起来了、拉流地址也对但画面死活黑屏后来抓包才发现 RTP 包根本进不了容器。这段 UDP 端口就是 zlm 媒体面的命脉少映射一段就是事故。下面是一个经过多次落地验证的基础 run 命令mkdir -p /opt/zlm/log /opt/zlm/config docker run -d --name zlm \ --restartalways \ --privileged \ -p 1935:1935/tcp \ -p 554:554/tcp \ -p 8080:80/tcp \ -p 10000-10050:10000-10050/udp \ -v /opt/zlm/config.ini:/opt/media/bin/config.ini \ -v /opt/zlm/log:/opt/media/log \ -e TZAsia/Shanghai \ zlmediakit:latest参数说明--privileged给容器加了主机级权限zlm 在收取 RTP 包时需要操作一些系统网络参数和 socket 选项不加这个参数在部分环境下会静默丢包这是血泪经验换来的别省。-p 10000-10050:10000-10050/udp这段是 RTP 收流端口段必须和 config.ini 里的rtp_proxy端口配置保持一致否则代理端口对不上媒体数据就断了。-v挂载把配置文件和数据卷放到宿主机这个是给后续升级做准备的容器删了配置还在后悔药就靠这个。TZ环境变量设置时区日志时间错乱的锅基本都能甩给忘了这一项。端口映射可以做一个小表来规划下面是我常用的模板协议容器端口用途备注TCP1935RTMP 推流/拉流摄像头推流常见入口TCP554RTSP 拉流通用播放器接入TCP8080/80HTTP-FLV 与 REST API看网页面板和调接口UDP10000-10050RTP 代理收流端口段可在配置中修改最容易被忽视的是 UDP 段和 privileged 参数。容器映射的端口段太长比如 10000-20000会影响 iptables 规则数量短了又容易在并发收流时端口不足。我一般按每路流占 2 个端口估算50 路以内给 10000-10100 就够了很多同学习惯性复制网上超大端口段反而增加了排查难度。3.2 配置挂载、时区设置与首次启动验证第一次启动 zlm 容器时我建议先不挂载 config.ini让它用镜像内默认配置跑起来确认无报错后再停掉把容器里的配置文件复制出来作为宿主机基础模板。这样做可以避免一个常见的翻车点新下载的 config.ini 是空文件或格式不兼容容器一启动读配置失败直接退出。# 第一步不带配置挂载先跑容器 docker run -d --name zlm-init --privileged zlmediakit:latest sleep 5 docker exec zlm-init ls -l /opt/media/bin/config.ini # 第二步停容器把默认配置复制到宿主机 docker cp zlm-init:/opt/media/bin/config.ini /opt/zlm/config.ini docker rm -f zlm-init # 第三步用宿主机配置重新正式启动 docker run -d --name zlm \ --restartalways \ --privileged \ -p 1935:1935/tcp \ -p 554:554/tcp \ -p 8080:80/tcp \ -p 10000-10050:10000-10050/udp \ -v /opt/zlm/config.ini:/opt/media/bin/config.ini \ -v /opt/zlm/log:/opt/media/log \ -e TZAsia/Shanghai \ zlmediakit:latest逻辑说明第一段docker cp是把容器内的默认配置原样带出来保证格式和字段与当前镜像版本吻合。第二段正式启动时再挂载配置就完全可控了。注意docker cp的路径要以实际容器内路径为准zlm 不同镜像版本的目录结构不完全一样先docker exec进去看一眼再复制是最稳的。这里还有个细节config.ini 中api.apiDebug和ffmpeg相关参数会直接影响排查效率。首次验证时把apiDebug打开能看到 REST API 的请求日志拉流失败时能快速分辨是媒体入口问题还是转协议问题。在生产环境再关掉减少日志量。首次启动后用下面两条命令确认服务活着docker ps curl http://127.0.0.1:8080/index/api/getServerInfodocker ps看容器状态是否是 UPcurl请求 getServerInfo 接口返回 JSON 里有version和api字段就说明主服务正常。看到这里离线安装的核心流程已经闭环但真正的挑战在于媒体流能不能通下面用 ffmpeg 做个端到端的验证。4. 离线安装 zlm 的避坑手册启动、收流、重启三个高频事故区4.1 容器启动失败与静默退出的排查现象docker run -d命令执行后没有报错但docker ps -a看到容器状态是 Exiteddocker logs里只有一行类似 “config file not found” 或者干脆是空日志。原因九成是挂载的 config.ini 是空文件或者路径不存在。docker 在启动容器时如果-v /opt/zlm/config.ini:/opt/media/bin/config.ini左侧文件不存在会自动创建一个目录而不是文件容器里读取配置时拿到的其实是一个目录进程直接崩了。另一种情况是文件存在但权限不足zlm 进程没权限读。解决先移除挂载用默认配置把容器拉起来然后docker cp出默认配置到宿主机最后再正式挂载。另外挂载目录前确保宿主机文件是普通文件而不是目录ls -ld /opt/zlm/config.ini看到首字符是d就说明之前自动建了目录需要rm -rf删掉重新创建文件。我在某台机器上就是第一次挂着不存在的文件跑了rundocker 自动把它变成了目录第二次怎么改都起不来最后删除挂载目录后一切正常这个坑排了我整整一个下午。4.2 RTP 流拉不进来UDP 端口段与防火墙的双重黑匣子现象容器起来了getServerInfo接口正常用播放器拉 RTSP 流一直 timeout但同样的地址在外网环境是通的。ssh 到内网机器上用tcpdump抓包能看到有数据到宿主网卡但容器内docker logs看不到任何收流日志。原因两个独立因素叠加。第一RTP 代理的 UDP 端口段没有映射进容器或者 config.ini 里配置的rtp_proxy端口段和docker run -p映射的段不一致数据到了宿主但无法转发到容器。第二内网防火墙没有放行这段 UDP 端口数据包在宿主外部就被丢了容器才收得到状态里静默无日志。解决逐层排查。先用ss -lunp | grep 10000确认宿主 UDP 端口处于监听状态再firewall-cmd --list-ports或 iptables确认端口段已放行。若使用的是内网安全组策略把 10000-10050/udp 加进规则。最后核对 config.ini 中rtp_proxy的端口范围和 docker 映射的端口段一致。还有一种快速验证手段把-p直接改成--network host方式临时跑一次如果流立刻通了就说明问题出在端口映射或防火墙而不是 zlm 配置本身。4.3 重启后配置回滚与镜像 tag 丢失现象容器跑了一周某天例行重启之后之前改过的流媒体端口、转码参数全部回到默认值。或者内网docker load完成后docker images列表里出现 REPOSITORY 为none的镜像导致docker run时没法按名字引用。原因配置回滚是因为没有挂载 config.ini所有修改都写进了容器的可写层容器被删除或重建后自然丢失。tag 为none则是离线打包时走了docker export它导出的是容器文件系统不保留镜像元数据而不是docker save或者 save 时镜像没有打 tag。解决配置用-v挂载到宿主固定目录并定期备份为config.ini.bak镜像打包强制用docker save REPOSITORY:TAG的完整格式不要图省事写镜像 ID。拿到none镜像时可以用docker history 镜像ID回溯它的来源如果是手工误操作导入的直接删掉重新从正确的 tar load 一次别浪费时间在一堆 unknown 镜像里找关系。离线部署的环境没有后悔药每一条命令都要想清楚它能被重复执行几回。5. 让它跑得更省心配置热更新与容器自愈的落地技巧zlm 容器化部署完成后最影响日常体验的是配置修改。常见做法是改完 config.ini 就docker restart zlm但生产环境中断几秒就意味着播放卡顿。我习惯用 zlm 的 REST API 做热更新避免容器反复重启先修改宿主机挂载的 config.ini然后调用接口让服务重载整个过程播放不中断。curl -X POST http://127.0.0.1:8080/index/api/reloadConfig参数说明reloadConfig 是 zlm 的配置重载接口前提是api.apiDebug模式下接口可访问。热更新能覆盖端口段调整、日志级别、协议开关等大部分配置但需要注意RTP 端口段的修改生效后docker 层面映射的 UDP 段不会自动跟着变所以涉及端口段调整时仍然需要重建容器。这一点很多人不知道改完接口提示成功结果收流还是断的。除了热更新自愈是第二件事。内网环境没有云平台的负载均衡进程级故障只能靠 restart 策略兜底。我一般会在宿主机加一条健康检查脚本循环探测getServerInfo接口连续失败三次就重启容器#!/bin/bash fail0 while true; do code$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:8080/index/api/getServerInfo) if [ $code ! 200 ]; then fail$((fail1)) if [ $fail -ge 3 ]; then docker restart zlm fail0 fi else fail0 fi sleep 10 done这段脚本的逻辑是curl只取 HTTP 状态码非 200 视为服务异常连续三次才触发docker restart避免单次网络抖动就误杀容器。用systemd或crontab托管这个脚本即可。那以后我每次在离线环境交付 zlm都会在部署目录里放三个东西镜像 tar 包、config.ini 挂载文件、这个健康检查脚本。有了这套组合容器能跑多久不取决于运气而取决于计划内的维护窗口。这套习惯源自一次现场事故——某台内网机器凌晨因日志写满磁盘导致 zlm 异常退出等我第二天才发现从那以后每次部署都会强制走一遍端口段核对、配置挂载确认和状态探测脚本。希望帮到你少走几趟没人提醒的内网弯路。本文还有配套的精品资源点击获取