1. 为什么把 TVHeadend 塞进容器:先把选型逻辑理清楚
TVHeadend 本质上是一个电视信号接收与流媒体分发服务,它把从调谐器、网络播放列表拿到的原始流,统一转成标准协议分发给局域网里的各种播放客户端,同时负责电子节目单抓取、定时录制、自动录播规则和简单的转码。社区里那套 linuxserver/tvheadend 镜像,把它连同依赖库、启动脚本、权限收敛一起打包好了,用一条 compose 就能拉起来。我折腾这套东西的出发点很朴素:家里有一块闲置的电视接收卡,配上一台常年通电的机器,想让几台设备都能看同一路信号,还想顺手把没时间看的节目录下来。
这套方案的受众其实挺明确。第一类是手上有 DVB 接收设备、又不想为了驱动和依赖把宿主系统搞脏的人;第二类是靠网络播放列表接收节目、需要一个统一的频道聚合和录制中枢的人;第三类是把这套服务当成家庭媒体中心的一个模块,想和已有的播放器、存储、自动化流程串起来的人。如果你只是想临时看个流,装个播放器就够了,完全没必要上这层。但凡是涉及多客户端、定时录制、节目单管理,单机播放器就撑不住了,这时候 TVHeadend 的调度能力才体现出来。
容器化的核心收益在于依赖隔离和可迁移性。TVHeadend 的编译依赖相当庞杂,涉及各类驱动库、编解码库、EPG 解析模块,一旦装在宿主机上,版本冲突几乎是必然的,尤其是在已经有其他媒体服务的机器上。容器把这一整套东西锁在镜像层里,宿主机只需要提供内核模块和设备的访问权限,剩下的都在容器内部消化。更实际的好处是可迁移:配置目录一整份拷走,换台机器换个架构,改一下设备映射就能重新跑起来,这个体验比重新编译一遍要舒服太多。
代价也得说清楚。容器化之后,硬件设备的直通变成了一件需要显式声明的事,调谐器、核显这类设备如果没映射进容器,服务就是空转状态;网络模式的选择也变得敏感,尤其是走组播接收的场景,桥接模式下的地址转换会让接收端一脸茫然。另外镜像本身的更新节奏由上游决定,你可能遇到“镜像升级了但配置文件格式变了”的情况,这就要求配置文件必须独立挂载、独立备份。想明白这些取舍,后面的部署过程才不会反复返工。
2. 部署前的环境勘察与目录规划
2.1 宿主机需要满足的硬性条件
动手之前先做三件确认。第一,确认接收设备在宿主机上已经被内核正确识别,设备节点存在且能读到。第二,确认 Docker 服务本身是正常的,版本别太老,compose 插件可用。第三,确认端口没被占用,9981 和 9982 这两个是服务的主入口和客户端协议入口,撞上了就得改映射。这三件事任何一件没确认就往下走,后面出问题排查成本会翻好几倍。
设备识别这一步,我习惯先看设备节点清单,再看内核有没有报错。如果节点在但读不出来,那大概率是驱动没加载完整,这时候往容器里映射设备是没意义的,容器只是把宿主机的问题原样带进去。我在一台旧机器上就遇到过这种情况,设备节点时有时无,最后定位是供电不稳导致的枚举失败,换成带独立供电的接口之后就稳定了。这类问题看起来和容器毫无关系,但它会伪装成“容器读不到设备”,非常耽误时间。
# 查看接收设备节点 ls -l /dev/dvb/adapter0/ 2>/dev/null # 查看核显渲染节点,做硬件转码要用 ls -l /dev/dri/ # 检查内核是否记录了设备异常 dmesg | tail -50端口检查也别偷懒,一条命令的事情,能省掉半小时的困惑。
ss -lntp | grep -E '9981|9982'如果已经有进程占着,要么换宿主机映射端口,要么先把旧的服务停掉。这里有个细节:容器内部端口是固定的,你改的只是宿主机侧的映射,客户端连接时填的也是宿主机侧的端口号,别搞混。
2.2 配置目录与录像目录的分离设计
目录规划这件事,我见过太多人图省事直接把所有东西塞进一个目录,结果迁移的时候一片混乱。比较稳妥的做法是两个挂载点分开:配置目录只放服务自身的设置、数据库、EPG 缓存,体积小、变动频繁、必须有备份;录像目录放大文件,体积大、变动少、可以放独立磁盘。这样拆开之后,备份策略可以对症下药,配置目录每天增量备份一次也就几兆到几十兆,录像目录按需清理即可。
宿主机上的目录建议用固定的绝对路径,别用相对路径,尤其是你打算用 systemd 或者其它方式托管容器的时候,相对路径的解析基准很容易出岔子。我给配置目录留的权限是服务运行用户可读写,别的用户只读,录像目录则是服务用户可读写、家庭组可读,方便其它媒体服务直接读取录制文件。
注意:配置目录一旦被服务写入了数据库文件,就不要再手动编辑里面的内容,服务运行中改动会导致状态不一致,严重时起不来。要改先停容器。
权限这块属于高频翻车点。镜像通过 PUID 和 PGID 两个环境变量决定容器内进程以什么用户身份运行,这两个值必须和宿主机上目标目录的属主一致。很多人填了 1000 但宿主机目录属主是别的号,服务能启动、界面能打开,但一保存设置就报错,或者录像目录写不进去。原理很简单,容器内的进程没有宿主机上对应目录的写权限,内核层面就被拒了,跟容器技术本身没关系。
id 你的用户名 # 拿到 uid 和 gid mkdir -p /srv/tvheadend/config /srv/tvheadend/recordings chown -R 1000:1000 /srv/tvheadend2.3 时区、用户与运行参数的取舍
时区这个环境变量看着不起眼,坑却最深。它直接影响三件事:日志时间戳、节目单时间的展示、定时录制任务的触发时刻。我见过最典型的表现是节目单整体偏移若干小时,节目表对不上,录制任务在错误的时间点启动。原因是服务内部把时间统一按 UTC 处理,展示层按容器时区换算,容器时区没设或者设错了,换算就错了。所以这个变量必须显式覆盖,不要指望默认值。
运行参数这个变量是留给高级用途的,本质上就是往启动命令后面追加参数。多数人用不上,但有几个场景值得知道:需要改内部监听端口、需要开启更详细的调试输出、需要给进程加额外的启动选项。我一般先留空,等遇到具体需求再往里加,加完记得把改动记录在配置文件旁边的注释里,不然过半年自己都不记得为什么这么配。
| 变量 | 作用 | 常见取值与说明 |
|---|---|---|
| PUID | 容器内进程的用户 ID | 与宿主机目录属主一致,一般是 1000 |
| PGID | 容器内进程的用户组 ID | 同上,填对应用户的主组 |
| TZ | 容器时区 | 按所在地时区填写,影响节目单与录制调度 |
| RUN_OPTS | 追加启动参数 | 默认留空,需要调端口或加调试选项时使用 |
用户与组的设计上,我的习惯是不用 root,也不用宿主机上已有登录权限的账号,单独建一个只用于跑容器服务的账号,只给它需要的目录权限。这么做的好处是安全边界清晰,将来这个服务被攻击或者出问题,影响范围被限制在它自己的目录里。
3. 完整实操:从 compose 文件到频道能看
3.1 compose 文件的写法与每一行的意图
下面这份是我现在在用的结构,参数不多,但每一行都有理由。
services: tvheadend: image: lscr.io/linuxserver/tvheadend:latest container_name: tvheadend environment: - PUID=1000 - PGID=1000 - TZ=Asia/Shanghai - RUN_OPTS= volumes: - /srv/tvheadend/config:/config - /srv/tvheadend/recordings:/recordings ports: - "9981:9981" - "9982:9982" devices: - /dev/dri:/dev/dri restart: unless-stopped镜像地址用的是上游的通用镜像仓库地址,比直接写短名更稳妥,能避免被拉到同名但来源不同的镜像。容器名固定下来,方便后续查看日志和做运维命令,别用自动生成的随机名。重启策略选 unless-stopped,意思是除非我手动停,否则异常退出会自动拉起,这个策略对家庭常驻服务最合适,机器重启后服务自己回来,不用人工干预。
端口映射这里有个必须说清楚的点:9981 是网页管理界面的入口,9982 是播放客户端使用的二进制协议入口。两个都要映射,只映射 9981 的话,管理界面能开,但客户端连不上,表现是播放器一直转圈或者提示连接被拒。设备映射那一行是为了硬件转码,不做转码的话可以删掉,但留着也没坏处,只要宿主机有对应节点。
如果需要接收组播形式的网络流,桥接模式会遇到麻烦,因为组播依赖链路层的加入请求,经过地址转换之后通常收不到。这种情况要改成 host 网络模式,同时把端口映射段删掉,因为 host 模式下端口直接占用宿主机,映射声明是无效的。
network_mode: host # 使用 host 模式时,删除 ports 段落改用 host 模式之后,容器和宿主机共享网络栈,服务监听的端口直接对外,管理界面还是同一个端口访问。这个改动看起来简单,但它会带来一个副作用:容器内的服务能看到宿主机所有网卡,如果宿主机有多块网卡或者多套网段,接入控制的配置要跟着调整,否则可能出现“明明同一个网段却提示未授权”的情况。
3.2 首次启动与初始化向导
拉起容器之后,先别急着开网页,看一眼日志确认没有报错退出。
docker compose up -d docker compose logs -f --tail=100日志里如果出现权限相关的报错,回头检查目录属主和 PUID、PGID 是否一致;如果出现端口占用的报错,回去处理端口冲突。日志干净之后,浏览器访问宿主机的 9981 端口,会进入初始化向导。
向导一般会让你选语言、配时区、设置访问控制和管理员凭据。这里有个容易忽略的点:访问控制决定了哪些网段可以访问、能不能使用管理界面、能不能拉流,默认规则通常是允许的,但如果你后续要收紧,记得先给自己留一条通行的规则,不然改完就把自己锁在外面了。管理员凭据务必设置,别图省事留空,局域网里的设备比你想的要多。
初始化完成之后,界面会重载。这时候先进配置页面把基础项过一遍:录制文件的存放路径确认指向挂载的录像目录,节目单抓取的开关先关掉避免干扰后续排查,日志级别保持默认即可。
3.3 接入信号源:接收设备与网络播放列表
TVHeadend 的信号来源主要分两类,物理接收设备和网络流。两类在界面上的入口不同,但后续的处理链路是一致的:先定义信号源,再扫描出可用频点,再从频点里提取服务,最后把服务映射成频道。理解这个链路,排查问题的时候就知道该看哪一层。
物理设备的接入相对直接,进入信号源配置页,能看到已经识别到的设备列表,选好类型,添加一个频点扫描任务,等服务自己扫完。扫描时间取决于频点数量,全频段扫描可能要十几分钟甚至更久,别盯着页面刷新,喝杯茶回来再看。扫描完之后在服务列表里能看到找到的条目,勾选需要的,映射成频道。
网络流的接入稍微绕一点。新建一个网络类型的信号源,把播放列表的地址填进去,协议按来源选,然后对这个网络执行扫描。扫描过程本质是逐个拉取条目并探测流的参数,条目多的播放列表会花不少时间。这里踩过一个坑:播放列表里混有已经失效的地址,扫描会卡在某个条目上很久,最后超时。我的处理办法是先拿一份自己验证过的精简列表做首次接入,跑通之后再往里加。
提示:扫描失败不一定是配置错了,先看日志里的具体报错。连接超时、格式不识别、鉴权失败是三种完全不同的原因,日志里区分得很清楚。
频道映射完成之后,别忘了在频道列表里排序、分组、补上图标。这一步看着是美化,实际影响使用体验:客户端拉取频道列表时按顺序展示,顺序乱七八糟的话,每天都要在几十个频道里翻找。分组可以按内容类型做,配合客户端的收藏功能,实际用起来顺手很多。
3.4 让客户端连上来并跑通第一路流
客户端接入依赖两件事:访问控制放行,以及客户端使用正确的协议和端口。前面提过 9982 是二进制协议入口,支持该协议的各种播放器客户端都走这个口。配置访问控制时,把局域网网段加进去,勾选允许播放和允许管理,保存后客户端立刻就能连上。
我在第一次接入时遇到的典型问题是“能连上但列表为空”。原因是我在访问控制里勾了允许播放,但没有把这个条目关联到具体的用户权限,导致客户端拿到的是一个空权限的会话。排查思路很简单:先看服务端日志有没有客户端连接记录,有连接记录说明网络通,问题在权限;没有连接记录说明网络或端口有问题,往回查映射和防火墙。
跑通第一路流之后,建议立刻做一次压力验证:同时用两三个客户端拉同一个频道,看服务端 CPU 和网络占用,看是否出现卡顿或者花屏。这一步能提前暴露带宽不足、转码性能不够、磁盘 IO 瓶颈之类的问题。广播流的复制分发本身开销不大,但一旦开了转码,每路流都是实打实的计算量,几个客户端同时转码能把弱平台直接压垮。
4. 性能调优:转码、存储与网络三线并行
4.1 硬件转码的启用路径与验证方法
软件转码是最容易把机器拖垮的环节。原始广播流的码率和封装格式未必是客户端能直接吃的,尤其是移动端和旧设备,经常需要重新编码。纯靠 CPU 做这件事,一路 1080p 流就能吃掉好几个核,多路并发直接卡死。所以只要宿主机有可用的硬件编码单元,就应该把转码交给它。
启用步骤分三步。第一步,把渲染设备节点映射进容器,compose 里那一行就是干这个的。第二步,在流配置页面新建或修改一个转码配置,编码器选硬件加速的那一项,并指定设备路径。第三步,把这个配置绑定到具体的频道或者客户端访问规则上,否则配了也不生效。很多人卡在第三步,配好了参数但客户端拿到的还是原始流,就是因为没有绑定。
验证是否真的走了硬件,别只看界面上的开关,要看实际负载。转码期间观察 CPU 占用,如果某几个核心占用很低但流正常输出,说明硬件在干活;如果 CPU 被吃满,说明还是软编,回去检查绑定和设备路径。
# 转码运行时观察负载 docker stats tvheadend top -bn1 | head -20还有一种情况是硬件支持但驱动不匹配,表现是配置能保存但转码任务立刻失败。这类问题查日志最直接,编码器初始化失败会打出明确的错误信息,按提示补驱动或者换编码器即可。
4.2 录像存储的容量估算与清理策略
录像盘容量规划是最容易被低估的部分。按经验估算,标清节目每小时大约一到两个 GB,高清节目每小时三到八个 GB,具体取决于码率和编码格式。如果你习惯“看到想看的就加自动规则”,一个月下来几百 GB 是常态。我的做法是先预留一块专门的盘,容量按“日均录制时长乘以单小时体积再乘以保留天数”来算,留出至少三成余量。
怎么估日均录制时长?把自动录播规则列一遍,看看每周大概有多少个节目会命中,乘以单集时长,再除以七。这个数字不用很精确,估算的意义在于让你知道盘会不会被撑爆。
清理策略有两条路。一条是按时间清理,超过 N 天的自动删除;另一条是按空间清理,剩余空间低于阈值时删最旧的。服务本身提供了相关配置项,也可以写个定时任务在宿主机侧处理。我的建议是两条都配上,互为兜底。曾经有一次自动规则写得太宽泛,一周录了几十个小时,要不是有空间阈值兜底,盘就写满了,写满之后连配置都存不进去,整个服务处于半瘫状态。
| 内容类型 | 单小时体积估算 | 建议保留策略 |
|---|---|---|
| 标清节目 | 1 至 2 GB | 保留 30 天,或按空间阈值清理 |
| 高清节目 | 3 至 8 GB | 保留 14 天,重要内容手动归档 |
| 转码后归档 | 视码率而定 | 归档到独立目录,不参与自动清理 |
4.3 网络模式选择对实际使用的影响
网络模式的选择不是偏好问题,是功能问题。桥接模式胜在隔离性好、端口清晰,适合纯单播场景;host 模式胜在能处理组播、能拿到真实客户端地址,适合需要接收组播或者需要精确的接入控制场景。选错模式的表现很具体:组播收不到、客户端地址全显示为网关地址导致接入控制失效、端口映射写了但不生效。
我现在的做法是分两套配置:一套桥接的用于日常单播,一套 host 的备用,需要接收组播时切过去。切换的成本很低,改两行配置重启容器即可,配置目录是同一个,数据不会丢。但要注意,host 模式下容器内的服务监听的是宿主机的端口,如果宿主机上还有别的服务占用同一个端口,冲突会直接导致启动失败。
还有个细节:host 模式下容器的服务会监听所有网卡,如果宿主机有对外接口,务必在接入控制里把网段收紧,只允许局域网访问。这个不是可选项,是必做项。
5. 常见故障的排查路径与速查表
5.1 权限与启动类故障
这类问题的特征是“容器起来了但服务不干活”。容器状态显示运行中,日志里却反复出现写文件失败、数据库打不开之类的错误。九成以上的原因是 PUID、PGID 和宿主机目录属主对不上。排查方法很直接:进容器看一眼进程身份,再对比宿主机目录属主。
docker exec -it tvheadend id ls -ln /srv/tvheadend/config两个输出的数字对不上就是问题所在。改法有两种,改环境变量或者改目录属主,选一个就行,但改完要重启容器让进程重新加载身份。这里有个容易犯的错:改了环境变量但没重启,配置虽然更新了但进程还是老身份,看着改完没效果,其实是没生效。
另一类启动故障是配置目录里有残留的状态文件,比如上次异常断电留下的锁文件。表现是容器反复重启,日志里提示数据库被锁定或者状态不一致。处理办法是停掉容器,把锁文件删掉,再启动。平时做好备份的话,也可以直接回滚到上一次的备份。
5.2 信号与流相关故障
“扫描不到任何频道”是最常见的求助。排查顺序建议固定下来:先确认信号源本身有没有数据,再确认权限能不能访问设备或网络,最后确认扫描参数是否正确。物理设备的话,宿主机上直接读一下节点就知道设备通不通;网络流的话,宿主机上直接访问那个地址看看能不能拉到数据。这两步能把问题范围砍掉一半。
“能扫到频道但播放黑屏”通常是客户端能力的问题。原始流的封装格式或者音频编码客户端不支持,需要开启转码或者换一个流配置。排查方法是先用一个能显示详细信息的客户端试,看它拿到的流参数是什么,再对着参数决定转哪个部分。
“播放几秒就断”多数和网络带宽或者服务端的输出缓冲有关。局域网千兆环境下一般不会带宽不足,更多是无线连接不稳,或者服务端输出缓冲配置太小。把缓冲调大一点,或者把客户端换到有线网络,通常就能解决。
5.3 录制与节目单异常
节目单整体偏移是最典型的时区问题,前面已经说过。还有一种情况是节目单抓不到,日志里提示解析失败,这通常是外部节目单文件格式不标准,或者抓取地址不可达。处理办法是先手动下载一份节目单文件,放到配置目录下的对应位置,看服务能不能解析,能解析说明源的问题,不能解析说明文件本身格式有问题。
录制任务到点没启动,排查方向有三个:任务是���真的被调度了、调度时间是否正确、目标目录是否可写。服务界面里能看到任务状态,如果显示已调度但没执行,看日志里有没有权限报错;如果压根没调度上,回去看自动规则是不是写得太窄,标题匹配一个字符之差就命不中。
“录出来的文件是零字节”我遇到过两次。一次是目标目录权限问题,文件创建了但写不进去;一次是信号在录制期间中断,任务没有做错误处理。前者改权限,后者可以在录制配置里开启失败重试,减少损失。
5.4 故障速查表
| 现象 | 高概率原因 | 处理动作 |
|---|---|---|
| 容器反复重启 | 配置目录权限或锁文件残留 | 核对 PUID/PGID,清理锁文件 |
| 管理界面打不开 | 端口冲突或映射未生效 | 检查端口占用,确认映射写法 |
| 扫描不到频道 | 设备未识别或网络地址不通 | 宿主机侧验证设备与地址可达性 |
| 客户端列表为空 | 接入控制未放行对应网段 | 补充网段并关联用户权限 |
| 播放黑屏 | 客户端不支持原始流格式 | 启用转码或调整流配置 |
| 节目单时间偏移 | 容器时区与预期不符 | 修正时区变量并重启 |
| 录像为零字节 | 目标目录不可写 | 修正属主与写权限 |
| 转码占满 CPU | 未走硬件编码 | 检查设备映射与配置绑定 |
6. 备份、升级与长期运行的几个习惯
6.1 配置备份的最小可用方案
配置目录里最重要的是数据库文件,它承载了频道映射、自动规则、用户权限、录制历史这些无法快速重建的信息。我的备份方式是每天凌晨打包配置目录,保留最近十四份,打包前先停容器或者用服务的导出功能,避免备份到写了一半的文件。录像目录不参与每日备份,重要内容手动归档到独立位置。
恢复的时候注意版本对应。配置文件的格式会随服务版本变化,用新版本读旧配置一般没问题,反过来用旧版本读新配置就可能出问题。所以升级前一定先备份,升级出问题能立刻回滚。
# 简单的每日备份脚本思路 tar -czf /srv/backup/tvheadend-$(date +%F).tar.gz -C /srv/tvheadend config find /srv/backup -name 'tvheadend-*.tar.gz' -mtime +14 -delete6.2 镜像升级的节奏与回滚准备
升级镜像的流程我固定成四步:备份配置、记录当前镜像标签、拉新镜像、重启验证。验证重点是三件事,管理界面能打开、频道列表还在、录制任务能正常触发。三件事都过了一遍,才算升级完成。如果验证失败,把镜像标签切回记录的那一版,重启即可回滚,配置目录没动过就不用担心数据。
不建议用自动升级。这个服务的配置结构变动虽然不频繁,但一旦遇到不兼容的改动,自动升级会在你不知情的时候把服务搞挂,等你发现的时候已经错过好几天的录制任务了。手动升级、手动验证,多花十分钟,省掉很多麻烦。
6.3 长期运行的一些个人习惯
跑了这么久,有几个习惯我觉得值得保留。第一,所有配置改动都写一行注释在 compose 文件里,注明改动原因和日期,半年后回头看能省下很多记忆成本。第二,定期看一眼日志的告警部分,哪怕服务看起来正常,早期信号不稳、磁盘空间下降这类问题都会先在日志里冒头。第三,别把所有服务都塞进一台机器,转码、录像、分发这三件事的负载特征完全不同,挤在一起的时候互相影响,排查起来也很痛苦。
还有一点关于频道维护。节目源是会变的,频点参数会调整,网络地址会失效,如果不定期维护,某天你打开客户端会发现一半频道播不了。我的做法是每月抽十分钟过一遍频道列表,随机点几个验证,有问题的当场处理。这个习惯能把“某天突然全部失效”的糟糕体验,拆成每次十分钟的小维护。
最后再分享一个我觉得很实用的技巧:给不同的客户端类型准备不同的流配置。客厅的大屏设备直出原始流,画质最好;移动设备和弱性能终端走转码配置,牺牲一点画质换流畅度。这样同一套服务能同时满足两类需求,而不是让所有人迁就其中一种。配置本身不复杂,在流配置页面建两份,然后在访问控制的条目上分别绑定就行,但实际用起来体验差别很大。