部署与运维
本文档基于以下源文件编写:
- Dockerfile -- 多阶段 Docker 构建定义
- scripts/docker-entrypoint.sh -- 容器启动入口与热替换逻辑
- internal/services/upgrade_service.go -- 在线升级服务(下载/测试/原子替换/回退)
- internal/app/app.go -- 应用初始化、配置解析、Start 方法
- internal/app/embed.go -- SPA 静态文件服务与 base href 注入
- internal/config/types.go -- 启动配置结构 AppConfig
- internal/version/version.go -- 版本号编译时注入
- internal/tracelycfg/tracelycfg.go -- Tracely 监控编译时注入
- internal/httputil/proxy.go -- 全局 HTTP 代理配置
- internal/services/whitelist.go -- SSRF 防护(内网封禁)
- internal/services/auth_service.go -- JWT 密钥生成与认证
- Makefile -- 构建系统(VERSION/BUILD_TYPE/LDFLAGS)
目录
1. 简介
Songloft 支持两种主要部署方式:Docker 容器和独立二进制。Docker 方式提供完整的容器化体验,包括热替换升级和自动重启;独立二进制方式则适用于裸机或不使用容器的环境。两种方式共享同一套启动参数体系,均支持反向代理子路径部署。
本文档覆盖从构建到生产运行的完整运维链路,包括构建变体选择、部署配置、在线升级、监控接入和安全加固。
章节来源
- Dockerfile:1-99 -- 完整 Docker 构建与运行时定义
- internal/app/app.go:459-483 -- Start 方法与 base-path 路由挂载
2. 构建系统
Songloft 的构建系统由两个正交维度控制:
| 维度 | 可选值 | 说明 |
|---|---|---|
| VERSION | dev / X.Y.Z | 控制是否为开发版。dev 时 Makefile 自动启用 -tags dev,包含 Swagger UI 和 pprof |
| BUILD_TYPE | lite / 空(即 full) | 控制是否嵌入 Flutter Web 前端。lite 以纯 API 模式运行 |
两个维度严格分离,禁止 BUILD_TYPE=dev 等混合值。
2.1 构建命令
make build # 开发版(完整,嵌入前端,含 Swagger + pprof)
make build-lite # 开发版(精简,不嵌入前端)
make build-prod # 生产版(完整,嵌入前端)
make build-prod-lite # 生产版(精简,不含前端)2.2 编译时注入
Makefile 通过 -ldflags -X 将以下变量注入到二进制中:
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章节来源
- Makefile:4-26 -- VERSION、BUILD_TYPE、LDFLAGS 定义
- Makefile:100-125 -- 四种构建目标
- internal/version/version.go:1-24 -- 版本号变量定义
3. Docker 部署
3.1 多阶段构建
Dockerfile 采用两阶段构建:
阶段一:go-builder(golang:1.26-alpine)
- 安装
gcc musl-dev make upx git编译工具链(带重试) - 先复制
go.mod/go.sum下载依赖(利用 Docker 层缓存加速) - 再复制源码,使用
--mount=type=cache缓存 Go 编译产物和模块目录 - 根据
LITE_BUILD参数选择make build-prod或make build-prod-lite
阶段二:运行时(alpine:latest)
- 安装
ca-certificates tzdata alsa-lib alsa-plugins alsa-utils alsa-ucm-conf(ALSA 运行时库) - 从
hanxi/ffmpeg镜像复制ffmpeg和ffprobe到/bin/ - 从 go-builder 复制编译好的
songloft二进制到/app/songloft - 复制
docker-entrypoint.sh到/app/ - 默认时区
Asia/Shanghai,暴露端口58091
图表来源
- Dockerfile:1-58 -- 构建阶段
- Dockerfile:59-99 -- 运行时阶段
3.2 卷挂载与环境变量
Docker 镜像定义两个 VOLUME:
| 挂载点 | 用途 | 建议 |
|---|---|---|
/app/music | 音乐文件存储目录 | -v /your/music/path:/app/music |
/app/data | 应用数据(数据库、缓存、插件、运行二进制) | -v /your/data/path:/app/data |
内置环境变量:
| 变量 | 默认值 | 说明 |
|---|---|---|
ADMIN_USERNAME | admin | 管理员用户名 |
ADMIN_PASSWORD | admin | 管理员密码 |
IN_DOCKER | true | 标识 Docker 环境,启用升级检查等 Docker 专属功能 |
LISTEN_PORT | 58091 | 监听端口 |
DB_PATH | data/songloft.db | 数据库文件路径 |
BASE_PATH | 空 | 反向代理子路径前缀 |
典型 docker run 命令:
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章节来源
- Dockerfile:84-97 -- VOLUME 与 ENV 声明
- internal/app/app.go:547-637 -- ParseConfig 环境变量解析
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 在线升级过 |
热替换流程:
- 首次启动(data 目录无二进制):直接从底包复制
- 后续启动:通过
songloft -version获取双方版本号和 BUILD_TYPE - 满足替换条件时:先备份旧版本到
/app/songloft.backup,再复制底包 - 不满足替换条件时:保留 data 目录中的二进制(可能是在线升级后的更新版本)
- 最终
exec /app/data/songloft "$@"启动服务
版本比较使用 awk 逐段比较数字部分(自动去除 -beta 等后缀),dev 和 unknown 版本不参与数值比较。
章节来源
- scripts/docker-entrypoint.sh:1-173 -- 完整入口脚本
- scripts/docker-entrypoint.sh:16-64 -- 版本比较函数
- scripts/docker-entrypoint.sh:92-159 -- 热替换决策逻辑
4. 独立二进制部署
无需 Docker 时,可直接下载或编译二进制运行。
4.1 CLI 参数
./songloft [参数]| 参数 | 默认值 | 说明 |
|---|---|---|
-port | 58091 | 监听端口 |
-db | data/songloft.db | 数据库文件路径 |
-username | admin | 管理员用户名 |
-password | admin | 管理员密码 |
-base-path | 空 | 反向代理子路径前缀(如 /songloft) |
-version | -- | 显示版本信息后退出 |
-help | -- | 显示帮助信息后退出 |
参数优先级:CLI 参数 > 环境变量 > 默认值。所有 CLI 参数都有对应的环境变量(LISTEN_PORT、DB_PATH、ADMIN_USERNAME、ADMIN_PASSWORD、BASE_PATH)。
4.2 启动示例
# 最简启动(使用所有默认值)
./songloft
# 自定义端口和凭证
./songloft -port 8080 -username myuser -password mypassword
# 指定数据库路径
./songloft -db /var/lib/songloft/songloft.db
# 子路径部署
./songloft -base-path /music4.3 数据目录结构
启动后会自动创建以下目录结构(相对于数据库文件所在目录):
data/
├── songloft.db # SQLite 数据库(WAL 模式)
├── covers/ # 封面图片存储
├── jsplugins/ # JS 插件安装目录
├── jsplugins_data/ # JS 插件数据目录(storage API 持久化)
└── music_cache/ # 远程音乐缓存目录(可自定义路径)章节来源
- internal/app/app.go:547-637 -- ParseConfig 参数解析
- internal/app/app.go:510-528 -- showHelp 输出
- internal/config/types.go:1-12 -- AppConfig 结构定义
5. 反向代理与子路径部署
当 Songloft 部署在反向代理(Nginx、Caddy、Traefik 等)后面并需要挂载到子路径时(如 https://example.com/songloft/),需要配置 BASE_PATH。
5.1 后端处理
启动时通过 -base-path /songloft 或环境变量 BASE_PATH=/songloft 配置。normalizeBasePath 函数执行以下规范化:
- 确保以
/开头 - 去除尾部
/ - 拒绝包含
?、#、..的路径
Start 方法中的路由挂载:
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/">:
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 配置示例
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。
章节来源
- internal/app/app.go:531-544 -- normalizeBasePath 规范化
- internal/app/app.go:472-480 -- StripPrefix 路由挂载
- internal/app/embed.go:39-54 -- base href 运行时替换
6. 在线升级系统
在线升级仅在 Docker 环境中可用(通过 IN_DOCKER=true 环境变量判断)。
6.1 升级渠道
| 渠道 | version.json 地址 | 说明 |
|---|---|---|
stable | github.com/.../releases/latest/download/version.json | 正式版(最新稳定 Release) |
dev | github.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 转发。
章节来源
- internal/services/upgrade_service.go:22-32 -- 版本文件 URL 与二进制路径常量
- internal/services/upgrade_service.go:278-337 -- UpgradeBinary 完整流程
- internal/services/upgrade_service.go:399-456 -- ResetToBaseImage 回退逻辑
- internal/services/upgrade_service.go:150-200 -- 底包 BUILD_TYPE 检测与平台后缀
7. Tracely 监控
Songloft 集成了 Tracely 监控客户端,用于收集匿名的安装/升级事件和心跳数据。该功能采用 opt-in 模式:仅在编译时注入了 AppID、AppSecret 和 Host 三个值时才启用。
7.1 编译时注入
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.go 的 Init 方法中初始化 Tracely 客户端:
- 心跳:每 60 秒上报一次,携带
version标签 - 安装事件:首次启动(
tracely_reported_version为空)时上报,包含版本号、平台(linux-amd64)和主机名 - 升级事件:版本号变化时上报,包含旧版本和新版本
- 上报后将当前版本写入
tracely_reported_version配置持久化,避免重复上报
开源构建默认不注入这三个值,Tracely 客户端不会被初始化,不产生任何网络请求。
章节来源
- internal/tracelycfg/tracelycfg.go:1-11 -- 编译时注入变量与 Enabled 判断
- internal/app/app.go:325-361 -- Tracely 初始化与事件上报
- Makefile:24-26 -- TRACELY_* 构建参数
8. 安全考虑
8.1 默认凭证
Songloft 的默认管理员账号密码均为 admin/admin。当使用默认凭证启动时,日志会输出明确提示:
使用默认管理员账号密码启动
默认管理员账号: admin,默认密码: adminAppConfig.UsingDefaultCredentials 布尔值标记当前是否使用默认凭证,生产环境应通过 -username/-password 参数或 ADMIN_USERNAME/ADMIN_PASSWORD 环境变量设置强密码。
8.2 JWT Secret 自动生成
首次启动时,initJWTSecret 检查数据库 configs 表中是否已有 jwt_secret。若不存在,调用 GenerateSecret() 生成 32 字节(256 位)随机密钥并持久化到数据库:
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/8、172.16.0.0/12、192.168.0.0/16、fc00::/7) - 链路本地地址(
169.254.0.0/16、fe80::/10) - 未指定地址(
0.0.0.0、::)
解析失败的域名会放行,交由后续 HTTP 请求自行报错。
8.4 插件沙盒隔离
JS 插件运行在 QuickJS 沙盒中,通过 internal/jsruntime 提供有限的 host 桥接能力(http.fetch、storage、logger)。插件的权限由 manifest 中的 permissions 字段声明,运行时由 internal/jsplugin 校验。未声明的权限调用将被拒绝。
8.5 全局 HTTP 代理
internal/httputil/proxy.go 维护全局代理配置。loopback 地址(localhost、127.0.0.1、::1)自动跳过代理,避免内部请求被转发到外部代理服务器。代理配置变更时调用 sharedTransport.CloseIdleConnections() 清理旧连接。
章节来源
- internal/app/app.go:459-465 -- 默认凭证提示
- internal/app/app.go:486-509 -- initJWTSecret 流程
- internal/services/auth_service.go:85-92 -- GenerateSecret 实现
- internal/services/whitelist.go:1-53 -- SSRF 防护完整实现
- internal/httputil/proxy.go:41-52 -- loopback 地址跳过代理
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-lib、alsa-plugins等),解决 MPD 打开 ALSA 设备时的 "No such file or directory" 错误 - 默认时区设置为
Asia/Shanghai,可通过-e TZ=xxx覆盖
章节来源
- Dockerfile:60-69 -- ALSA 运行时库安装
- internal/services/upgrade_service.go:55-57 -- Docker 环境检测
10. 运维速查表
10.1 常用环境变量
| 变量 | 默认值 | 说明 |
|---|---|---|
ADMIN_USERNAME | admin | 管理员用户名 |
ADMIN_PASSWORD | admin | 管理员密码 |
LISTEN_PORT | 58091 | 监听端口 |
DB_PATH | data/songloft.db | 数据库文件路径 |
BASE_PATH | 空 | 反向代理子路径前缀 |
IN_DOCKER | true(Docker 内) | 启用 Docker 专属功能 |
TZ | Asia/Shanghai | 容器时区 |
10.2 关键 API 端点
| 端点 | 方法 | 说明 |
|---|---|---|
/api/v1/upgrade/check | GET | 检查可用更新(仅 Docker) |
/api/v1/upgrade/execute | POST | 执行升级 |
/api/v1/upgrade/progress | GET | 查询升级进度 |
/api/v1/upgrade/reset | POST | 回退到底包版本 |
/api/v1/settings/http-proxy | GET/PUT | 全局 HTTP 代理配置 |
/api/v1/settings/hls-proxy | GET/PUT | HLS 代理开关 |
/api/v1/cache-manage/config | GET/PUT | 音乐缓存配置 |
/api/v1/cache-manage/stats | GET | 缓存统计信息 |
/api/v1/settings/log-level | GET/PUT | 运行时日志等级切换 |
10.3 故障排查
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 升级后启动失败 | 新版本二进制损坏 | 重启容器触发 entrypoint 底包回退,或调用 /api/v1/upgrade/reset |
| 升级检查返回 403 | GitHub API 限流或网络问题 | 配置 HTTP 代理(/settings/http-proxy)或 GitHub 镜像 |
| 跨设备 rename 失败 | 缓存目录与 /tmp 不在同一文件系统 | 项目已内置 moveFile 自动回退 copy+remove,无需手动处理 |
| SQLITE_BUSY 错误 | 并发写事务冲突 | 检查是否有多个进程访问同一数据库文件,确保单实例运行 |
| 子路径部署 404 | BASE_PATH 配置不一致 | 确保后端 -base-path 与反向代理配置的路径前缀一致 |
章节来源
- internal/app/app.go:87-92 -- 日志等级动态切换
- internal/services/upgrade_service.go:35-52 -- UpgradeService 结构与 HTTP 客户端
