Skip to content

部署与运维

本文档基于以下源文件编写:

目录

  1. 简介
  2. 构建系统
  3. Docker 部署
  4. 独立二进制部署
  5. 反向代理与子路径部署
  6. 在线升级系统
  7. Tracely 监控
  8. 安全考虑
  9. 平台适配注意事项
  10. 运维速查表

1. 简介

Songloft 支持两种主要部署方式:Docker 容器独立二进制。Docker 方式提供完整的容器化体验,包括热替换升级和自动重启;独立二进制方式则适用于裸机或不使用容器的环境。两种方式共享同一套启动参数体系,均支持反向代理子路径部署。

本文档覆盖从构建到生产运行的完整运维链路,包括构建变体选择、部署配置、在线升级、监控接入和安全加固。

章节来源


2. 构建系统

Songloft 的构建系统由两个正交维度控制:

维度可选值说明
VERSIONdev / X.Y.Z控制是否为开发版。dev 时 Makefile 自动启用 -tags dev,包含 Swagger UI 和 pprof
BUILD_TYPElite / 空(即 full控制是否嵌入 Flutter Web 前端。lite 以纯 API 模式运行

两个维度严格分离,禁止 BUILD_TYPE=dev 等混合值。

2.1 构建命令

bash
make build              # 开发版(完整,嵌入前端,含 Swagger + pprof)
make build-lite          # 开发版(精简,不嵌入前端)
make build-prod          # 生产版(完整,嵌入前端)
make build-prod-lite     # 生产版(精简,不含前端)

2.2 编译时注入

Makefile 通过 -ldflags -X 将以下变量注入到二进制中:

makefile
LDFLAGS=-s -w \
    -X songloft/internal/version.Version=$(VERSION) \
    -X songloft/internal/version.GitCommit=$(GIT_COMMIT) \
    -X songloft/internal/version.BuildTime=$(BUILD_TIME) \
    $(if $(BUILD_TYPE),-X songloft/internal/version.BuildType=$(BUILD_TYPE)) \
    $(if $(TRACELY_APP_ID),-X songloft/internal/tracelycfg.AppID=$(TRACELY_APP_ID)) \
    $(if $(TRACELY_APP_SECRET),-X songloft/internal/tracelycfg.AppSecret=$(TRACELY_APP_SECRET)) \
    $(if $(TRACELY_HOST),-X songloft/internal/tracelycfg.Host=$(TRACELY_HOST))

运行时通过 songloft -version 查看注入结果:

Songloft Version: 2.10.0
Git Commit: a102490
Build Time: 2026-06-12T10:00:00Z
Build Type: full

章节来源


3. Docker 部署

3.1 多阶段构建

Dockerfile 采用两阶段构建:

阶段一:go-buildergolang:1.26-alpine

  • 安装 gcc musl-dev make upx git 编译工具链(带重试)
  • 先复制 go.mod/go.sum 下载依赖(利用 Docker 层缓存加速)
  • 再复制源码,使用 --mount=type=cache 缓存 Go 编译产物和模块目录
  • 根据 LITE_BUILD 参数选择 make build-prodmake build-prod-lite

阶段二:运行时alpine:latest

  • 安装 ca-certificates tzdata alsa-lib alsa-plugins alsa-utils alsa-ucm-conf(ALSA 运行时库)
  • hanxi/ffmpeg 镜像复制 ffmpegffprobe/bin/
  • 从 go-builder 复制编译好的 songloft 二进制到 /app/songloft
  • 复制 docker-entrypoint.sh/app/
  • 默认时区 Asia/Shanghai,暴露端口 58091

图表来源

3.2 卷挂载与环境变量

Docker 镜像定义两个 VOLUME:

挂载点用途建议
/app/music音乐文件存储目录-v /your/music/path:/app/music
/app/data应用数据(数据库、缓存、插件、运行二进制)-v /your/data/path:/app/data

内置环境变量:

变量默认值说明
ADMIN_USERNAMEadmin管理员用户名
ADMIN_PASSWORDadmin管理员密码
IN_DOCKERtrue标识 Docker 环境,启用升级检查等 Docker 专属功能
LISTEN_PORT58091监听端口
DB_PATHdata/songloft.db数据库文件路径
BASE_PATH反向代理子路径前缀

典型 docker run 命令:

bash
docker run -d \
  --name songloft \
  -p 58091:58091 \
  -v /data/music:/app/music \
  -v /data/songloft:/app/data \
  -e ADMIN_USERNAME=myuser \
  -e ADMIN_PASSWORD=mypassword \
  songloft/songloft:latest

章节来源

3.3 docker-entrypoint.sh 热替换规则

Docker 镜像中的底包位于 /app/songloft,实际运行的二进制位于持久化 data 卷的 /app/data/songloft。容器每次启动时,entrypoint 脚本根据以下规则决定是否用底包覆盖 data 目录中的二进制:

核心原则:底包代表用户意图;dev/正式或 full/lite 不一致时用底包覆盖。只有「同通道 + 同 BUILD_TYPE」时才比较新旧:dev 按 Build Time,release 按版本号。

场景行为原因
dev ↔ release 通道不同替换用户换了镜像通道
BUILD_TYPE 不同(full 与 lite 互换)替换用户换了镜像变体
同为 dev + 同类型 + 底包 Build Time > data Build Time替换dev 滚动构建按构建时间选最新
同为 dev + 同类型 + data Build Time >= 底包 Build Time不替换data 可能通过 API 在线升级过
同为 release + 同类型 + 底包版本 > data 版本替换正式版升级
同为 release + 同类型 + data 版本 >= 底包不替换data 可能通过 API 在线升级过

热替换流程:

  1. 首次启动(data 目录无二进制):直接从底包复制
  2. 后续启动:通过 songloft -version 获取双方版本号和 BUILD_TYPE
  3. 满足替换条件时:先备份旧版本到 /app/songloft.backup,再复制底包
  4. 不满足替换条件时:保留 data 目录中的二进制(可能是在线升级后的更新版本)
  5. 最终 exec /app/data/songloft "$@" 启动服务

版本比较使用 awk 逐段比较数字部分(自动去除 -beta 等后缀),devunknown 版本不参与数值比较。

章节来源


4. 独立二进制部署

无需 Docker 时,可直接下载或编译二进制运行。

4.1 CLI 参数

./songloft [参数]
参数默认值说明
-port58091监听端口
-dbdata/songloft.db数据库文件路径
-usernameadmin管理员用户名
-passwordadmin管理员密码
-base-path反向代理子路径前缀(如 /songloft
-version--显示版本信息后退出
-help--显示帮助信息后退出

参数优先级:CLI 参数 > 环境变量 > 默认值。所有 CLI 参数都有对应的环境变量(LISTEN_PORTDB_PATHADMIN_USERNAMEADMIN_PASSWORDBASE_PATH)。

4.2 启动示例

bash
# 最简启动(使用所有默认值)
./songloft

# 自定义端口和凭证
./songloft -port 8080 -username myuser -password mypassword

# 指定数据库路径
./songloft -db /var/lib/songloft/songloft.db

# 子路径部署
./songloft -base-path /music

4.3 数据目录结构

启动后会自动创建以下目录结构(相对于数据库文件所在目录):

data/
├── songloft.db         # SQLite 数据库(WAL 模式)
├── covers/             # 封面图片存储
├── jsplugins/          # JS 插件安装目录
├── jsplugins_data/     # JS 插件数据目录(storage API 持久化)
└── music_cache/        # 远程音乐缓存目录(可自定义路径)

章节来源


5. 反向代理与子路径部署

当 Songloft 部署在反向代理(Nginx、Caddy、Traefik 等)后面并需要挂载到子路径时(如 https://example.com/songloft/),需要配置 BASE_PATH

5.1 后端处理

启动时通过 -base-path /songloft 或环境变量 BASE_PATH=/songloft 配置。normalizeBasePath 函数执行以下规范化:

  • 确保以 / 开头
  • 去除尾部 /
  • 拒绝包含 ?#.. 的路径

Start 方法中的路由挂载:

go
if a.config.BasePath != "" {
    mux := http.NewServeMux()
    mux.Handle(a.config.BasePath+"/", http.StripPrefix(a.config.BasePath, a.router))
    mux.HandleFunc(a.config.BasePath, func(w http.ResponseWriter, r *http.Request) {
        http.Redirect(w, r, a.config.BasePath+"/", http.StatusMovedPermanently)
    })
    handler = mux
}

http.StripPrefix 在最外层剥离子路径前缀,内部路由(Chi router)始终以 / 开头工作,无需感知子路径。访问 /songloft(无尾斜线)时自动 301 重定向到 /songloft/

5.2 前端 base href 注入

embed.go 在运行时将嵌入的 index.html 中的 <base href="/"> 替换为 <base href="/songloft/">

go
if a.config.BasePath != "" {
    indexBytes = bytes.Replace(
        indexBytes,
        []byte(`<base href="/">`),
        []byte(`<base href="`+a.config.BasePath+`/">`),
        1,
    )
}

替换后的 index.html 会重新生成预压缩版本(Brotli/Gzip),保证压缩缓存与内容一致。前端嵌入模式通过 Uri.base.path 自动检测子路径,无需额外配置。

5.3 Nginx 配置示例

nginx
location /songloft/ {
    proxy_pass http://127.0.0.1:58091/songloft/;
    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;
    
    # WebSocket 支持(如有需要)
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

注意:反向代理应将完整的子路径(含前缀)传递给后端,由后端自行 StripPrefix

章节来源


6. 在线升级系统

在线升级仅在 Docker 环境中可用(通过 IN_DOCKER=true 环境变量判断)。

6.1 升级渠道

渠道version.json 地址说明
stablegithub.com/.../releases/latest/download/version.json正式版(最新稳定 Release)
devgithub.com/.../releases/download/dev/version.json开发版(main 分支滚动构建)

CheckForUpdates 只检查当前运行通道:dev 只检查 dev,release 只检查 stable。dev 通过 build_time 判断是否有更新;release 通过版本号判断是否有更新。在线升级不允许在 dev/release 之间切换,也不允许在 full/lite 之间切换。

6.2 升级流程

UpgradeBinary 执行 6 步流程,每一步都更新进度状态供前端轮询:

1. FetchVersionInfo   -- 获取目标版本信息(version.json)
2. DownloadBinary     -- 下载新版本到 /app/data/songloft.new(带进度回调)
3. TestBinary         -- chmod +x 后执行 -help 测试可用性
4. backupCurrentBinary -- 备份当前版本到 /app/data/songloft.backup
5. os.Rename          -- 原子替换 songloft.new -> songloft
6. os.Exit(0)         -- 延迟 5 秒退出,Docker restart policy 自动拉起新版本

升级进度状态机:

idle -> downloading -> testing -> replacing -> restarting
                \         \          \
                 \---------\----------\--> failed

下载时根据底包的 BUILD_TYPE 自动拼接平台后缀(如 -linux-amd64-linux-amd64-lite),保证升级后的构建类型与底包一致。

6.3 回退到底包

ResetToBaseImage 提供一键回退功能:将 /app/songloft(Docker 镜像中的原始底包)复制回 /app/data/songloft,然后重启服务。流程与升级类似(备份 -> 写临时文件 -> 原子替换 -> 退出)。

6.4 GitHub 代理

升级服务支持 proxyPrefix 参数用于 GitHub 镜像加速。代理前缀以 URL 拼接方式工作(如 https://ghproxy.com/ + 原始 URL)。此外还可通过 /api/v1/settings/http-proxy 配置全局 HTTP 代理(HTTP/HTTPS/SOCKS5),两者可共存:先拼接镜像前缀再经 HTTP Proxy 转发。

章节来源


7. Tracely 监控

Songloft 集成了 Tracely 监控客户端,用于收集匿名的安装/升级事件和心跳数据。该功能采用 opt-in 模式:仅在编译时注入了 AppID、AppSecret 和 Host 三个值时才启用。

7.1 编译时注入

bash
make build-prod \
  TRACELY_APP_ID=your-app-id \
  TRACELY_APP_SECRET=your-secret \
  TRACELY_HOST=https://tracely.example.com

注入通过 -ldflags -X 写入 internal/tracelycfg 包的三个变量。Enabled() 函数在三者均非空时返回 true

7.2 运行时行为

启用后,app.goInit 方法中初始化 Tracely 客户端:

  • 心跳:每 60 秒上报一次,携带 version 标签
  • 安装事件:首次启动(tracely_reported_version 为空)时上报,包含版本号、平台(linux-amd64)和主机名
  • 升级事件:版本号变化时上报,包含旧版本和新版本
  • 上报后将当前版本写入 tracely_reported_version 配置持久化,避免重复上报

开源构建默认不注入这三个值,Tracely 客户端不会被初始化,不产生任何网络请求。

章节来源


8. 安全考虑

8.1 默认凭证

Songloft 的默认管理员账号密码均为 admin/admin。当使用默认凭证启动时,日志会输出明确提示:

使用默认管理员账号密码启动
默认管理员账号: admin,默认密码: admin

AppConfig.UsingDefaultCredentials 布尔值标记当前是否使用默认凭证,生产环境应通过 -username/-password 参数或 ADMIN_USERNAME/ADMIN_PASSWORD 环境变量设置强密码。

8.2 JWT Secret 自动生成

首次启动时,initJWTSecret 检查数据库 configs 表中是否已有 jwt_secret。若不存在,调用 GenerateSecret() 生成 32 字节(256 位)随机密钥并持久化到数据库:

go
func GenerateSecret() (string, error) {
    bytes := make([]byte, 32)
    if _, err := rand.Read(bytes); err != nil {
        return "", err
    }
    return hex.EncodeToString(bytes), nil
}

JWT Secret 存储在 SQLite 数据库中(而非配置文件),随 data 卷一起持久化。后续启动直接读取已有密钥,保证 token 跨重启有效。

8.3 SSRF 防护

HLS 代理等涉及服务端发起 HTTP 请求的功能采用两道 SSRF 防线:

第一道:同源校验 -- HLS 代理端点在入口检查请求 URL 的 scheme+host+port 是否与歌曲原始 URL 严格相等,非同源 URL 保持原样不改写,避免成为开放代理。

第二道:内网封禁 -- services.IsHostnameAllowed 采用黑名单策略,阻止访问以下地址:

  • localhost.local 后缀域名
  • 回环地址(127.0.0.0/8::1
  • 私有地址(10.0.0.0/8172.16.0.0/12192.168.0.0/16fc00::/7
  • 链路本地地址(169.254.0.0/16fe80::/10
  • 未指定地址(0.0.0.0::

解析失败的域名会放行,交由后续 HTTP 请求自行报错。

8.4 插件沙盒隔离

JS 插件运行在 QuickJS 沙盒中,通过 internal/jsruntime 提供有限的 host 桥接能力(http.fetchstoragelogger)。插件的权限由 manifest 中的 permissions 字段声明,运行时由 internal/jsplugin 校验。未声明的权限调用将被拒绝。

8.5 全局 HTTP 代理

internal/httputil/proxy.go 维护全局代理配置。loopback 地址(localhost127.0.0.1::1)自动跳过代理,避免内部请求被转发到外部代理服务器。代理配置变更时调用 sharedTransport.CloseIdleConnections() 清理旧连接。

章节来源


9. 平台适配注意事项

Songloft 的 Flutter 前端覆盖 6 个平台,以下是各平台的已知适配要点。

9.1 macOS

  • secure_storage(Flutter Secure Storage)在未签名的沙盒环境下无法使用 Keychain
  • 自动降级到 SharedPreferences 存储 token,安全性较低但功能可用
  • 正式分发应通过 Apple Developer 签名以启用 Keychain

9.2 Android

  • 构建前需运行 sdkmanager --licenses 接受所有许可协议
  • Android 13(API 33)及以上版本需要在运行时申请通知权限(POST_NOTIFICATIONS),否则前台服务通知不会显示
  • HyperOS3 等积极后台管理的 ROM 需设置 androidStopForegroundOnPause: false,防止暂停时前台服务被回收导致播放中断

9.3 Windows / Linux

  • 音频播放后端使用 just_audio_media_kit,底层依赖 libmpv
  • Windows 下 libmpv DLL 需随应用分发或确保系统 PATH 中可找到
  • Linux 下需安装 libmpv(大多数发行版通过包管理器安装 mpv 即可)

9.4 Docker 专属

  • 在线升级检查(/api/v1/upgrade/check)仅在 IN_DOCKER=true 时可用
  • 容器内安装了 ALSA 用户态库(alsa-libalsa-plugins 等),解决 MPD 打开 ALSA 设备时的 "No such file or directory" 错误
  • 默认时区设置为 Asia/Shanghai,可通过 -e TZ=xxx 覆盖

章节来源


10. 运维速查表

10.1 常用环境变量

变量默认值说明
ADMIN_USERNAMEadmin管理员用户名
ADMIN_PASSWORDadmin管理员密码
LISTEN_PORT58091监听端口
DB_PATHdata/songloft.db数据库文件路径
BASE_PATH反向代理子路径前缀
IN_DOCKERtrue(Docker 内)启用 Docker 专属功能
TZAsia/Shanghai容器时区

10.2 关键 API 端点

端点方法说明
/api/v1/upgrade/checkGET检查可用更新(仅 Docker)
/api/v1/upgrade/executePOST执行升级
/api/v1/upgrade/progressGET查询升级进度
/api/v1/upgrade/resetPOST回退到底包版本
/api/v1/settings/http-proxyGET/PUT全局 HTTP 代理配置
/api/v1/settings/hls-proxyGET/PUTHLS 代理开关
/api/v1/cache-manage/configGET/PUT音乐缓存配置
/api/v1/cache-manage/statsGET缓存统计信息
/api/v1/settings/log-levelGET/PUT运行时日志等级切换

10.3 故障排查

症状可能原因解决方案
升级后启动失败新版本二进制损坏重启容器触发 entrypoint 底包回退,或调用 /api/v1/upgrade/reset
升级检查返回 403GitHub API 限流或网络问题配置 HTTP 代理(/settings/http-proxy)或 GitHub 镜像
跨设备 rename 失败缓存目录与 /tmp 不在同一文件系统项目已内置 moveFile 自动回退 copy+remove,无需手动处理
SQLITE_BUSY 错误并发写事务冲突检查是否有多个进程访问同一数据库文件,确保单实例运行
子路径部署 404BASE_PATH 配置不一致确保后端 -base-path 与反向代理配置的路径前缀一致

章节来源