上周一个朋友找我诉苦,说他在一台服役多年的 CentOS 7 服务器上折腾 yt-dlp,目标只是把一个 YouTube 视频下载成 1080P 高画质,结果从下午弄到半夜:先是系统自带 Python 版本太老,接着 ffmpeg 装不上,最后好不容易启动起来,又抛出一个requested format is not available,整个人都快被搞崩溃了。
这个场景我相信不少还在 CentOS 7 上干活的朋友都遇到过。yt-dlp 本身不复杂,真正麻烦的是它依赖的运行环境:需要够新的 Python 版本,需要 ffmpeg 做音视频流合并,还要应对 YouTube 时不时调整页面结构带来的高频更新。想在一台操作系统停止大版本维护、自带软件包停留在五六年前的老服务器上把这些全搞定,靠手动编译和 yum 装包,基本是在跟环境死磕。
我当时给朋友的建议很直接:不要再跟这台 CentOS 7 的系统环境较劲了,直接把 yt-dlp 和 ffmpeg 都装进一个 Docker 容器,把整个下载环境固化下来。这篇文章就把我当时落地的完整方案写出来,包括 Dockerfile 的思路、高画质下载的格式选择命令、日常打包成脚本的方法,以及后续踩过的一些坑。如果你也在 CentOS 7 或类似的旧系统上做 Youtube 高画质下载,可以直接照着搬。
1. 一装就卡三关:CentOS 7 上 yt-dlp 不是工具不行,是环境太旧
1.1 第一道坎:系统默认 Python 版本根本不够新
CentOS 7 自带的 Python 是 2.7,即便是后来通过 Software Collections 装的python3,也基本停留在 Python 3.6 左右。而现在的 yt-dlp,官方要求已经是 Python 3.9 以上,新版本甚至建议直接用 3.10 或 3.11。这个版本差一截,不是“凑合用”就能解决的,因为 yt-dlp 的代码里会广泛使用较新的语法和标准库特性,版本不够时经常在运行中途直接崩溃,报各种看不懂的SyntaxError或ModuleNotFoundError。
我见过不少人为了解决 Python 版本问题,手工编译了一个 Python 3.11 放到/usr/local,结果后面又冒出更多连锁问题:编译时缺libffi-devel、缺 OpenSSL 开发头文件,编译出来的 Python 虽然能跑,但某些网络功能表现得不太正常。最关键的是,手工编译的 Python 不会跟 CentOS 7 的包管理系统形成依赖关系,后续系统安全更新时很容易把你的自定义 Python 环境弄坏。你要的是一个下载工具,不是去挑战“从源码构建完整 Python 运行环境”这种高难度副本。
1.2 第二道坎:没有 ffmpeg,高画质下载等于缺了手
真正决定能不能拿到高画质的关键工具,其实是 ffmpeg。YouTube 网站上 1080P 以上的视频流和音频流绝大多数是分开存放的,yt-dlp 必须先分别下载独立的视频流、音频流,再用 ffmpeg 把两条轨道合并成一个文件。如果系统里没有 ffmpeg,或者 ffmpeg 版本太老,yt-dlp 就只能拿到低画质的单文件格式,或者干脆合并失败,最终落下一堆.webm、.m4a的碎片文件。
麻烦的是,CentOS 7 的默认 yum 源里根本没有 ffmpeg 包。想装它,要么自己编译,要么折腾第三方源。自己编译 ffmpeg 虽然听起来不算太难,但编译时间长、依赖项多,而且非专业做音视频的人很容易在 configure 阶段就被各种缺库劝退。我朋友那台服务器上,光是把 ffmpeg 源码准备好、编译完,就已经花了快一个小时,结果还因为系统里某个库版本不合,编译出来的 ffmpeg 运行就崩。
1.3 第三道坎:库版本冲突和依赖诅咒
CentOS 7 的老还体现在系统的底层库上:OpenSSL 版本偏老、CA 证书库可能过期、glibc版本较低。这些看起来跟下载视频没关系,但实际上都会影响到 Python 的网络请求。你在 CentOS 7 上跑 yt-dlp,偶尔会碰到类似的报错:证书验证失败、TLS 握手异常,甚至某些新版本的 Python 包需要比较高版本的glibc,CentOS 7 原生环境根本跑不起来。
这种问题最麻烦的地方在于:它不是 yt-dlp 的错,也不是你命令写错了,而是“系统库和环境版本”已经不适合现代 Python 生态。你可以花一整天一个个去修补,但补完这个还有下一个,像是在一间地基已经偏移的房子里反复刷墙。这也是为什么我最终决定直接跳过宿主机系统环境,用 Docker 把下载环境整体换掉。
2. 为什么我坚持用 Docker 来解决,而不是手动编译或 venv
2.1 “本地化安装”表面的好,实际上是把问题养大
在考虑 Docker 之前,我也认真评估过两条更常规的路线:一是直接在 CentOS 7 源码编译 Python 和 ffmpeg,二是用虚拟环境venv隔离 Python 包依赖。先说结论:这两条路不是完全走不通,但都不适合“长期、稳定、可迁移”的下载任务。
源码编译的问题已经说了,时间成本高,且很难保证环境的可复现性。你在这台机器上好不容易调通了,换一台机器又得重新折腾一遍。更别说 CentOS 7 的运维周期已经接近尾声,安全补丁和软件源更新越来越少,你今天编译出来的环境,半年后可能就变成一个新的“历史遗留问题”。
venv只能隔离 Python 包层面的依赖,它隔离不了 Python 解释器版本,更隔离不了系统级的libffi、OpenSSL、ffmpeg 这些外部程序。你仍然要先把这些底层依赖在 CentOS 7 上装好,才能在 venv 里顺利跑 yt-dlp。等于说,最麻烦的部分一点没少。
2.2 容器的本质:把 CentOS 7 那一套系统库隔离在门外
Docker 的思路就完全不一样了。它不是在 CentOS 7 上“修复环境”,而是直接给 yt-dlp 提供一个全新的用户空间。容器内部可以运行 Debian、Ubuntu 或任何我们想要的基础系统,它的 Python、ffmpeg、OpenSSL 全部来自一个全新的软件源体系,和宿主机那个陈旧的 CentOS 7 完全没有关系。
对宿主机的依赖,只剩下 Linux 内核和 Docker 服务本身。CentOS 7 内核虽然在变老,但运行 Docker 容器是没问题的,尤其是云服务器上常用的一批 CentOS 7 机器,都还在正常跑着各种容器业务。这个方案相当于把“操作系统环境”这个最大的变量直接移除掉了。只要 Docker 能跑,yt-dlp 的下载环境就永远是同一套。
那我在标题里说“Docker 解决”的意思,说白了就是:不要试图在旧系统里塞进一个全新的世界,而是把那个新世界打包成独立的小盒子,随用随取。
3. Dockerfile 与构建流程:我把部署过程完整复现一遍
3.1 为什么要用 python:3.11-slim 做基底而不是 alpine
我选择的基底镜像是python:3.11-slim-bookworm。这个选择是踩过不少次alpine版本坑之后才确定的。
用alpine的优点是镜像小,但缺点在于它用的是musl libc,和常见的 Debian/Ubuntu 二进制轮子有时存在兼容问题。比如yt-dlp本身是纯 Python 问题不大,但 ffmpeg 这类音视频处理程序在musl环境下偶尔会出现一些行为差异,某些编码器的编译分支和性能表现也未必有 glibc 版本稳定。
而python:3.11-slim基于 Debian,软件包体系成熟,apt-get install ffmpeg能直接装到一个可用的 ffmpeg,不用额外编译。镜像体积会多一两百兆,但对于下载任务来说完全无所谓,稳定性和可排查性更重要。
3.2 Dockerfile 内容和每行命令的用意
下面这个 Dockerfile 就是我现在实际在用的方案,整体非常精简:
FROM python:3.11-slim-bookworm RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg \ ca-certificates \ openssl \ && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir -U yt-dlp WORKDIR /downloads ENTRYPOINT ["yt-dlp"]逐行解释一下我的设计意图。
ffmpeg负责音视频流的下载后合并,是最重要的外部依赖。ca-certificates和openssl则是为了让 Python 在做 HTTPS 请求时能正常完成证书验证,避免出现SSL: CERTIFICATE_VERIFY_FAILED之类的问题。Debian 镜像里默认不一定带全套根证书,所以我会显式装上。
pip install --no-cache-dir -U yt-dlp这行是重点。-U表示每次构建都升级到 PyPI 上的最新版本。yt-dlp 这个工具更新非常频繁,YouTube 每次改动页面结构和接口字段,都会导致老版本失效。把更新动作直接固化在镜像构建过程里,比进了容器再手动yt-dlp -U要干净得多。
为什么用ENTRYPOINT ["yt-dlp"]而不直接写CMD?因为docker run后面跟的参数会自动追加到 ENTRYPOINT 后面。这样你执行docker run ytdlp-tool:latest --version时,实际等价于在容器里运行yt-dlp --version,用起来就像本地装了 yt-dlp 一样顺手。
3.3 构建、验证和日常更新方式
构建命令很简单:
docker build -t ytdlp-tool:latest .第一次构建如果网络慢,可能要几分钟,因为要拉基础镜像和安装 ffmpeg。构建完成后先验证一下:
docker run --rm ytdlp-tool:latest --version能正常打印版本号,说明基本环境已经通了。再测试一个最简单的下载:
docker run --rm \ -v /data/downloads:/downloads \ ytdlp-tool:latest \ -o "/downloads/%(title)s.%(ext)s" \ "https://www.youtube.com/watch?v=VIDEO_ID"这里-v /data/downloads:/downloads是把宿主机的下载目录挂载进容器,容器里生成的视频文件会直接写到宿主机的/data/downloads。后面所有命令我都会这么用,保证文件“落”在宿主机上,而不是藏在容器销毁后的临时层里。
至于日常更新,我不建议进容器里执行yt-dlp -U。因为容器本身是临时的,更新结果不会保留,而且每次运行都重新下载更新反而浪费时间。我习惯的做法是隔一两周重新执行一次:
docker build --pull -t ytdlp-tool:latest .--pull会先把基础镜像拉到最新,然后再重装一次最新版 yt-dlp,整个过程一气呵成。
4. 高画质下载的核心:格式选择与合并逻辑
4.1 为什么不能只写“best”
很多刚接触 yt-dlp 的人会以为加一个-f best就能下载最高画质。这个理解在 YouTube 场景下是不对的。YouTube 对高分辨率视频普遍采用“自适应码流”的方式:画面流和声音流分开编码,分别通过不同的媒体流提供。你写-f best时,yt-dlp 通常只能拿到一个封装好的低画质文件,因为那个“最好的单个文件”并不会包含最高的视频码流。
想要真正拿到 1080P、1440P 甚至 4K,你必须显式告诉 yt-dlp:把最高画质的视频流挑出来,再把最高音质的音频流挑出来,下载之后通过 ffmpeg 合并成同一个文件。
4.2 一套稳妥的格式选择模板
我日常使用的高画质下载命令模板是这样的:
docker run --rm \ -v /data/downloads:/downloads \ ytdlp-tool:latest \ -f "bv*[height<=2160]+ba/b[height<=2160]" \ --merge-output-format mkv \ -o "/downloads/%(title)s [%(id)s].%(ext)s" \ "https://www.youtube.com/watch?v=VIDEO_ID"来解释一下-f参数的含义:
bv*表示选择最佳的 video-only 视频流,星号允许同时挑选多条备用流;[height<=2160]是高度过滤器,限制分辨率不超过 4K;+ba表示同时选择最佳的 audio-only 音频流;- 最后的
/b[height<=2160]是回退选项,如果前面那种“视频流加音频流”的组合失败(比如某些视频没有独立音频流),就退回选择不超过该分辨率的单个最佳文件。
如果你想保守一点,只下载 1080P,把2160改成1080即可:
-f "bv*[height<=1080]+ba/b[height<=1080]"如果想优先 H.264 编码、放弃体积更大的 AV1,可以再叠加格式排序参数:
-S "codec:h264,res,br"这个参数的意思是:优先选 H.264 编码,再按分辨率、码率从高到低排。老电脑或播放器兼容性比较差时,这条命令很好用。
4.3 FFmpeg 合并与 mp4/mkv 选择
格式选择只是第一步。下载下来的视频流和音频流是两个文件,必须靠 ffmpeg 合并。这也是我为什么在 Dockerfile 里专门装 ffmpeg 的原因。yt-dlp 在执行合并时,会调用容器环境里的 ffmpeg,输出格式由--merge-output-format控制。
我个人建议默认用mkv。原因很简单:YouTube 的高画质视频流大量使用 VP9、AV1 编码,音频流则可能是 Opus 编码。这些编码格式在 MKV 容器的兼容性更好,合并时不容易出问题。某些组合如果想强行塞进 MP4,可能因为容器不支持对应编码而报错或导致播放器不识别。
如果你确实需要 MP4,我建议不要去修改--merge-output-format,而是先在 mkv 层面把合并做好,再加一个 remux 参数:
--remux-video mp4remux只是重新封装容器格式,不重新编码,速度快得多。如果 remux 时发现编码与 MP4 容器不兼容,yt-dlp 会跳过并保留 mkv 文件,这比一开始就强制合并成 mp4 更稳妥。
5. 把 Docker 封装成顺手脚本,顺手接到定时任务
5.1 通用下载脚本
如果你只是偶尔下载一两个视频,直接敲上面的docker run命令没问题。但如果想把下载工具长期跑在一台 CentOS 7 服务器上,甚至每天定时下载,我建议把命令包成一个脚本,以后只需要一行就能执行。
我放在/usr/local/bin/yd的脚本长这样:
#!/usr/bin/env bash set -euo pipefail OUTPUT_DIR="${OUTPUT_DIR:-/data/downloads}" CACHE_DIR="${CACHE_DIR:-/data/ytdlp-cache}" QUALITY="${QUALITY:-1080}" mkdir -p "$OUTPUT_DIR" "$CACHE_DIR" docker run --rm \ -u "$(id -u):$(id -g)" \ -v "$OUTPUT_DIR":/downloads \ -v "$CACHE_DIR":/cache \ -e "YTDLP_CACHE_DIR=/cache" \ ytdlp-tool:latest \ -f "bv*[height<=${QUALITY}]+ba/b[height<=${QUALITY}]" \ --merge-output-format mkv \ --no-mtime \ -o "/downloads/%(title)s [%(id)s].%(ext)s" \ "$@"脚本里有两个细节值得注意。
第一是-u "$(id -u):$(id -g)"。这个参数会把当前 Linux 用户的 UID 和 GID 传入容器,让下载生成的文件所有者就是你当前用户,而不是 root。如果不加这个参数,容器里以 root 身份写出来的文件,宿主机上可能需要sudo才能删除或移动,非常别扭。
第二是--no-mtime。下载时我选择不保留原始文件修改时间,而是用下载完成时间作为时间戳。原因是有些视频的发布时间在未来或者不准确,保留 mtime 反而会让文件排序变得混乱。看个人喜好,没有绝对的对错。
使用方式很简单:
yd "https://www.youtube.com/watch?v=VIDEO_ID"想下载 4K 时:
QUALITY=2160 yd "https://www.youtube.com/watch?v=VIDEO_ID"多了两个可选环境变量,其他交给脚本处理。
5.2 播放列表和定时更新
如果你要下载整个播放列表,yt-dlp 原生支持,脚本也能直接透传参数。比如:
yd --playlist-start 1 --playlist-end 20 "https://www.youtube.com/playlist?list=PLAYLIST_ID"如果你希望每天晚上自动下载一个列表里的内容,先在/data/downloads/下建一个文本文件list.txt,一行一个 URL,然后执行:
yd --batch-file /downloads/list.txt --ignore-errors再用 crontab 加上定时任务:
0 3 * * * cd /data/downloads && /usr/local/bin/yd --batch-file /downloads/list.txt --ignore-errors >> /var/log/yd.log 2>&1这样服务器每天凌晨三点会自己开始下载,无聊的等待时间全在夜里消化掉了。日志单独写到/var/log/yd.log,第二天想确认有没有成功,直接tail一下就行。
5.3 升级与维护
这个方案唯一的维护点,就是定期重建一次镜像,把 yt-dlp 更新到最新版本。我的建议频率是每两周一次,或者当你发现下载开始报错时,第一时间先重建。
docker build --pull -t ytdlp-tool:latest .整个重建过程通常在一两分钟内完成,不会影响正在运行的下载任务。因为每次都是docker run --rm,容器用完即删,无状态,所以重建镜像时不需要停任何“服务”,非常轻量。
6. 实操中更容易踩的坑:排查思路记录
6.1 格式条件太严格,报错格式不可用
我朋友第一天遇到的就是这个报错:requested format is not available。它表面含义是没有匹配的格式,但真正原因往往是格式筛选条件写得过于严格。比如bv*[height<=2160][ext=mp4]听起来很合理,但如果 YouTube 当前对那个视频只提供了 WebM 容器的高分辨率视频流,没有 MP4 容器,条件就直接落空了。
排查思路是先把该视频可用的格式全部打印出来:
docker run --rm ytdlp-tool:latest -F "https://www.youtube.com/watch?v=VIDEO_ID"看输出列表里实际有哪些编码和分辨率,再调整筛选条件。尽量把筛选条件写成“可回退”的结构,也就是带/best兜底,不要一刀切写死。
6.2 YouTube 反爬与访客限制报错
用 yt-dlp 下载公开视频时,偶尔会碰到:Sign in to confirm you're not a bot。这是 YouTube 对访客访问的一种风控策略,触发原因通常是短时间内请求次数太多,或者某个 IP 下多个会话并发下载。
遇到这种情况,先做两件事:一是降低下载并发,去掉--concurrent-fragment-downloads这类加速参数,让下载节奏自然一些;二是给 yt-dlp 配置浏览器 Cookie,让它带上更完整的身份信息。具体做法是先导出浏览器里的 Cookie 到文件/data/downloads/cookies.txt,然后在命令里追加:
yd --cookies /downloads/cookies.txt "https://www.youtube.com/watch?v=VIDEO_ID"这里要注意一点:Cookie 文件不要放到挂载会被反复写入的目录,免得被其他下载任务误覆盖。我一般单独放在固定目录,权限设成仅当前用户可读。
6.3 目录权限与 root 用户问题
有一个很隐蔽的问题:如果你直接执行docker run不带-u参数,容器内进程默认是 root,下载出来的文件 owner 是 root。CentOS 7 宿主机上,普通用户想进/data/downloads删除文件,就会遇到权限不足的报错。
这时候不要直接对目录执行chmod -R 777,那是野蛮做法。正确办法就是在脚本里保留-u "$(id -u):$(id -g)",让容器里进程的 UID/GID 和宿主机当前用户保持一致。如果之前已经有 root 产生的文件,可以用chown -R 用户名:用户组 /data/downloads一次性修正。
6.4 镜像更新不及时导致的老化问题
yt-dlp 这个工具的生命力完全靠持续更新。它跟 YouTube 之间的博弈有点像“猫鼠游戏”,YouTube 一旦调整页面结构或接口,老版本就可能立刻失效。常见的特征不是报错,而是下载的视频只有画面没有声音,或者直接下载到一个错误页面文件。
所以如果你的下载长时间不更新镜像,别急着怀疑 Dockerfile 写错了,先去确认容器里的 yt-dlp 是不是已经落后了很多版本。这也是我为什么把pip install -U写进 Dockerfile 的原因,因为每次重建镜像都自动完成一次“修复”,不需要记得手动升级。
用 Docker 跑 yt-dlp 之后,我最大的感受是:下载工具终于变成了一个“一次性搭建、长期可用”的固定组件,而不是一个随时需要伺候的系统疑难杂症。CentOS 7 的老库、Python 版本、ffmpeg 缺失,这些曾经让我头疼的破事,全都被隔离在容器外面了。哪怕以后换一台新服务器,我只需要把同一个 Dockerfile 拿过去重新构建一遍,下载环境就能完整复现,不会因为机器不同而“薛定谔地跑通跑不通”。如果你也有一台旧服务器需要处理高画质下载任务,我个人建议直接照这个方案试一次,省出来的时间绝对比折腾源码编译值太多了。