Black Candy 常用命令与运维实战

Black Candy v3.2.1 运维实战:compose 服务管理、媒体同步三入口、用户/密码管理、rails CLI(runner/console/db:prepare/about/routes)与移动端 API(Bearer 会话令牌)实测、WAL checkpoint 备份与恢复闭环演练,排障实录沉淀。

Black Candy流媒体音乐自托管Rails2026/10/28

Black Candy 常用命令与运维实战

‍‍​​‌​> 目标:沉淀 Black Candy v3.2.1 的日常运维命令体系——服务管理、媒体库同步、用户与认证管理、Rails CLI 深入、移动端 API 实测、备份与恢复闭环。全部命令在 Ubuntu 22.04 + Docker Engine 29.9.2 实测,回显逐行照抄。部署与配置基础见《Black Candy Docker Compose 部署实战》与《Black Candy 配置参数详解》。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64),Docker Engine 29.9.2 / Compose v5.6.1
  • Black Candy:v3.2.1(映像 ghcr.io/blackcandy-org/blackcandy:latest,Rails 8.1.3 / Ruby 4.0.2+YJIT / SolidQueue-1.1.5)
  • 服务端口:8136(容器内 80);编排目录 /opt/black-candy/(项目名 black-candy-v3.2.1-stack)
  • 曲库现状:3 张专辑 / 6 首测试曲(128 kbps mp3、30 秒/首);用户 2 个(管理员 admin@admin.com,密码已改为 q1w2e3r4;普通用户 listener@test.local)
  • 媒体目录:宿主 /opt/black-candy/media_data ↔ 容器 /media_data;数据目录:宿主 /opt/black-candy/storage_data ↔ 容器 /rails/storage

二、前置条件

  1. 服务栈已按部署篇就绪并可登录(docker compose ps 状态为 Up);
  2. 运维操作的两类入口:
docker exec black-candy-v3.2.1-app bin/rails <命令>   # Rails CLI 类(db/runner/console 等)
curl http://127.0.0.1:8136/...                        # HTTP 类(Web 表单/API)

💡 镜像内没有 sqlite3 命令行工具(实测 which sqlite3 返回 NO_SQLITE3_CLI),也没有 psql 等客户端——数据核查一律走 bin/rails runner,见「五、CLI 深入」。

三、命令速查表

场景命令
实例状态/端口docker compose ps
跟踪日志docker compose logs -f app
重启/停止/启动docker compose restart / stop / start
数据库结构就绪检查bin/rails db:prepare(无输出即就绪)
迁移版本bin/rails db:version
运行时环境速览bin/rails about
任务清单bin/rails -T
任意数据查询bin/rails runner '<Ruby 表达式>'
交互式控制台bin/rails console
路由清单bin/rails routes
触发全量同步(Web)管理设置页 Sync 按钮,或 POST /media_syncing
触发全量同步(CLI)bin/rails runner 'MediaSyncAllJob.perform_now'
移动端 API 认证POST /api/v1/authentication
备份WAL checkpoint + 打包 /rails/storage(见第八节)

四、服务管理命令

在编排目录 /opt/black-candy/ 内执行(部署篇第四章已实测部分命令,此处为运维视角汇总):

docker compose ps

实测输出:

NAME                     IMAGE                                      COMMAND                  SERVICE   CREATED         STATUS         PORTS
black-candy-v3.2.1-app   ghcr.io/blackcandy-org/blackcandy:latest   "./bin/docker-entryp…"   app       8 minutes ago   Up 8 minutes   0.0.0.0:8136->80/tcp, [::]:8136->80/tcp
docker compose stop && docker compose start

实测输出:

 Container black-candy-v3.2.1-app Stopped
 Container black-candy-v3.2.1-app Started

stop/start 全程登录会话不失效(SECRET_KEY_BASE 已固定的前提,见配置篇 4.3)。日志跟踪:

docker compose logs -f app

(实时输出 Ctrl-C 退出;关键日志信号包括 Server started、Puma worker booted、SolidQueue-x.x.x Started ...,判读方法见部署篇 4.6 节。)

五、媒体库同步命令

