大部分远程 Emby 的线路都在国外,国内连接基本都需要代理,在家里电视上看视频还得单独开代理,不太方便。于是买了一台 9929 线路的服务器,用 Nginx 做了一层反向代理:图片和静态资源留在本机缓存,视频音频直接 302 给客户端,另外用一个开关文件在「直连」和「本机中转」之间切换。这里把当时的配置记一下;加开关主要是为了在 VPS 流量不够的时候切回 Emby 自己的线路。
整体思路#
下面的配置里,emby.example.com 是我们自己的域名,https://origin.example.net 是 Emby 的登录地址,https://video.example.net 是实际播放地址(可能跟登录地址一样,也可能不一样),Nginx 按请求类型分流:
| 请求 | 处理方式 |
|---|---|
/emby/**/Images/** 图片资源 | 本机反代,写入本地缓存 |
/videos/**、/audio/** | 302 到源站,本机不转发字节,有开关配置 |
| 其余 UI/API | 本机反代 |
| 存在开关文件 | 全部改为本机转发,并改写上游返回的地址 |
另外还有两个统一处理:把 Dart、axios 这类 UA 换成普通浏览器 UA——有的站点会限制 UA,之前折腾一直有问题,就是部分客户端的 UA 被 Emby 站点拦截了;反代时把客户端的 IP 头抹掉。
完整配置#
配置里的域名、证书路径和日志路径都换成了占位符:
# ==========================================
# UA 动态替换规则 (必须写在 server 块外面)
# ==========================================
map $http_user_agent $safe_user_agent {
# 默认情况:保留客户端原始的 UA
default $http_user_agent;
# 如果 UA 里包含 "Dart" (不区分大小写),就强行替换成标准 Windows Chrome 的 UA
"~*Dart" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
"~*axios" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
}
# keys_zone=emby_images:50m 划分 50M 内存存索引,
# max_size=2g 限制硬盘最多用 2G,30 天不访问清理
# 注意:父目录不存在或权限不对时,启动会报 mkdir() 失败;保险起见先创建:mkdir -p /var/cache/nginx/emby_cache
proxy_cache_path /var/cache/nginx/emby_cache levels=1:2 keys_zone=emby_images:50m max_size=2g inactive=30d use_temp_path=off;
server {
server_name emby.example.com;
gzip on;
# logging
access_log /var/log/nginx/emby.example.com.access.log;
error_log /var/log/nginx/emby.example.com.error.log warn;
# proxy_pass 用变量时,域名是请求时才解析的,必须配 resolver;
# 如果 proxy_pass 直接写死域名,Nginx 会在启动/重载时解析一次,DNS 失败就起不来,
# 同机的其他站点会一起挂(遇到过几次)。变量 + resolver 则能跟着 DNS 变化自动更新。
resolver 1.1.1.1 8.8.8.8 223.5.5.5 valid=30s ipv6=off;
resolver_timeout 5s;
location = / {
return 502;
}
client_max_body_size 5000M;
proxy_cache off;
proxy_redirect off;
# 安全头设置
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# ==========================================
# 0. 静态图片专属缓存(保持原样,流量很小)
# ==========================================
location ~* /emby/.*(/Images/|/web/.*\.(css|js|woff|woff2|png|jpg|jpeg|svg|webp|ico)) {
set $backend "origin.example.net:443";
proxy_pass https://$backend;
proxy_http_version 1.1;
proxy_set_header Host origin.example.net;
proxy_ssl_server_name on;
# 开启本地缓存逻辑
proxy_cache emby_images; # 调用最顶端命名的缓存区
proxy_cache_valid 200 304 30d; # 对状态码为 200 的图片在 VPS 本地缓存 30 天
proxy_cache_key $uri$is_args$args; # 设置缓存的唯一标识
proxy_cache_lock on; # 多人同时请求同一张海报时只回源一次
# 强制无视上游的防缓存头
proxy_ignore_headers Cache-Control Expires Set-Cookie;
# 客户端缓存下发
add_header Cache-Control "public, max-age=31536000, immutable";
proxy_hide_header Pragma;
proxy_hide_header Expires;
proxy_hide_header Cache-Control;
add_header X-Cache-Status $upstream_cache_status;
proxy_set_header User-Agent $safe_user_agent;
}
# ==========================================
# 1. 专属视频代理
# 直连模式下客户端不会再指向这里,保留着不影响;中转模式下才会被引用
# 下面的 video.example.net:443 可能跟你代理的 origin.example.net:443 不一样,
# 大部分 Emby 站点,浏览用的域名和播放线路并不是同一个,需要自己去找实际地址;
# 可以开个代理,播放时看哪个域名在跑流量
# ==========================================
location /video-proxy/ {
set $video_backend "video.example.net:443";
rewrite ^/video-proxy/(.*)$ /$1 break;
proxy_pass https://$video_backend;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_set_header User-Agent $safe_user_agent;
proxy_set_header Host video.example.net;
# 抹除 IP
proxy_set_header X-Real-IP "";
proxy_set_header X-Forwarded-For "";
proxy_set_header CF-Connecting-IP "";
# 断点续传与无缓冲优化
gzip off;
proxy_set_header Accept-Encoding "";
proxy_buffering off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Headers "Range" always;
add_header Access-Control-Expose-Headers "Content-Length, Content-Range" always;
}
# ==========================================
# 2. 主站反代 —— 直连模式(默认,省流量)
# ==========================================
location / {
# 同一个开关:存在开关文件 → 中转模式(本机转发,原行为)
error_page 418 = @emby_vps;
if (-f /etc/nginx/emby_vps_proxy.on) {
return 418;
}
# 直连模式:视频/音频请求直接 302 到远程,本机不转发任何字节
# 客户端自己去找远程,远程该跳线路就跳线路
if ($request_uri ~* "^/(emby/)?(videos|audio)/") {
return 302 https://origin.example.net$request_uri;
}
# 其余请求(UI / API / JSON 等小流量)继续反代
set $backend "origin.example.net:443";
proxy_pass https://$backend;
proxy_http_version 1.1;
proxy_set_header Host origin.example.net;
proxy_ssl_server_name on;
proxy_set_header User-Agent $safe_user_agent;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Real-IP "";
proxy_set_header X-Forwarded-For "";
proxy_set_header CF-Connecting-IP "";
gzip off;
proxy_set_header Accept-Encoding "";
proxy_buffering off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
}
# ==========================================
# 3. 主站反代 —— 中转模式(仅当开关文件存在时生效)
# 即原来的行为:把视频流量拉回本机走 /video-proxy/
# ==========================================
location @emby_vps {
set $backend "origin.example.net:443";
proxy_pass https://$backend;
proxy_http_version 1.1;
proxy_set_header Host origin.example.net;
proxy_ssl_server_name on;
proxy_set_header User-Agent $safe_user_agent;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Real-IP "";
proxy_set_header X-Forwarded-For "";
proxy_set_header CF-Connecting-IP "";
gzip off;
proxy_set_header Accept-Encoding "";
proxy_buffering off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
# 3. 拦截 302 重定向并指向专属线路二代理
proxy_redirect https://video.example.net:443/ /video-proxy/;
proxy_redirect https://origin.example.net:443/ /;
# 4. JSON 替换 (处理硬编码在 PlaybackInfo 中的地址)
sub_filter_types application/json text/plain;
sub_filter_once off;
sub_filter 'https://video.example.net:443' 'https://emby.example.com/video-proxy/';
sub_filter 'https://origin.example.net:443/' 'https://emby.example.com/';
}
# 返回 robots.txt 内容
location = /robots.txt {
add_header Content-Type text/plain;
return 200 "User-agent: *\nDisallow: /\n";
}
listen 443 ssl;
http2 on;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
if ($host = emby.example.com) {
return 301 https://$host$request_uri;
}
server_name emby.example.com;
listen 80;
return 404;
}nginx几个关键点#
UA 替换#
map 必须写在 server 块外层,default 保留客户端原始 UA,命中 Dart 或 axios 时替换成桌面版 Chrome。后面所有 proxy_set_header User-Agent $safe_user_agent; 都直接复用这个变量。
图片缓存#
proxy_cache_path 划分了 50M 的索引区和 2G 的磁盘上限,30 天不访问自动清理。图片 location 里用 proxy_ignore_headers 忽略上游的 Cache-Control、Expires 和 Set-Cookie,再用 proxy_cache_valid 固定缓存 30 天;proxy_cache_lock on 保证同一张海报并发回源时只打一次上游。
直连与中转开关#
if (-f /etc/nginx/emby_vps_proxy.on) 检查开关文件,存在就 return 418,由 error_page 418 = @emby_vps; 转到中转用的命名 location。if 里只放 return,是 Nginx 里少数可以放心使用的写法。开关文件本身只是个普通文件,直接 touch /etc/nginx/emby_vps_proxy.on 创建即可:有文件就走本机中转,没有就按 Emby 原本的线路走。
直连模式下,/videos/ 和 /audio/ 直接 302 到源站,其余请求才反代。
中转模式#
中转时会改写上游返回的地址:proxy_redirect 处理 Location 头,sub_filter 处理 JSON 里硬编码的绝对地址。sub_filter 默认只作用于 text/html,所以要显式加上 application/json text/plain;同时反代时统一 Accept-Encoding "",避免上游返回 gzip 导致替换失效。
几个当时踩到的坑#
- 没配
resolver/ DNS 解析失败导致 Nginx 起不来:proxy_pass直接写死域名时,Nginx 只在启动和重载时解析一次。那段时间上游 DNS 出问题,重启 Nginx 直接报host not found in upstream,整台机器上的站点一起挂掉,遇到过好几次。改成「变量 +resolver」后,解析推迟到请求时,valid=30s还能跟着 DNS 变化走;但变量形式下如果忘了写resolver,请求会直接 502,error.log 里报no resolver defined to resolve ...。 - UA 不对会被上游拦:部分客户端用的是
Dart、axios这类 UA,有的 Emby 站点会直接拒绝,表现是接口 403、海报空白或者播放失败,日志里看不出什么线索,当时排查了半天才想到去抓请求头。最后用map把这些 UA 统一换成桌面版 Chrome,问题才消失。 add_header是全有或全无:只要某个 location 自己写了任意一条add_header,server 级的add_header就全部不继承。图片缓存和/video-proxy/这两个 location 上的 HSTS、X-Frame-Options等安全头其实是丢掉的,需要重新补一遍。:443写死:proxy_redirect和sub_filter的匹配串里带了默认端口,上游如果生成的是不带端口的 URL 就匹配不上,两种写法最好都加上。sub_filter的替换串:匹配串没带结尾斜杠、替换串带了,遇到/emby/...会拼出双斜杠。默认merge_slashes on会兜住,但写全更稳妥。- 缓存和 gzip:图片缓存 location 里忘了清
Accept-Encoding,客户端带 gzip 时可能把 gzip 变体写进缓存,之后碰到不支持 gzip 的客户端就会出问题。 - 302 与 POST:如果把 API 也改成 302,多数客户端会把 POST 降级成 GET,登录和播放进度上报会失败;只对媒体路径做 302 更安全。
最后#
这套配置的思路不复杂:能让客户端直连的就直连,必须经过本机的才经过本机。图片体积小、请求量大,放本地缓存收益最明显;视频动辄几个 G,302 出去能省下大部分流量。开关文件则留了一个后手,线路出问题时不用改配置,touch 一下就能切回中转。