家庭影音直链播放系统搭建:alist+Emby+Docker实战

家庭影音直链播放系统搭建:alist+Emby+Docker实战
1. 这不是“私有云”是家庭数字资产的自主播放中枢很多人一看到“家里云”三个字第一反应就是搭个NAS、装个群晖套件、开个SMB共享——结果呢手机点开视频要等30秒转码电视端播4K片源卡成PPT孩子想看的动画片得先下载到平板再传过去。这哪是“云”这是“晕”。我折腾了三年多从最早用树莓派硬解H265失败到后来在旧笔记本上跑Docker被风扇吵得睡不着最后才明白家庭影音服务的核心矛盾从来不是存储容量或带宽大小而是“内容路径”的可控性与确定性。所谓“直链播放”说白了就是让Emby服务器不经过任何中间转码、不走代理缓存、不调用第三方CDN直接把原始文件地址比如你存在115网盘里的MKV链接、阿里云OSS里的MP4地址、甚至本地NAS上某个目录的HTTP可访问路径原封不动地推给播放器。播放器拿到的就是真实URL它自己决定要不要缓冲、要不要硬解、要不要跳过片头——这才是真正的“自主权”。而alist就是这个链条里最关键的“地址翻译官”它不存文件只管把各种云盘、对象存储、本地路径统一翻译成标准HTTP链接Emby再通过插件或反向代理把这些链接识别为“本地媒体”从而绕过所有转码逻辑。关键词里反复出现的docker-compose不是为了装逼而是因为这套组合对家庭环境极其友好——它把Emby、alist、nginx反向代理、甚至MySQL如果需要元数据持久化全部打包成一个可复现的配置文件。你换台机器只要装好Dockerdocker-compose up -d一条命令整个系统就活了。不像宝塔面板那种图形化方案看着简单出问题时日志散落在七八个地方排查起来像大海捞针。我见过太多人卡在“docker-compose up 报错 unable to get image mysql:8.0.34”这一步其实根本不是镜像拉不到而是Docker daemon没启动、或/etc/docker/daemon.json里配置了国内镜像源但网络不通、又或者docker-compose.yml里写的镜像标签压根不存在比如写成了mysql:8.0.34但Docker Hub上实际只有mysql:8.0或mysql:8.0.33。这些细节恰恰是“能跑通”和“能长期稳住”的分水岭。所以这篇内容不讲怎么注册115账号、不教Emby Premiere订阅怎么买、更不会告诉你“alist可以win7吗”——Win7早已停止支持连Docker Desktop都不兼容强行折腾只会掉进驱动冲突、WSL版本错配、Hyper-V无法启用的三重坑。我们要解决的是当你已经有一台能跑Linux的旧电脑、一个115会员、一份存了上千部电影的资源库如何用最轻量、最可控、最不易崩的方式让客厅电视、孩子iPad、你自己的MacBook点开就能播且画质声音完全不打折。后面所有操作都围绕这个目标展开。2. alist不是“网盘挂载器”而是HTTP协议层的地址路由器很多新手把alist当成“另一个网盘客户端”装完就急着点“添加存储”填115账号密码然后发现Emby里啥也没扫出来或者扫出来全是乱码文件名。这就错了。alist的本质不是帮你“登录网盘”而是为你已有的存储资源生成一套标准化、无状态、可预测的HTTP访问路径。它不关心你存的是115、阿里云OSS还是家里的Samba共享只关心一件事当浏览器或Emby发来一个GET请求比如http://alist:5244/115/电影/阿凡达.mp4alist能不能在100毫秒内把这个请求准确转发到115的真实下载链接并返回302重定向让播放器直接去那个链接拿数据。这就决定了alist的配置核心从来不是“账号密码”而是存储策略Storage Policy与路径映射Path Mapping的精确设计。以115为例官方文档推荐用“115 WebDAV”方式接入但实测下来WebDAV在alist里稳定性极差上传大文件易中断、目录刷新延迟高、遇到特殊字符文件名直接报错。真正稳的方案是用115官方提供的“离线下载直链”能力——你手动在115网页端把资源离线到“alist专用目录”然后在alist里添加“115 Cookie”存储填入你浏览器F12抓包拿到的cookie字段注意是完整SESSDATAxxx; 115_langzh; ...字符串不是单个值并设置root_folder为你离线到的那个目录ID比如123456789。这样alist每次请求都是带着有效会话去115后台拉取直链成功率接近100%。再来看路径映射的关键细节。alist默认生成的链接是http://alist:5244/d/115/电影/阿凡达.mp4这个/d/前缀是“下载模式”会触发alist内部下载再吐出完全违背“直链”初衷。必须改成/p/前缀即“预览模式”它才会返回302跳转到115原始直链。但Emby默认不认/p/开头的路径怎么办两个方案方案A推荐在alist的settings.json里把site_url设为http://your-domain.com你的域名再把cdn_url设为https://cdn.your-domain.comCDN加速域名然后在Nginx反向代理里把所有/p/开头的请求rewrite成/d/并加X-Forwarded-For头同时把响应头Location里的/d/替换成/p/。听起来复杂其实就三行Nginx配置location /p/ { proxy_pass http://alist:5244/d/; proxy_set_header X-Forwarded-For $remote_addr; sub_filter /d/ /p/; sub_filter_once off; }方案B简化版直接在Emby后台的“媒体库”设置里把路径填成http://alist:5244/p/115/然后安装Emby插件“Emby AList Plugin”它会自动把/p/路径转换为Emby可识别的媒体源。但插件更新慢遇到alist新版本常有兼容问题属于“能用但不推荐长期依赖”。提示alist的/p/模式返回302跳转对播放器有要求。iOS上的Infuse、nPlayer完全支持安卓的MX Player需开启“网络流媒体”选项TV端的Kodi需安装inputstream.adaptive插件。如果你家电视盒子是老款晶晨芯片建议优先测试/d/模式Emby转码虽然牺牲画质但兼容性无敌。3. Emby的“直链播放”不是开关一开就成而是元数据、路径、权限的三重校准Emby界面右上角那个“直链播放”开关90%的人以为打开它所有视频就自动走直链了。错。这个开关只是“允许Emby尝试直链”真正的执行权在三个更底层的地方媒体库路径配置、文件系统权限、以及播放器客户端的能力协商。先说媒体库路径。Emby扫描媒体时必须把alist暴露的HTTP路径当作“本地文件路径”来处理。比如alist的http://alist:5244/p/115/对应你115里的“电影”文件夹那Emby新建媒体库时“文件夹路径”必须填http://alist:5244/p/115/而不是/mnt/115/那是alist容器内部的挂载点Emby根本访问不到。这里有个致命陷阱Emby的Web界面会自动把http://开头的路径识别为“远程路径”并禁用“直链播放”选项。破解方法是——用Emby的API手动创建媒体库。用curl发个POST请求curl -X POST http://emby:8096/emby/Library/MediaFolders \ -H X-Emby-Token: your_api_key \ -H Content-Type: application/json \ -d { Name: 115电影, Path: http://alist:5244/p/115/, NetworkPath: http://alist:5244/p/115/, EnabledForVideo: true, RefreshIntervalDays: 1 }这个API调用会强制Emby把HTTP路径当成本地路径处理直链开关才能生效。再说文件系统权限。Emby容器默认以uid998(gid998)运行而alist容器里文件的owner是root。如果alist没正确配置umask或gidEmby在扫描时会因“Permission Denied”跳过大量文件。解决方案是在docker-compose.yml里给alist服务加上services: alist: # ...其他配置 environment: - UID998 - GID998 # 或者更彻底用user: 998:998这样alist生成的所有HTTP响应头都会带上正确的Content-Length和Last-ModifiedEmby才能准确判断文件是否已变更、是否需要重新扫描。最后是播放器能力协商。Emby不是把直链URL直接扔给播放器而是先发一个HEAD请求检查目标URL的Content-Type必须是video/mp4、video/x-matroska等、Accept-Ranges必须是bytes、Content-Length不能为*。如果115直链返回Content-Type: application/octet-streamEmby就会放弃直链切回转码。这时就得改alist的storage配置在115存储的driver参数里加上mime_type_mapping: {mkv: video/x-matroska, mp4: video/mp4}强制指定MIME类型。注意Emby Premiere订阅解锁的“直链播放”功能本质是解除播放器端的DRM限制。如果你用的是免费版Emby即使后端配置全对iOS端Infuse仍可能因Apple ATS安全策略拒绝加载HTTP直链必须HTTPS。此时必须用Nginx做SSL终止把https://emby.your-domain.com/videos/反向代理到http://alist:5244/p/115/让播放器看到的永远是HTTPS链接。4. docker-compose不是魔法咒语是服务依赖、网络隔离与健康检查的精密编排网上流传的docker-compose.yml模板十有八九是直接复制粘贴的“能跑就行”版本alist、Emby、MySQL全堆在一个default网络里depends_on只写服务名restart: always一加了事。结果就是——某天alist升级到v3.30MySQL突然连不上Emby疯狂刷database connection refused日志全家影音瘫痪两小时。这不是运气差是docker-compose的依赖逻辑被严重误读。depends_on在Docker Compose v2中只控制容器启动顺序不保证服务就绪。比如depends_on: [mysql]只表示alist容器会在mysql容器start之后才start但mysql容器start完成时MySQL服务可能还在初始化InnoDB引擎根本没监听3306端口。正确做法是用healthcheck定义服务就绪标准并用condition: service_healthy替代depends_on。以MySQL为例services: mysql: image: mysql:8.0.33 # 注意别写8.0.34Hub上没有这个tag healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -u, root, -p$$MYSQL_ROOT_PASSWORD] timeout: 20s retries: 10 start_period: 40s然后在alist服务里alist: # ...其他配置 depends_on: mysql: condition: service_healthy这样alist容器启动前会等MySQL健康检查通过10次确保服务真正可用。再看网络隔离。把alist、Emby、MySQL全放在default网络看似方便实则埋雷。比如alist的Web管理界面默认开放5244端口如果宿主机防火墙没关严公网IP5244就能直接访问alist后台——等于把你的115 Cookie裸奔在互联网上。正确姿势是划分三个独立网络——frontend供Nginx和外部访问、backendalist与Emby通信、database仅MySQL与alist通信。docker-compose.yml里这么写networks: frontend: driver: bridge backend: driver: bridge database: driver: bridge services: nginx: networks: [frontend] emby: networks: [frontend, backend] alist: networks: [backend, database] mysql: networks: [database]这样alist的5244端口只在backend网络内可达Emby通过http://alist:5244调用外部完全无法触达。最后是健康检查的实战价值。我曾经遇到alist容器内存泄漏RSS涨到2GB后卡死但docker ps显示状态仍是Up 2 days。加了健康检查后docker ps里alist状态变成Unhealthy配合restart: on-failureDocker自动重启容器整个过程无需人工干预。健康检查脚本就一行alist: healthcheck: test: [CMD, curl, -f, http://localhost:5244/ping] interval: 30s timeout: 10s retries: 3/ping接口是alist内置的轻量健康检测毫秒级响应比查数据库靠谱多了。提示docker-compose up -d的意思是“detached mode”后台运行。但很多人执行后立刻去浏览器访问发现打不开就以为失败了。其实容器启动需要时间特别是MySQL首次初始化要30秒以上。正确做法是docker-compose up -d docker-compose logs -f --tail100实时看日志直到出现mysqld: ready for connections和alist: server started才算真正就绪。5. 从“能播”到“好播”的临门一脚Nginx反向代理的七层流量调度当alist和Emby都跑起来了你可能会发现用http://alist:5244/p/115/阿凡达.mp4在浏览器里能直接下载但Emby里点播放却黑屏或者手机App里能播但电视盒子上提示“无法连接服务器”。这些问题的根源往往不在alist或Emby本身而在HTTP请求的七层特征被中间设备篡改。比如运营商劫持、企业防火墙过滤、甚至某些智能路由器的QoS策略会把Range: bytes0-这种分片请求给截断导致播放器无法seek、无法缓冲。这时候Nginx反向代理就不是“可选项”而是“必选项”。Nginx在这里干三件事SSL终止把HTTPS请求解密再以HTTP发给alist/Emby避免证书链问题Header净化移除可能触发运营商劫持的User-Agent、Accept-Encoding等头Range请求透传确保Range头100%原样转发这是4K视频流畅播放的生命线。一个生产级的Nginx配置长这样upstream alist_backend { server alist:5244; } upstream emby_backend { server emby:8096; } server { listen 443 ssl http2; server_name emby.your-domain.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; location /videos/ { proxy_pass http://alist_backend/p/115/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 关键透传Range请求 proxy_http_version 1.1; proxy_set_header Connection ; proxy_buffering off; proxy_cache off; proxy_redirect off; # 防劫持强制清除可能被篡改的头 proxy_hide_header X-Powered-By; proxy_hide_header Server; } location / { proxy_pass http://emby_backend/; # Emby的WebSocket需要特殊处理 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } }这段配置里/videos/路径专门代理alist的直链/路径代理Emby主服务。重点看proxy_buffering off和proxy_cache off如果开启缓冲Nginx会先把整个视频文件缓存到内存再吐给播放器这完全违背“直链”初衷且内存爆炸。proxy_http_version 1.1和Connection 则是为了正确处理HTTP/1.1的keep-alive和Range分片。更关键的是这个配置让所有直链URL变成https://emby.your-domain.com/videos/阿凡达.mp4。为什么非要用域名因为iOS/iPadOS强制HTTPSHTTP直链会被ATS拦截某些安卓盒子的WebView内核对HTTP混合内容HTTPS页面加载HTTP视频有严格限制域名可绑定CDN把115直链流量卸载到边缘节点减轻alist服务器压力。CDN配置极其简单在Cloudflare或腾讯云CDN控制台把emby.your-domain.com的/videos/*路径源站设置为你的Nginx服务器IP缓存规则设为“不缓存”然后在Nginx里加一行location /videos/ { # ...前面的配置 # CDN回源时用真实IP而非CDN IP set_real_ip_from 103.21.244.0/22; real_ip_header CF-Connecting-IP; }这样alist日志里看到的就是用户真实IP方便后续做访问统计或限速。实操心得第一次配Nginx反代务必用curl -I -H Range: bytes0-1023 https://emby.your-domain.com/videos/阿凡达.mp4测试。如果返回206 Partial Content和Content-Range: bytes 0-1023/1234567890说明Range透传成功如果返回200 OK和Content-Length: 1234567890说明Nginx把Range头吃掉了得检查proxy_http_version和Connection配置。6. 真正的“逐步优化”是监控、日志与故障自愈的闭环建设标题里“逐步优化”四个字最容易被忽略。很多人以为优化就是换个更快的硬盘、升级alist到最新版、或者给Emby开更多转码线程。但真正的优化是建立一套可观测、可追溯、可自愈的运维闭环。没有这个闭环今天调通的配置明天一次alist升级就全崩没有这个闭环你永远在“救火”而不是“防火”。第一步日志集中化。docker-compose logs只能看实时输出历史日志全丢在/var/lib/docker/containers/里找起来像考古。必须把所有服务日志统一推送到ElasticsearchKibana。最轻量的方案是用fluentd作为日志收集器在docker-compose.yml里加一个fluentd服务所有其他服务的logging配置指向它services: fluentd: image: fluent/fluentd:v1.16-1 volumes: - ./fluentd/conf:/fluentd/etc ports: - 24224:24224 alist: logging: driver: fluentd options: fluentd-address: localhost:24224 tag: alist然后在fluentd/conf/fluent.conf里定义解析规则source type forward port 24224 /source filter alist type parser key_name log parse type json /parse /filter match ** type elasticsearch host elasticsearch port 9200 logstash_format true /match这样alist的每条日志都会带上tag: alist、container_id、timestamp在Kibana里搜alist AND error5秒定位问题。第二步关键指标监控。光看日志不够得量化。用PrometheusGrafana监控三类指标alistalist_storage_request_total{status200}成功请求数、alist_storage_request_duration_seconds_bucket响应延迟分布Embyemby_session_active活跃会话数、emby_transcode_queue_length转码队列长度Nginxnginx_http_request_duration_seconds_bucket{le1}1秒内响应占比、nginx_http_requests_total{code~5..}5xx错误率。这些指标靠prometheus-node-exporter是拿不到的得用各服务的exporteralist自带/metrics端点v3.28Emby需装emby-prometheus-exporter插件Nginx用nginx-vts-exporter。第三步故障自愈。当监控发现alist_storage_request_duration_seconds_bucket{le5} 0.9595%请求超5秒自动触发告警并执行修复脚本#!/bin/bash # check_alist_health.sh if ! curl -sf http://alist:5244/ping | grep -q pong; then echo $(date): alist unresponsive, restarting... /var/log/alist-autoheal.log docker-compose restart alist sleep 30 # 重启后验证 if ! curl -sf http://alist:5244/ping | grep -q pong; then echo $(date): alist restart failed, alert admin /var/log/alist-autoheal.log # 发送微信/邮件告警 fi fi这个脚本用cron每5分钟跑一次比等用户投诉再处理效率高10倍。最后分享一个血泪教训某次alist升级后/p/路径返回的302跳转Location头里带了?signxxx参数但Emby解析时把?后面全截掉了导致直链失效。查了3小时日志才发现是alist的sign_query配置项默认开启。关掉它sign_query: false问题立解。所以“逐步优化”的终点不是功能多强大而是每一次变更都有对应的监控、日志、回滚预案。这才是家庭影音系统真正“稳如磐石”的根基。