5.1 Web 方式触发(推荐)

管理设置页点 Sync 按钮,等价于提交 POST /media_syncing。命令行复现(注意 Rails 8 的 per-form CSRF:token 必须取自该表单自身,见「九、常见问题」Q1):

# 1) 取同步表单专属 CSRF token(从 /setting 页面 HTML 解析 action="/media_syncing" 表单内的 authenticity_token)
# 2) 提交同步
curl -s -b cookies.txt -X POST http://127.0.0.1:8136/media_syncing \
  --data-urlencode "authenticity_token=<同步表单token>" \
  -D - -o /dev/null

实测响应头(节选):

HTTP/1.1 302 Found
Location: http://127.0.0.1:8136/setting

5.2 同步任务执行的验证

同步是 Solid Queue 异步任务,日志取证:

docker compose logs app 2>&1 | grep -i 'MediaSync'

实测输出(节选):

[ActiveJob] Enqueued MediaSyncAllJob (Job ID: cfb1afd4-f5b2-449d-9f00-8553be6edc6d) to SolidQueue(critical)
[ActiveJob] [MediaSyncAllJob] [cfb1afd4-f5b2-449d-9f00-8553be6edc6d] Performing MediaSyncAllJob (Job ID: cfb1afd4-f5b2-449d-9f00-8553be6edc6d) from SolidQueue(critical)

入队 + 执行两条日志都出现即同步完成。结果核查(同时是 rails runner 的典型用法):

docker exec black-candy-v3.2.1-app bin/rails runner 'puts "SONGS=#{Song.count} ALBUMS=#{Album.count} ARTISTS=#{Artist.count} USERS=#{User.count}"'

实测输出:

SONGS=6 ALBUMS=3 ARTISTS=3 USERS=2

5.3 CLI 方式触发与同步状态查询

前台同步(不等队列,立即执行,适合排障):

docker exec black-candy-v3.2.1-app bin/rails runner 'MediaSyncAllJob.perform_now; puts "AFTER: SONGS=#{Song.count}"'

实测输出:

AFTER: SONGS=6

同步状态与文件枚举核查:

docker exec black-candy-v3.2.1-app bin/rails runner 'puts "SYNCING=#{Media.syncing?}"; puts "MEDIA_PATH=#{Setting.media_path}"; puts "FILES=#{MediaFile.file_paths(Setting.media_path).size}"'

实测输出:

SYNCING=false
MEDIA_PATH=/media_data
FILES=6

💡 日常不需要手动触发:v2.1.0 起曲库文件变化会自动增量同步;手动全量同步用于批量改完 ID3 标签后强制刷新,或排障时验证链路。

六、用户与认证管理

6.1 修改管理员密码(Web 表单方式)

入口:头像菜单 → Setting(即 /users/1/setting)。命令行复现(同属 per-form CSRF,PATCH 语义走 _method=patch):

curl -s -b cookies.txt -X POST http://127.0.0.1:8136/users/1 \
  --data-urlencode "authenticity_token=<该表单token>" \
  --data-urlencode "_method=patch" \
  --data-urlencode "user[password]=q1w2e3r4" \
  --data-urlencode "user[password_confirmation]=q1w2e3r4" \
  -D - -o /dev/null

实测响应头(节选):

HTTP/1.1 302 Found
Location: http://127.0.0.1:8136/

6.2 创建普通用户

管理员在 Users 页(/users/new)创建。命令行复现:

curl -s -b cookies.txt -X POST http://127.0.0.1:8136/users \
  --data-urlencode "authenticity_token=<该表单token>" \
  --data-urlencode "user[email]=listener@test.local" \
  --data-urlencode "user[password]=q1w2e3r4" \
  --data-urlencode "user[password_confirmation]=q1w2e3r4" \
  -D - -o /dev/null

实测响应头(节选):HTTP/1.1 302 Found、Location: http://127.0.0.1:8136/users。用户清单核查:

docker exec black-candy-v3.2.1-app bin/rails runner 'User.find_each { |u| puts "USER email=#{u.email} admin=#{u.is_admin?}" }'

实测输出:

