Karaoke Eternal Docker Compose 部署实战
Karaoke Eternal v2.0.2 从零到多端点歌的 Compose 部署实录:编排文件逐项解说、首启初始化与媒体扫描、socket 点歌验证,以及数据目录权限、端口冲突、纯音频不入库三大排障还原。
Karaoke Eternal Docker Compose 部署实战
本文记录 Karaoke Eternal v2.0.2 在一台 Linux 服务器上从零到"手机点歌成功"的完整部署实录:编写 Compose 编排、准备曲库目录、启动服务、完成首次初始化、添加媒体文件夹、触发扫描、多端点歌验证,并把过程中踩到的每一个坑原样呈现。项目背景与选型依据见《Karaoke Eternal 介绍与选型指南》;参数级调优见《Karaoke Eternal 配置参数详解》;日常运维见《Karaoke Eternal 常用命令与运维实战(核心篇)》。
一、部署前的三件事
动手前先确认三件事:版本、目录规划、端口。本次部署的目标版本为 v2.0.2(当前最新,2026-10-28 取证),规划如下:
- 数据目录
/opt/kes/config:存放数据库文件database.sqlite3,必须持久化——这是整个服务唯一需要备份的状态; - 曲库目录
/opt/kes/media:存放媒体文件,只读使用(服务只扫描,不修改媒体本身); - 服务端口
8142:映射到服务进程默认监听的8080; - 访问地址:
http://<服务器IP>:8142。
先创建目录骨架:
# mkdir -p /opt/kes/config /opt/kes/media
# cp "Test Trio - Sine Wave Serenade.mp4" "星光乐队 - 夜航星.mp4" /opt/kes/media/
# ls -l /opt/kes/media/
-rw-r--r-- 1 root root 1079701 Oct 28 23:31 'Test Trio - Sine Wave Serenade.mp4'
-rw-r--r-- 1 root root 1307331 Oct 28 23:31 '星光乐队 - 夜航星.mp4'
💡 技术拓展:文件名采用官方约定的 "Artist - Title"(歌手 - 标题) 格式——媒体扫描器默认按"空格-连字符-空格"拆分歌手与标题,命名不规范的文件扫描后不会出现在曲库里(详见第四章排障实录)。
二、编写 Compose 编排文件
本章引导语:Compose 编排是官方推荐的部署方式,官方文档给出了参考模板,我们在其基础上补齐中文时区与固定 IP。
在 /opt/kes 下创建 compose.yaml:
name: karaoke-eternal-stack
services:
karaoke-eternal:
image: radrootllc/karaoke-eternal:2.0.2
container_name: karaoke-eternal
ports:
- "8142:8080"
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
volumes:
- ./config:/config
- ./media:/mnt/karaoke
restart: unless-stopped
networks:
kes-lan:
ipv4_address: 10.89.64.10
networks:
kes-lan:
driver: bridge
ipam:
config:
- subnet: 10.89.64.0/24
逐项解说:
image: radrootllc/karaoke-eternal:2.0.2:官方 Docker Hub 命名空间,务必带版本号,不用latest——卡拉OK服务要在派对中稳定运行,升级应该是主动行为而非被动发生;PUID/PGID=1000:服务进程以该 uid/gid 运行(仿 LinuxServer 规范),这两个值决定了数据目录的属主要求,第四章第一个坑就源于此;TZ:影响日志时间戳与房间创建时间;./config:/config:数据库目录,这是唯一需要备份的目录;./media:/mnt/karaoke:曲库目录,稍后在 Web 界面中把它添加为媒体文件夹;restart: unless-stopped:崩溃或宿主机重启后自动拉起(排障实录里它会救场两次)。
三、启动与首次初始化
本章引导语:Compose 的一等公民命令只有三条——pull、up -d、logs,本章按序执行并解读每一段真实输出。
3.1 拉取与启动
# cd /opt/kes
# docker compose pull
[+] Pulling 1/1
✔ karaoke-eternal Pulled
# docker compose up -d
[+] Running 2/2
✔ Network karaoke-eternal-stack_kes-lan Created
✔ Container karaoke-eternal Starting
3.2 首启日志解读
# docker logs karaoke-eternal | tail -20
00:12:46.428 (main[1]) › NODE_ENV=production
00:12:46.428 (main[1]) › KES_CONSOLE_COLORS=false
00:12:46.428 (main[1]) › KES_PATH_DATA=/config
00:12:46.428 (main[1]) › KES_PORT=8080
00:12:46.428 (main[1]) › KES_SCANNER_LOG_LEVEL=0
00:12:46.428 (main[1]) › KES_SERVER_LOG_LEVEL=0
00:12:46.428 (main[1]) › PGID=1000
00:12:46.429 (main[1]) › PUID=1000
00:12:46.432 (db) › Opening database file (writeable) /config/database.sqlite3
00:12:46.437 (db) › Running migration 1: initial-schema
00:12:46.439 (db) › Running migration 2: replaygain
00:12:46.441 (db) › Running migration 3: queue-linked-list
00:12:46.442 (db) › Running migration 4: paths-rooms-data
00:12:46.443 (db) › Running migration 5: roles
00:12:46.607 (Prefs) › Rotating JWT secret key (length=64)
00:12:46.618 (server) › Web server running at http://10.89.64.10:8080/
解读四点:
- migration 1→5:数据库迁移按序执行(初始表结构、ReplayGain 音量均衡字段、队列链表结构、路径与房间数据、角色表),以后每次升级版本也会在这里看到新增迁移;
- Rotating JWT secret key:每次启动都会轮换会话签名密钥,意味着重启后所有已登录会话失效,所有人需重新登录;
- Web server running at:服务就绪标志,浏览器访问
http://<服务器IP>:8142即可打开 Web 应用; - Opening database file (writeable):
writeable字样很关键——如果这里变成read-only,说明进程对数据库文件没有写权限(第四章排障实录的主角)。
3.3 首次初始化:注册管理员
首启状态下打开 Web 页面会出现"创建管理员账户"向导,等价的接口调用是 POST /api/setup(只在首次启动时可用,创建管理员后自动建立默认房间 "Room 1" 并直接登录):
# curl -s -X POST http://127.0.0.1:8142/api/setup \
-H 'Content-Type: application/json' \
-d '{"username":"admin","newPassword":"q1w2e3r4","newPasswordConfirm":"q1w2e3r4","name":"管理员"}'
{"dateCreated":1793204022,"dateUpdated":0,"isAdmin":true,"isGuest":false,"name":"管理员","roomId":1,"userId":1,"username":"admin"}
回显确认三件事:userId=1、isAdmin=true、roomId=1(默认房间已自动创建)。此时用 admin / q1w2e3r4 即可在 Web 端登录。
首启保护是硬性的——初始化完成后再次调用会被直接拒绝:
# curl -s -X POST http://127.0.0.1:8142/api/setup \
-H 'Content-Type: application/json' \
-d '{"username":"hacker","newPassword":"x1234567","newPasswordConfirm":"x1234567"}' -w ' [HTTP %{http_code}]'
Forbidden [HTTP 403]
💡 技术拓展:JWT(JSON Web Token)是无状态的登录凭证——服务端只签名不存储,登录后浏览器以 Cookie
keToken携带。好处是服务端无需会话表,代价是无法"踢人下线",这就是密钥轮换设计的意义。
3.4 添加媒体文件夹
登录后进入 Settings → Media Folders 添加 /mnt/karaoke。等价接口是 POST /api/prefs/path,注意路径放在 URL 查询参数 dir 里(需 URL 编码,/mnt/karaoke → %2Fmnt%2Fkaraoke):
# curl -s -b admin.jar -X POST "http://127.0.0.1:8142/api/prefs/path?dir=%2Fmnt%2Fkaraoke" \
-H 'Content-Type: application/json' -d '{}'
{"paths":{"result":[2],"entities":{"2":{"pathId":2,"path":"/mnt/karaoke","priority":0,"prefs":{}}}},
"roles":{"result":[1,4,2,3],"entities":{"1":{"roleId":1,"name":"admin"},"2":{"roleId":2,"name":"player"},
"3":{"roleId":3,"name":"standard"},"4":{"roleId":4,"name":"guest"}}}, ...}
返回里同时能看到系统内置的四个角色:admin(管理员)、player(播放端)、standard(普通点歌用户)、guest(访客)。
添加成功后服务会自动扫描该路径,无需手动触发:
# docker logs karaoke-eternal | tail -14
16:14:17.514 (FileScanner) › [2/2] /mnt/karaoke/星光乐队 - 夜航星.mp4
16:14:17.516 (FileScanner) › => duration: 0:12
16:14:17.519 (FileScanner) › => 0 db result(s)
16:14:17.519 (FileScanner) › => new: {"artistId":2,"artist":"星光乐队","artistNorm":"星光乐队",
"songId":2,"title":"夜航星","titleNorm":"夜航星"}
16:14:17.521 (FileScanner) › Scanned 2 valid media files
16:14:17.521 (FileScanner) › Searching for invalid media entries
16:14:17.521 (FileScanner) › Removed 0 invalid media entries
00:14:17.525 (Media) › cleanup: 0 media in nonexistent paths
00:14:17.526 (Media) › cleanup: 0 songs with no associated media
00:14:17.526 (Media) › cleanup: 0 stars for nonexistent songs
00:14:17.526 (Media) › cleanup: 0 queue items for nonexistent songs
00:14:17.528 (Library) › built library cache in 0.258ms
00:14:17.528 (main[1]) › Media scanner process exited (0)
日志要点:Scanned 2 valid media files 表示 2 个 MP4 全部解析入库(new: 行就是从文件名解析出的歌手/标题);结尾的 cleanup 四连 0 与 Media scanner process exited (0) 表示扫描进程干净退出。手动全库扫描的接口是 GET /api/prefs/paths/scan(返回 OK),日常运维用法在《Karaoke Eternal 常用命令与运维实战(核心篇)》展开。
3.5 点歌与多端验证
曲库与队列的实时交互走 socket.io 长连接。用一个最小 Python 客户端验证完整链路:登录 → 连线 → 接收服务端推送 → 点歌。
登录(任一用户会话):
# curl -s -c admin.jar -X POST http://127.0.0.1:8142/api/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"q1w2e3r4","roomId":1}'
{"dateCreated":1793204022,...,"isAdmin":true,"name":"管理员","roomId":1,"userId":1,"username":"admin"}
socket.io 连线后,服务端主动推送四类状态(这就是"打开页面即看到最新曲库与队列"的机制):
push: 42["action",{"type":"prefs/PREFS_PUSH","payload":{"paths":{"result":[2],...
push: 42["action",{"type":"library/PUSH","payload":{"artists":{"result":[1,2],...
push: 42["action",{"type":"user/STARS_PUSH","payload":{"starredArtists":[],"starredSongs":[]}}]
push: 42["action",{"type":"queue/PUSH","payload":{"result":[],"entities":{}}}]
解析 library/PUSH 得到曲库全量:曲库歌曲数: 2 => ['Sine Wave Serenade', '夜航星']。
发送点歌指令(server/QUEUE_ADD,指定 songId):
=== admin 点歌 songId=2(星光乐队 - 夜航星)===
回执: 431[{"type":"server/QUEUE_ADD_SUCCESS"}]
回执: 42["action",{"type":"queue/PUSH","payload":{"result":[1],"entities":{"1":{"queueId":1,"songId":2,
"userId":1,"prevQueueId":null,"mediaId":2,"userDisplayName":"管理员",...
两级确认:先收到 QUEUE_ADD_SUCCESS 应答,再收到广播 queue/PUSH(userDisplayName:"管理员" 说明队列项已带上点歌人昵称——所有连线客户端此刻都会看到队列多了一条)。
多端验证:用管理员创建第二个用户并登录,模拟"另一位客人":
# curl -s -b admin.jar -X POST http://127.0.0.1:8142/api/user \
-H 'Content-Type: application/json' \
-d '{"username":"demo","newPassword":"demo123456","newPasswordConfirm":"demo123456",
"name":"演示用户","role":"standard","roomId":1}'
{} [HTTP 200]
# curl -s -c demo.jar -X POST http://127.0.0.1:8142/api/login \
-H 'Content-Type: application/json' \
-d '{"username":"demo","password":"demo123456","roomId":1}'
{"dateCreated":1793204220,...,"isAdmin":false,"name":"演示用户","roomId":1,"userId":2,"username":"demo"} [HTTP 200]
isAdmin:false 且 role=standard 的第二个会话建立成功——同一服务现在服务着两个并行的浏览器会话,各自可以在自己手机上搜歌、点歌、看队列。至此"从零到多端点歌"闭环完成。
四、排障实录
本章引导语:以下三个坑全部来自本次部署的真实过程,按"现象 → 定位 → 修复"结构还原,每一个都能在日志里找到铁证。
4.1 坑一:启动即崩溃,日志刷 unable to open database file
现象:docker compose up -d 后服务反复重启,Web 页面无法访问。
# docker logs karaoke-eternal | tail -12
00:12:18.432 (db) › Opening database file (writeable) /config/database.sqlite3
00:12:18.248 (main[1]) › Unhandled Rejection: Error: unable to open database file
at new DatabaseWrapper (file:///opt/.npm-global/lib/node_modules/karaoke-eternal/build/server/lib/Database.js:12:19)
定位:compose.yaml 里 ./config:/config 挂载的目录若不存在,会由引擎自动创建,属主是 root;而服务进程按 PUID=1000 运行,在 root 属主的目录里建库被拒。restart: unless-stopped 会不断拉起进程,形成"崩溃-重启"循环。
修复:把数据目录属主交给进程身份:
# chown -R 1000:1000 /opt/kes/config
# docker logs karaoke-eternal | tail -6
00:12:46.432 (db) › Opening database file (writeable) /config/database.sqlite3
00:12:46.437 (db) › Running migration 1: initial-schema
...
00:12:46.618 (server) › Web server running at http://10.89.64.10:8080/
无需重启,下一轮自动拉起即恢复——这也是 restart: unless-stopped 的价值。经验法则:chown -R $PUID:$PGID <数据目录> 应该成为挂载前的固定动作。
4.2 坑二:端口被占,Bind for 127.0.0.1:8140 failed: port is already allocated
现象:docker compose up -d 直接报错:
Error response from daemon: failed to set up container networking: driver failed programming
external connectivity on endpoint karaoke-eternal-b2 (...): Bind for 127.0.0.1:8140 failed:
port is already allocated
定位与修复:用 ss -tlnp 查明目标端口已有监听(本环境 8140 被 another 服务占用,8141 被本地工具占用),换用空闲端口 8142 后正常启动。规划端口前先 ss -tlnp | grep <port> 是一分钟就能省掉十分钟的事。
4.3 坑三:曲库里"少了歌"——纯音频文件扫描后不出现
现象:曲库目录里放了一个 Synth Sisters - Loop City.mp3(命名完全合规),扫描完成后曲库里却没有它:
16:02:35.898 (FileScanner) › [1/3] /mnt/karaoke/Synth Sisters - Loop City.mp3
16:02:35.901 (FileScanner) › => no .cdg sidecar found
16:02:35.901 (FileScanner) › [2/3] /mnt/karaoke/Test Trio - Sine Wave Serenade.mp4
16:02:35.918 (FileScanner) › => ok
...
16:02:35.898 (FileScanner) › => found 3 files with valid extensions [".m4a",".mp3",".mp4",".zip"]
...
16:02:35.853 (FileScanner) › Scanned 2 valid media files
定位:注意日志的三层信息——found 3 files with valid extensions(扩展名层面 3 个文件都合法)、mp3 行后只有 no .cdg sidecar found 而没有 => ok、最终 Scanned 2 valid media files(只有 2 个算有效媒体)。结论:一个孤立的 MP3(无 .cdg 歌词画面伴生文件、无视频轨)不构成完整卡拉OK曲目,扫描器会静默跳过它。扩展名合法 ≠ 会入库。
修复:给 MP3 配上同名 .cdg 文件(或改用带视频轨的 MP4)。这与官方文档一致——支持列表是 MP3+G(音频+歌词画面,含 zip 打包)与 MP4 视频。
五、部署验收清单
-
docker compose ps显示服务 Up,docker logs末尾有Web server running at - Web 页面可打开,
admin / q1w2e3r4登录成功 - Settings 中媒体文件夹路径正确,扫描日志出现
Media scanner process exited (0) - 手机连同一局域网访问
http://<服务器IP>:8142,能搜到曲库歌曲 - 手机点歌后,播放端页面队列实时出现该歌曲与点歌人昵称
- 数据目录
config/database.sqlite3属主为1000:1000
部署完成后,建议立刻做一次基线备份(一条命令的事),方法见《Karaoke Eternal 常用命令与运维实战(核心篇)》;想调整端口、日志级别、扫描行为等服务参数,见《Karaoke Eternal 配置参数详解》。