五条命令自建追番客户端:Docker部署与OAuth配置避坑

发布时间:2026/10/23 5:47:41
五条命令自建追番客户端:Docker部署与OAuth配置避坑 1. 先聊清楚自建一个追番客户端到底图什么动手敲命令前先把“我为什么要自建”这件事说透。Bangumi 这类番组数据库的开放接口本质是把“作品资料、播出日历、个人收藏进度”这三类数据以 HTTP 形式暴露出来官方网页端只是它的一个消费者。所谓“自建追番客户端”就是你自己跑一个前端 轻量后端的进程直接吃这套接口的数据自己的浏览器连自己的服务器看。标题里说的“五条命令跑起来”指的就是从拉代码到容器起来的这条最短路径我下面会一条一条拆给你看。它解决的问题很具体官方页面功能全但信息密度是按大众口味调过的你想看的东西经常要点三层才点得到你想把“本周还没看的、评分 8 分以上的、我看了一半的”这三个条件叠加筛一遍网页端做不到你想让自己和一起看番的三五个人共用一份自定义的季度清单也没地方放。自建客户端把这些都变成你自己说了算列表怎么排、卡片多大、评分怎么过滤、要不要显示声优和制作组、进度条点一下是不是直接标“看过”全都是配置文件里几行字的事。这篇适合谁看三类人。第一类是有台常年开机的小主机软路由、旧笔记本、云主机都行想让它在后台常驻一个服务全家或者小圈子访问第二类是想拿这个项目当练手靶子的前端或全栈因为它麻雀虽小五脏俱全——OAuth 授权、接口缓存、限流、容器化、反代一条龙走完第三类是纯粹被“追番记录散落在备忘录和 Excel 里”折磨过的人只想找个地方把看过的番、打分、短评统一存起来顺便看看别人怎么评价。我先把预期拉平五条命令能让你在十分钟内看到一个能打开的页面但从“能打开”到“天天愿意用”中间还有一串配置项要填、一串坑要踩。下面的内容按“为什么这么设计 → 每条命令在干嘛 → 配置怎么填 → 怎么验收 → 怎么上线 → 出事怎么查”的顺序推进你按顺序走基本不会卡住。2. 环境盘点与方案选型别急着敲命令2.1 三种部署形态先选一条路这类客户端项目一般同时提供三种跑法纯前端静态托管、Node 进程直跑、容器一键起。选哪种不是口味问题是约束问题。纯前端静态托管最省资源一台 1 核 512M 的机器都嫌多但它有个硬伤——OAuth 的client_secret必须放在服务端纯静态意味着你得把密钥写进前端代码等于把家门钥匙钉在门板上。除非项目方明确提供了“无后端模式”靠 PKCE 流程在浏览器端换 token否则我不建议走这条路。Node 进程直跑是中间态npm run build出静态产物再用项目自带的轻量服务端托管顺带处理登录回调和接口代理。优点是调试方便日志直接打印在终端缺点是你得自己解决守护进程、开机自启、崩溃重启这些问题。容器是最省心的尤其是项目提供了docker-compose.yml的时候——依赖版本、Node 运行时、目录挂载全在文件里写死了换台机器拷贝过去一样跑。我自己的选择是容器理由很朴素半年后我肯定忘了当初装的哪个 Node 版本、有没有全局装过什么包但容器不会忘。2.2 版本与资源几个绕不开的硬指标项目建议值说明Node 运行时20 LTS 及以上低于 18 会在构建阶段报 ESM 相关错误内存构建时 ≥1GB运行时 ≥256MB前端构建阶段最容易 OOM磁盘≥1GB 可用依赖目录加构建产物以及 SQLite 数据文件CPU1 核勉强够2 核舒服构建阶段吃满单核是常态端口默认 8080可改别和已有的 80/3000/8080 撞车内存这条我要专门强调。前端构建阶段的峰值内存占用经常被低估1 核 512M 的小机器跑到vite build或者webpack打包阶段直接被内核 OOM Killer 干掉终端上看到的只是Killed两个单词日志里什么都没留下很容易误判成代码 bug。真遇到这种情况两个办法一是临时加 swapfallocate -l 2G /swapfile那一套二是在构建命令前加NODE_OPTIONS--max-old-space-size768把 V8 的堆上限压下来——代价是构建变慢但不会挂。2.3 动手前先把三件事定下来第一件是访问入口。你是打算局域网 IP 直连http://192.168.x.x:8080还是要挂个域名走 HTTPS这个决定直接影响 OAuth 回调地址能不能填。绝大多数番组数据服务方在 OAuth 环节要求回调地址使用 HTTPS且要和注册应用时填的地址逐字符一致包括结尾有没有斜杠。第二件是数据落在哪。项目通常会把用户配置、缓存、收藏快照写进 SQLite 文件或者一个数据目录。这个目录必须在容器重启后还在所以一定要挂载出来不能用默认的容器内路径。第三件是谁能访问。只是自己用还是给朋友开账号如果是后者你需要在启动前就把ALLOW_REGISTER之类的开关关掉或者加一层基础认证——这类小项目默认往往是“第一个注册的人是管理员之后开放注册”直接暴露在公网上的话一晚上能给你注册出两百个陌生账号。提示先把这三件事写在便签上再往下走。我见过太多人兴冲冲跑完五条命令结果卡在“回调地址填什么”这一步然后回头重装。3. 五条命令逐条拆开看3.1 第一条拉代码git clone https://code.example.test/bgm/bgm-client.git cd bgm-client这条命令干两件事把仓库拉到本地把当前目录切进去。看起来最没技术含量但坑不少。如果你在国内网络环境下拉取速度很慢别急着换源先确认是不是仓库本身带了较大的历史提交或者 LFS 大文件。可以用git clone --depth1只拉最新一次提交速度通常能快一个数量级——代价是拿不到提交历史如果你打算跟着上游更新后面用git pull时再--unshallow补回来就行。切目录这一步也别偷懒。后面所有命令都是相对路径你少cd一次cp .env.example .env就会复制到一个莫名其妙的地方然后npm ci报“找不到 package.json”——这类错误九成是路径没对。拉下来之后先别急花三十秒做件事看一眼根目录有没有docker-compose.yml、有没有Dockerfile、README里写的 Node 版本要求是多少。这三个信息决定了你后面四条命令要不要改。3.2 第二条生成配置文件cp .env.example .env ${EDITOR:-vi} .envcp是复制模板后面的编辑器命令是打开它。我特意写成${EDITOR:-vi}是因为每个人习惯的编辑器不同你直接把${EDITOR:-vi}换成vim、nano、code都行。.env是整个部署里最关键的一个文件几乎所有“跑起来了但用不了”的问题都出在这里。下面这份是我实际用下来的一个完整样例字段名可能和你拿到的模板略有出入但语义是对应的# ---- 基础 ---- APP_PORT8080 APP_BASE_URLhttps://bgm.example.test TZAsia/Shanghai # ---- 上游接口 ---- API_BASEhttps://api.example-bgm.test API_UAbgm-client/0.1.0 (self-hosted; contact: youexample.test) API_TIMEOUT10000 # ---- OAuth ---- OAUTH_CLIENT_IDxxxxxxxxxxxxxxxx OAUTH_CLIENT_SECRETxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx OAUTH_REDIRECT_URIhttps://bgm.example.test/api/auth/callback OAUTH_SCOPEread:collection # ---- 会话与存储 ---- SESSION_SECRET换成你自己生成的随机串 DB_PATH./data/bgm.db CACHE_TTL21600 # ---- 限流 ---- RATE_LIMIT_QPS0.8TZAsia/Shanghai这行别删。番组数据里的播出时间是带时区的标准时间戳前端渲染时按本地时区转换。容器默认是 UTC你不设这个变量日历上“周三 24:30 播出”的番会显示成“周三 16:30”你会以为数据错了其实是时区差了八小时。SESSION_SECRET一定要换成随机值别用模板里的占位符。生成方法很简单openssl rand -hex 32把输出整串贴进去。这个值的作用是给会话 cookie 签名泄露了别人就能伪造登录态。3.3 第三条装依赖npm ci这里我特意用npm ci而不是npm install。两者的区别值得说清楚npm install会读package.json里的版本范围可能装到比作者当时更新的次版本还会顺手改写package-lock.jsonnpm ci严格按 lock 文件里锁死的版本装装完不改 lock而且会先删掉node_modules再重装保证环境干净。部署场景下要的就是这种确定性——作者测过的那套版本组合你一模一样地复现。如果你的机器内存紧张可以加参数跳过开发依赖npm ci --omitdev但注意有些项目把构建工具放在devDependencies里你跳过之后npm run build会直接报“找不到命令”。判断方法很简单看package.json里build脚本调用的是哪个命令如果是vite、webpack、tsc这类那它们大概率在 dev 依赖里就别跳过。真正需要--omitdev的场景是“我只想跑现成的构建产物不打算重新构建”。装依赖时如果卡在某个包上下载不动先看是不是源的问题。换成国内镜像通常能解决npm config set registry https://registry.npmmirror.com装完做个快速验证ls node_modules | wc -l看看数量是不是几百上千个如果只有几十个多半是装到一半失败了。3.4 第四条构建npm run build前端项目的构建就是把源码编译成浏览器能直接跑的静态文件输出到dist或.output目录。这一步是整条链路里最容易出问题的环节因为它同时要吃内存、吃 CPU、还要联网有些构建流程会去拉字体或者图标。构建时长取决于机器2 核的机器大概一到三分钟1 核的可能要五分钟往上。中间如果终端突然安静超过十分钟没有任何输出基本可以判定是卡住了CtrlC中断检查两件事内存够不够free -h看可用内存、有没有需要联网的步骤被墙在了半路。构建成功的标志是终端最后打印出类似 “built in 42.3s” 的字样并且dist目录里有index.html。你可以顺手验证一下ls -lh dist/ | head -20 du -sh dist/dist的体积一般在几百 KB 到几 MB 之间。如果只有几十 KB可能是构建配置有问题产物不完整。如果你用的是容器方案这一步其实可以跳过——docker compose build会在镜像里自动完成构建。手动跑一遍的好处是先在这里把错误暴露出来而不是等到容器里再排查日志更清晰。3.5 第五条起来docker compose up -d-d是后台运行。如果你没装 Docker用进程方式起也行npm start或者node .output/server/index.mjs具体看项目文档。起来之后立刻做一件事看日志。docker compose logs -f --tail50正常情况下你会看到几行启动信息包括监听的端口、数据库文件路径、以及类似 “server listening on 0.0.0.0:8080” 的提示。如果看到进程启动后立刻退出日志里最后一行是exit code 1往下翻几十行找真正的报错通常是环境变量缺了某一项或者.env文件里有中文全角符号。浏览器打开http://你的IP:8080能看到登录页或者首页五条命令就算走完了。4. 环境变量里的四个关键项填错了就是白跑4.1 User-Agent 不是可选项是门票番组数据服务方对第三方客户端的第一个要求往往就是请求头里必须带一个能识别出“这是个什么应用、出问题了找谁”的 User-Agent。格式上一般要求包含应用名、版本号以及一个能联系到你的地址项目主页、邮箱都行。API_UAbgm-client/0.1.0 (self-hosted; contact: youexample.test)这行就是这个作用。你若图省事留着默认值或者用浏览器 UA 伪装运气好能用一阵运气不好直接收到 403而且错误信息往往只说“forbidden”不会告诉你原因排查起来极其痛苦。我踩过一次本地调试时图快把 UA 写成了curl/7.88.1本地一切正常部署到服务器之后所有接口都返回 403。折腾了四十分钟才想起来是 UA 的问题——因为从服务器出口的请求被识别成了“脚本批量抓取”而本地走的是同一个出口 IP但请求频率低没触发。后来我把 UA 固定写在.env里并且在代码里加了一行启动时打印一眼就能看到当前生效的值是什么。注意改完.env之后一定要重启进程。很多项目只在启动时读一次环境变量你在运行中的容器里改文件是不生效的docker compose restart或者down up -d都行。4.2 OAuth 回调地址一个字符都不能错这是新手最容易卡住的地方我把它单独拎出来讲。OAuth 授权码流程是这样的你的客户端把用户重定向到服务方的授权页用户在那边点“同意”服务方把用户送回你事先登记过的回调地址并在 URL 上附带一个code参数你的服务端拿到code再在后台用自己的client_secret去换access_token。整个过程里回调地址起到了“身份证”的作用——服务方只肯把 code 交给登记过的地址。所以要保证三件事.env里的OAUTH_REDIRECT_URI和你在这类服务方后台登记的回调地址完全一致协议头http / https一致不要一个写 http 一个写 https结尾斜杠一致/callback和/callback/是两个不同的地址。我用过的错误组合包括本地调试填http://localhost:8080/api/auth/callback上线后忘了改成域名域名换了但服务方后台没更新反向代理把路径改写了导致实际到达的路径和登记的路径不同。这几种情况报错信息高度相似都是redirect_uri mismatch只能靠逐一比对排查。另外一个细节如果你的服务在 Nginx 后面一定要把X-Forwarded-Proto和X-Forwarded-Host透传过去否则服务端生成的跳转地址可能变成http://开头因为它以为自己是裸奔的 HTTP和登记的https://对不上。4.3 缓存 TTL 怎么定别靠感觉CACHE_TTL21600是六小时单位秒。这个值不是随便填的要按数据的变化频率来定数据类型变化频率建议 TTL理由作品基础资料极低7 天标题、简介基本不变每日放送日历每天24 小时按天更新用户收藏进度用户自己改5 分钟太长会出现“我明明标了看过”搜索结果高10 分钟不值得长缓存条目评分与评论数每天12 小时缓慢变化全局 TTL 只有一个值的时候取中间偏保守的六小时是个不错的默认。为什么因为缓存太短会把请求量全压到上游接口上容易触发限流缓存太长会让你自己看到过期数据体验很怪。4.4 限流参数宁可慢不要被封RATE_LIMIT_QPS0.8的意思是每秒最多发 0.8 个请求也就是平均 1.25 秒一个。大部分开放接口对单个 UA 或单个 IP 的速率限制在每秒 1 次左右我把本地限流设在 0.8留 20% 的余量。这个余量的意义在于你的服务内部可能有多个并发点同时需要数据首页日历、侧边栏收藏、详情页如果本地不加约束一瞬间就可能打出五六个请求正好撞到上游的窗口上。加了本地令牌桶之后请求会被排队、平滑地发出去用户侧感知到的只是一点点延迟而不是报错。如果上游返回了 429请求过多一个成熟的项目通常会读Retry-After响应头等待指定秒数再重试。你可以在配置里找找有没有相关的重试次数和退避参数把这个逻辑打开。没有的话至少把 QPS 再下调到 0.5。5. 首跑验收怎么确认它真的能用5.1 三层健康检查从上往下打第一层进程活着docker compose ps curl -sS -o /dev/null -w %{http_code}\n http://127.0.0.1:8080/返回 200 说明 HTTP 服务在听。返回 502 通常是反代配置问题上游根本没起来返回 000 是连接被拒绝端口不对或者服务没起。第二层后端能连通上游。多数项目会暴露一个健康检查接口或者调试接口curl -sS http://127.0.0.1:8080/api/health期望看到{status:ok,upstream:reachable,db:ok}之类。如果 upstream 显示不可达问题在.env的API_BASE或者 UA 上。第三层登录链路能走通。这是最容易被忽略的一层——前两层都绿了不代表 OAuth 能跑。真实验证方法是浏览器打开首页点登录跳转授权回到自己的站点右上角出现你的头像和用户名。这一圈走完说明 client_id、secret、回调地址、scope 全部匹配。如果卡在“授权后跳回来但没登录上”看服务端日志里有没有invalid_clientsecret 错了或者invalid_grantcode 过期通常是服务器时间和标准时间差太多。第二种情况在家用机器上很常见——主板电池没电导致时间漂移date一看差了十几分钟。5.2 数据落盘确认确认数据库文件真的写在你挂载的目录里ls -lh ./data/ sqlite3 ./data/bgm.db .tables如果能列出几张表说明初始化正常。如果./data/是空的而容器里明明有数据那就是挂载路径写错了——容器里的路径和宿主机的路径要一一对应写反了之后数据写进了容器内部docker compose down一删就全没了。我建议第一次启动之后立刻做个手工备份tar czf ~/bgm-backup-$(date %F).tar.gz ./data ./config养成这个习惯后面折腾配置的时候心里有底。5.3 页面级验收清单技术上通了还得看用起来顺不顺。我会按这个清单过一遍首页日历本周七天是否都有条目日期是否和本地时区对得上条目详情海报图能不能加载出来加载不出来的话多半是图片防盗链需要走图片代理搜索输入一个中文名和一个日文名各试一次收藏操作随便挑一个条目标记“在看”刷新页面看状态是否保持进度记录标记“看到第 3 集”再刷新确认移动端手机浏览器打开看卡片布局有没有横向溢出图片加载不出来是高频问题。番组数据服务方的图片资源通常做了防盗链浏览器直接请求时 Referer 是自己的站点会被拒。解决方案是让后端做一层图片代理请求时带上正确的 Referer 或者干脆去掉。项目配置里一般有IMAGE_PROXYtrue之类的开关打开它。6. 挂到公网反代、常驻与备份6.1 Nginx 反代的最小配置server { listen 443 ssl; http2 on; server_name bgm.example.test; ssl_certificate /etc/letsencrypt/live/bgm.example.test/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/bgm.example.test/privkey.pem; client_max_body_size 10m; location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; 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; proxy_set_header X-Forwarded-Host $host; proxy_read_timeout 60s; } }几个参数值得解释proxy_set_header Host $host保证后端拿到的是真实域名而不是127.0.0.1:8080这直接影响跳转地址的拼接X-Forwarded-Proto让后端知道用户是走 HTTPS 进来的client_max_body_size是给头像上传之类的功能留的余量默认 1M 有时候不够。再加一层访问控制会更稳。如果只是小圈子用在location里加auth_basic restricted; auth_basic_user_file /etc/nginx/.htpasswd;用htpasswd -c /etc/nginx/.htpasswd 用户名生成。这一层在应用自身的登录之外多挡一道防止哪天应用出了个注册漏洞被人刷。6.2 常驻与自动重启容器方案的常驻靠restart: always就够了在docker-compose.yml里确认这一行存在。进程方案则要写 systemd[Unit] Descriptionbgm-client Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple WorkingDirectory/opt/bgm-client EnvironmentFile/opt/bgm-client/.env ExecStart/usr/bin/npm start Restarton-failure RestartSec5 Userbgm [Install] WantedBymulti-user.targetRestarton-failure加RestartSec5的意思是不管什么原因挂了五秒后自动拉起来。这个组合能扛住绝大多数偶发崩溃比如数据库短暂锁住、内存瞬时飙高。EnvironmentFile指向.env是关键很多人忘了这一行导致服务起来之后所有环境变量都是空的表现就是“手动跑好好的用 systemd 跑就报错”。6.3 备份策略小项目更容易丢数据这类自建服务的数据库通常就一个 SQLite 文件备份成本极低但没有备份意识的人极多。我自己的做法是三条第一每天凌晨用sqlite3 ./data/bgm.db .backup ./backup/bgm-$(date %F).db做一次热备份。用.backup命令而不是直接cp是因为直接复制正在写入的数据库文件可能拿到一个损坏的副本。第二备份文件保留最近 14 天用find ./backup -name *.db -mtime 14 -delete清理。第三每月手动把备份包下载到本地一份。这一步不是不信任服务器是防“服务器整台没了”这种单点故障。7. 常见问题速查与踩坑记录7.1 一张表覆盖八成故障现象最可能的原因排查动作启动后立即退出日志 exit 1.env缺字段或有全角字符逐行比对外壳变量名检查冒号括号页面白屏控制台一堆 401会话密钥变了或没登录检查SESSION_SECRET重登一次所有接口 403UA 不合规或被识别为爬虫换成规范的应用名/版本 (联系方式)授权后跳回但未登录回调地址不一致逐字符比对注意协议与结尾斜杠图片全是灰块防盗链打开图片代理开关日历时间差 8 小时容器时区是 UTC加TZAsia/Shanghai并重启构建阶段被 Killed内存不足加 swap 或限 V8 堆内存database is lockedSQLite 并发写开 WAL 模式PRAGMA journal_modeWAL反代 502上游没起或端口不对curl本机端口确认收藏状态改不了scope 未包含写权限重新授权勾选写收藏范围7.2 三个只在实战里才会遇到的坑坑一改了代码不改 lock 文件重新构建就崩。有一次我想给页面加个自定义排序按钮直接在本地npm install了个拖拽库测试没问题就推上去了。结果服务器上跑npm ci直接报错“lock 文件不同步”——因为npm ci要求 lock 和package.json严格一致而我只改了后者。正确的做法是本地npm install 包名让 lock 自动更新然后把package.json和package-lock.json一起提交。坑二容器里的时间不是你以为的时间。表现是日志时间戳全是 UTC和自己的作息对不上排查问题时经常算错时间窗口。除了设TZ还要确认镜像里是否装了tzdata精简镜像比如 alpine 系的默认不带时区数据库光设TZ变量是没用的时间还是一样的。要么在 Dockerfile 里补装要么把宿主机的/etc/localtime和/etc/timezone挂进容器。坑三升级上游版本时数据被“迁移”没了。这类项目迭代快某次更新可能改了数据库表结构启动时自动跑迁移脚本。正常情况下没问题但如果迁移脚本写得不严谨老数据有可能丢字段。所以升级前一定先备份./data目录这是我用血换来的教训——有一次升级完发现所有条目的“看到第几集”全清零了重建花了小半个晚上。7.3 几个让体验更好的小调整把常用筛选做成书签。项目如果支持 URL 查询参数大部分都支持你可以把“在看 评分大于 8 按更新时间排序”存成浏览器书签点一下直接到结果页比每次点三个下拉框舒服得多。改一下首页默认视图。大多数人打开这类客户端的第一诉求是“本周还有什么没看”所以把默认首页设成日历视图比设成时间线更实用。这个在配置文件或者设置页里通常有一项DEFAULT_VIEW。关掉不需要的模块。有些项目会默认开启“排行榜”“猜你喜欢”“社区动态”这些模块每个都要额外发请求。你自己用的话把不看的模块关掉首页加载能快不少也少消耗上游的限流额度。给自己的服务器加个监控。一个简单的 cron 每分钟curl一次健康检查接口连续三次失败就发个通知给自己比被动发现“打不开了”要主动得多。8. 最后说点实在的这套东西我从第一次跑到真正天天用前后大概折腾了一个周末。五条命令确实能跑起来但要让它变成“顺手”的工具中间那些配置项的思考是绕不过去的——UA 怎么写、回调地址怎么对、TTL 怎么定、限流留多少余量这些没有标准答案只能按自己的使用节奏调。我现在用的是最朴素的一套一台常年开机的小主机容器部署Nginx 反代加基础认证SQLite 每天热备一次。平时上班摸鱼的时候点开看一眼本周日历晚上回家顺手把看完的标一下。它不完美界面比不上商业产品精致但每一条数据都在自己手里导得出来、搬得走、改得动这个感觉是租来的服务给不了的。如果你卡在某一步优先看日志其次是看.env最后才是怀疑代码。按我的经验八成的问题出在前两步剩下两成里有一大半是网络和时区。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询