USER email=admin@admin.com admin=true
USER email=listener@test.local admin=false

6.3 密码改动的连带效应(实测)

改密后旧登录会话全部失效——实测改密前保存的 Cookie 访问任意页面立即被重定向到 /sessions/new 重新登录;而 SECRET_KEY_BASE 与改密无关,重启不丢会话。两个失效原因要分开判断:改密失效看服务端日志无异常、纯 302;密钥失效伴随每次重启必现(配置篇 4.3)。

七、CLI 深入

7.1 版本与环境

docker exec black-candy-v3.2.1-app bin/rails --version

实测输出:

Rails 8.1.3
docker exec black-candy-v3.2.1-app bin/rails about

实测输出(节选):

Rails version             8.1.3
Ruby version              ruby 4.0.2 (2026-03-17 revision d3da9fec82) +YJIT +PRISM [x86_64-linux-musl]
Rack version              Rack version 3.2.1
Environment               production
Database adapter          SQLite
Database schema version   20260320070527

about 是最快速的运行时环境速览:框架/语言版本、生产环境、数据库适配器与 schema 版本一眼可查,升级后核对首选用。

7.2 数据库类任务

docker exec black-candy-v3.2.1-app bin/rails db:prepare

实测输出:无(库结构已就绪时的静默成功;首次启动它会建库并跑迁移——部署篇排障 Q1 的报错就发生在这条任务里)。

docker exec black-candy-v3.2.1-app bin/rails db:version

实测输出:

Current version: 1

任务清单速查:

docker exec black-candy-v3.2.1-app bin/rails -T

实测输出(节选):

bin/rails db:create                          # Create the database from DATABASE_URL or config/database.yml for the current RAILS_ENV...
bin/rails db:create:cache                    # Create cache database for current environment
bin/rails db:create:primary                  # Create primary database for current environment
bin/rails db:prepare                         # Prepare the database for the current environment...
bin/rails db:version                         # Retrieve the current schema version number
bin/rails media_syncing:start                # Sync the media library

💡 技术拓展:任务清单里的 media_syncing:start 是同步任务的 CLI 入口,与 Web 设置页的 Sync 按钮、5.3 节的 perform_now 殊途同归——三种触发方式最终都执行同一个 MediaSyncAllJob。本文所述组件即采用该「Web/API/CLI 三入口同一任务内核」的机制。

7.3 rails runner:无 sqlite3 CLI 时的数据操作

runner 等价于一次性脚本执行器,模型方法全可用。除 5.2/6.2 节示例外,再举核查音频元数据的例子:

docker exec black-candy-v3.2.1-app bin/rails runner 'Song.order(:id).limit(3).each { |s| puts "SONG id=#{s.id} #{s.name} format=#{s.format} duration=#{s.duration}" }'

实测输出:

SONG id=1 Test Song 1 format=mp3 duration=30.0
SONG id=2 Test Song 2 format=mp3 duration=30.0
SONG id=3 Test Song 3 format=mp3 duration=30.0

7.4 rails console:交互式排查

docker exec -it black-candy-v3.2.1-app bin/rails console

实测输入输出(管道喂入示例):

Loading production environment (Rails 8.1.3)
blackcandy(production)> puts "CONSOLE_OK #{Rails.env} #{Setting.media_path}"
CONSOLE_OK production /media_data

(排障时用它交互验证模型行为;退出输入 exit。)

7.5 路由查询

docker exec black-candy-v3.2.1-app bin/rails routes | grep -E 'stream|media_syncing'

实测输出(节选):

stream_new GET      /stream/new(.:format)          streams#new
transcoded_stream_new GET /transcoded_stream/new(.:format) transcoded_streams#new
media_syncing POST     /media_syncing(.:format)       media_syncings#create
/api/v1/stream/new GET  /api/v1/stream/new(.:format)   api/v1/streams#new

路由表是找播放端点、同步端点的权威依据(Web 播放走 /stream/new,移动端 API 走 /api/v1/...,见第八节)。

八、移动端 API 实测

