Karaoke Eternal Docker Compose 部署实战

Karaoke Eternal v2.0.2 从零到多端点歌的 Compose 部署实录:编排文件逐项解说、首启初始化与媒体扫描、socket 点歌验证,以及数据目录权限、端口冲突、纯音频不入库三大排障还原。

Karaoke Eternal流媒体自托管Karaoke Eternal2026/10/29

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 配置参数详解》。