Black Candy 常用命令与运维实战
Black Candy v3.2.1 运维实战:compose 服务管理、媒体同步三入口、用户/密码管理、rails CLI(runner/console/db:prepare/about/routes)与移动端 API(Bearer 会话令牌)实测、WAL checkpoint 备份与恢复闭环演练,排障实录沉淀。
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
二、前置条件
- 服务栈已按部署篇就绪并可登录(
docker compose ps状态为 Up); - 运维操作的两类入口:
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
恢复闭环成立。注意:备份包含会话数据,恢复即恢复全部用户登录态——这也是不要把备份包放到公开位置的原因之一。
十、监控与日志
日常巡检三看:
- 实例与端口:
docker compose ps(Up + 端口映射存在)与curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8136/(302 即活); - 任务队列:
docker compose logs app | grep SolidQueue——应能看到 Supervisor/Dispatcher/Worker 三个 Started 记录(部署篇 4.6 节回显);同步任务看grep MediaSync; - 资源:转码播放是唯一 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)、令牌是否在改密/会话失效后未刷新。
十二、参考链接
- 官方仓库与 README:https://github.com/blackcandy-org/blackcandy
- Rails CLI 指南(runner/console/routes 通用用法):https://guides.rubyonrails.org/command_line.html
- 本专栏系列篇:《Black Candy 介绍与选型指南》、《Black Candy Docker Compose 部署实战》、《Black Candy 配置参数详解》