官方 iOS/Android 应用即使用这套 API。v3.2 认证形态:登录换取签名会话令牌(api_token),后续请求经 Authorization: Bearer 携带——没有独立的长期 API Key 概念。

8.1 认证与版本探测

curl -s -X POST http://127.0.0.1:8136/api/v1/authentication \
  -H 'Content-Type: application/json' \
  -d '{"session":{"email":"admin@admin.com","password":"q1w2e3r4"}}'

实测输出:

{"user":{"id":1,"email":"admin@admin.com","is_admin":true,"api_token":"eyJfcmFpbHMiOnsiZGF0YSI6MywicHVyIjoic2Vzc2lvbiJ9fQ--cb474d37105bd8d487f2065749cbc234cc2be73c7dfde9b004c4891e634bfe14"}}

版本信息端点(无需认证,客户端启动探测用):

curl -s http://127.0.0.1:8136/api/v1/system

实测输出:

{"blackcandy":{"version":"3.2.1"},"api":{"version":"2.0.0"}}

8.2 业务端点(Bearer 令牌)

令牌变量:T=<上一步返回的 api_token>。

歌曲详情:

curl -s -H "Authorization: Bearer $T" http://127.0.0.1:8136/api/v1/songs/1

实测输出(节选):

{"id":1,"name":"Test Song 1","duration":30.0,"url":"http://127.0.0.1:8136/api/v1/stream/new?song_id=1","album_name":"Test Album 1","artist_name":"Test Artist 1","is_favorited":false,"format":"mp3",...}

当前播放列表:

curl -s -H "Authorization: Bearer $T" http://127.0.0.1:8136/api/v1/current_playlist/songs

实测输出:HTTP 200,JSON 含 Test Song 2、Test Song 1(与 Web 端播放列表一致)。

音频流(移动端播放):

curl -s -H "Authorization: Bearer $T" "http://127.0.0.1:8136/api/v1/stream/new?song_id=2" -o /dev/null -w '%{http_code} %{content_type} %{size_download}B'

实测输出:

200 audio/mpeg 481202B

⚠️ 实测排障:无令牌或令牌错误时业务端点统一返回 401(head :unauthorized);访问不存在的资源路径返回 404。另注意认证响应字段名是 api_token(签名会话令牌)而非旧文档印象中的 api_key——按字段名照抄即可。Web 端播放端点 /stream/new 用的是浏览器会话 Cookie,与 API 的 Bearer 通道相互独立。

九、备份与恢复

9.1 备份思路

数据面只有两处:/rails/storage(四个 SQLite 库 + 封面缓存)与音乐文件目录。备份三步:checkpoint 压平 WAL → 打包 → 异地存放。

踩坑实录 1:镜像内没有 sqlite3 命令行工具(which sqlite3 → NO_SQLITE3_CLI),无法用经典的 sqlite3 x.db ".backup" 在线备份。 踩坑实录 2:镜像内 ruby sqlite3 gem 的 Database#backup API 在当前版本不可用(实测调用报错)。 可行方案:应用不停机,直接对各库执行 PRAGMA wal_checkpoint(TRUNCATE) 把 WAL 内容压回主文件,再文件级打包(-wal/-shm 一并保留,保证一致性):

docker exec black-candy-v3.2.1-app bin/rails runner '
["production","production_cable","production_queue","production_cache"].each do |db|
  ActiveRecord::Base.establish_connection(db.to_sym)
  r = ActiveRecord::Base.connection.execute("PRAGMA wal_checkpoint(TRUNCATE)")
  puts "CHECKPOINT #{db} => #{r.inspect}"
end'

实测输出:

CHECKPOINT production => [{"busy":0, "log":3, "checkpointed":3}]
CHECKPOINT production_cable => [{"busy":0, "log":0, "checkpointed":0}]
CHECKPOINT production_queue => [{"busy":0, "log":2, "checkpointed":2}]
CHECKPOINT production_cache => [{"busy":0, "log":4, "checkpointed":4}]

(busy:0 即无阻塞、全部压平成功。)

9.2 打包

tar -czf /opt/black-candy/black-candy-backup-20261028.tar.gz -C /opt/black-candy/storage_data .

实测产物:black-candy-backup-20261028.tar.gz,28471 字节(6 首测试曲规模;生产规模随曲库主数据增长,音乐本体在 media_data 目录另行备份)。

tar -tzf /opt/black-candy/black-candy-backup-20261028.tar.gz | head -8

实测输出(节选):

./
./production.sqlite3
./production.sqlite3-shm
./production.sqlite3-wal
./production_cache.sqlite3
./production_queue.sqlite3
./production.sqlite3-wal
./production_cable.sqlite3

9.3 恢复演练(实测闭环)

模拟灾难:停栈 → 清空数据目录 → 还原备份 → 起栈 → 验证数据完整:

cd /opt/black-candy
docker compose stop
rm -rf /opt/black-candy/storage_data/*
tar -xzf /opt/black-candy/black-candy-backup-20261028.tar.gz -C /opt/black-candy/storage_data
docker compose start

起栈后验证(登录会话随库恢复,无需重新登录即可验证):

curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8136/

实测输出:302(未登录跳登录页,服务正常);曲库核查:

docker exec black-candy-v3.2.1-app bin/rails runner 'puts "RESTORED SONGS=#{Song.count} ALBUMS=#{Album.count} USERS=#{User.count}"'

实测输出:

RESTORED SONGS=6 ALBUMS=3 USERS=2

恢复闭环成立。注意:备份包含会话数据,恢复即恢复全部用户登录态——这也是不要把备份包放到公开位置的原因之一。

十、监控与日志

日常巡检三看:

  1. 实例与端口:docker compose ps(Up + 端口映射存在)与 curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8136/(302 即活);
  2. 任务队列:docker compose logs app | grep SolidQueue——应能看到 Supervisor/Dispatcher/Worker 三个 Started 记录(部署篇 4.6 节回显);同步任务看 grep MediaSync;
  3. 资源:转码播放是唯一 CPU 尖峰来源(ffmpeg),大量并发公网收听时用 docker stats black-candy-v3.2.1-app 观察(本环境 6 首曲规模下资源占用平稳,未记录到异常值)。

十一、常见问题(排障实录)

Q1:curl 提交表单(同步/改密/建用户)总被 302 回登录页,浏览器却正常

踩坑实录:Rails 8 默认启用 per-form CSRF token——每个表单的 authenticity_token 与该表单 action 绑定。实测教训:从页面其他表单抄 token 提交 POST /media_syncing,被 302 到 /sessions/new(CSRF 失败触发登出),日志显示该 POST 仅处理 1ms、0 条 SQL;换成同步表单自身的 token 后一次成功。规律:token 必须取自目标表单自身(在 HTML 中先定位 action="/media_syncing" 再取其内部 token)。

Q2:rails runner 里引用了不存在的模型/常量

实测报错:uninitialized constant ApiKey(v3.2 已无该模型,认证改为签名会话令牌)。runner 直接执行 Ruby,写错常量即报错退出;先用 rails routes、app/models/ 目录核对真实模型名再写查询。

Q3:宿主机上找不到 ffmpeg 能用吗?

服务端转码不依赖宿主 ffmpeg(镜像内置 8.0.1)。但制作测试音频、批量改标签这类宿主侧数据准备需要宿主有 ffmpeg;实测环境宿主 ffmpeg 缺失,改用镜像内的 ffmpeg 执行造数(docker run --rm -v <媒体目录>:/media_data <镜像> ffmpeg ...),效果等价。

Q4:同步完成后部分歌曲重复入库?

正常不会:同步以「文件路径 + 修改时间的 MD5 摘要」判重,移动文件不产生重复,被移出的曲目自动下架。若确有重复,优先排查是否两次挂载了同一目录(两个挂载点指向同一音乐目录会被视为两个路径)。

Q5:API 返回 401 但 /system 正常?

/system 是免认证端点(版本探测用),其余业务端点都需要 Authorization: Bearer <api_token>。401 优先排查:令牌是否为认证接口返回的 api_token 字段值(实测踩坑:误把其他字段当令牌,全部 401)、令牌是否在改密/会话失效后未刷新。

十二、参考链接