Funkwhale Docker Compose 部署实战
Funkwhale 2.0.12 六服务编排 0→1 实录:端口/网段/目录规划、数据库迁移 236 条、collectstatic 补课、admin 创建与 API 会话登录、原位导入与 Web 播放验证、登录跳转悬空引用等七坑排障。
Funkwhale Docker Compose 部署实战
本文在一台真实 Linux 服务器上把 Funkwhale 2.0.12 从零拉起:官方多服务编排模板 + 自备 PostgreSQL/Redis 的六服务栈、数据库迁移、静态资源收集、管理员创建(admin / q1w2e3r4)、原位导入曲库、Web 播放验证,并把全程踩到的坑——登录跳转
users:redirect硬编码、静态资源 SPA 兜底假 200、/music只读、CSRF 403、非交互 CLI 中止——一并沉淀为排障实录。
认知与选型先看 《Funkwhale 介绍与选型指南》;本文所有变量含义的逐项拆解见《Funkwhale 配置参数详解》,日常管理动作见《Funkwhale 常用命令与运维实战(核心篇)》。
一、部署目标与整体规划
1.1 目标形态
在单台 Linux 服务器上跑起一个功能完整的 Funkwhale pod(实例),版本锁定 2.0.12(当前最新稳定版,发布于 2026-10-15)。按官方多服务编排模板,一个完整 pod 由六个协作服务组成:
| 服务 | 使用版本 | 职责 | 端口规划 |
|---|---|---|---|
| api | funkwhale/api:2.0.12 | Django/gunicorn 应用服务,REST API 与管理后台 | 5000(内部),宿主机映射 127.0.0.1:8140 |
| celeryworker | funkwhale/api:2.0.12 | Celery 异步任务:导入扫描、缩略图、联邦投递 | 无 |
| celerybeat | funkwhale/api:2.0.12 | Celery 周期调度器 | 无 |
| front | funkwhale/front:2.0.12 | Nginx:承载 Vue 前端 + 反代 API + 媒体文件保护 | 80 → 宿主机 8138(对外唯一入口) |
| postgres | postgres:15-alpine | 唯一受支持的数据库 | 5432(内部) |
| redis | redis:7-alpine | 缓存、会话与 Celery 消息中间件 | 6379(内部) |
💡 技术拓展:为什么是六个服务? Funkwhale 的后端是 Python/Django 体系,导入扫描、封面缩略图、联邦消息投递这类慢操作全部走 Celery 异步队列,所以 worker 与 beat 必须独立成服务(官方编排还允许 worker 单独扩容);前端是独立的 Vue 应用,由自带的 Nginx 承载并反代后端,浏览器只访问这一个入口。PostgreSQL 12+ 是 2.x 的硬性要求,Redis 则同时充当 Django 缓存与 Celery 消息中间件(两者用不同的逻辑库号隔离,见 2.2 节)。
1.2 端口与网段规划
对外只暴露 front 的 8138(0.0.0.0),API 的 8140 仅绑 127.0.0.1 供服务器本机调试,PostgreSQL/Redis 不对宿主机暴露端口。选端口前先确认未被占用:
ss -ltn | grep -E '8138|8140'
实测输出为空(端口可用)。给整个栈建独立自定义网络,网段 10.89.61.0/24,创建前先扫一遍既有网段防冲突:
for n in $(docker network ls --format '{{.Name}}'); do
docker network inspect "$n" --format '{{.Name}} {{range .IPAM.Config}}{{.Subnet}}{{end}}'
done
实测输出(节选,确认 10.89.61.0/24 空闲后才写入编排文件):
navidrome-0642-net 10.89.60.0/24
funkwhale-2.0.12-net 10.89.61.0/24
1.3 目录规划
所有持久化数据集中在一个 stack 目录下,便于整目录备份与迁移:
/opt/data/quzuo/tmp/funkwhale/stack/
├── docker-compose.yml
├── .env
├── backup/ # 备份产物落点(运维篇实测)
└── data/
├── music/ # 待导入的音频文件(原位导入源目录)
├── media/ # Funkwhale 媒体库(上传文件、缩略图)
├── static/ # Django 静态资源(collectstatic 产物)
├── postgres/ # 数据库数据目录
└── redis/ # Redis 持久化目录
二、编排文件与环境变量
2.1 docker-compose.yml 全文
以下为实测可直接使用的编排文件(服务名与官方模板一致, funkwhale-manage 等入口均由官方 funkwhale/api 发行版内置):
name: funkwhale-2012-stack
services:
postgres:
image: postgres:15-alpine
container_name: funkwhale-2.0.12-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=${DATABASE_USER}
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
- POSTGRES_DB=${DATABASE_NAME}
volumes:
- ./data/postgres:/var/lib/postgresql/data
networks:
- funkwhale-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER} -d ${DATABASE_NAME}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: funkwhale-2.0.12-redis
restart: unless-stopped
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
volumes:
- ./data/redis:/data
networks:
- funkwhale-net
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
api:
image: funkwhale/api:${FUNKWHALE_VERSION}
container_name: funkwhale-2.0.12-api
restart: unless-stopped
command: gunicorn
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
env_file: .env
volumes:
- '${MUSIC_DIRECTORY_SERVE_PATH}:${MUSIC_DIRECTORY_PATH}:ro'
- '${MEDIA_ROOT}:${MEDIA_ROOT}'
- '${STATIC_ROOT}:${STATIC_ROOT}'
ports:
- '127.0.0.1:8140:5000'
networks:
- funkwhale-net
celeryworker:
image: funkwhale/api:${FUNKWHALE_VERSION}
container_name: funkwhale-2.0.12-celery-worker
restart: unless-stopped
command:
- celery
- --app=funkwhale_api.taskapp
- worker
- --loglevel=${LOGLEVEL}
- --concurrency=${CELERYD_CONCURRENCY}
environment:
- C_FORCE_ROOT=true
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
env_file: .env
volumes:
- '${MUSIC_DIRECTORY_SERVE_PATH}:${MUSIC_DIRECTORY_PATH}:ro'
- '${MEDIA_ROOT}:${MEDIA_ROOT}'
networks:
- funkwhale-net
celerybeat:
image: funkwhale/api:${FUNKWHALE_VERSION}
container_name: funkwhale-2.0.12-celery-beat
restart: unless-stopped
command:
- celery
- --app=funkwhale_api.taskapp
- beat
- --loglevel=${LOGLEVEL}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
env_file: .env
networks:
- funkwhale-net
front:
image: funkwhale/front:${FUNKWHALE_VERSION}
container_name: funkwhale-2.0.12-front
restart: unless-stopped
depends_on:
- api
env_file: .env
environment:
- 'NGINX_MAX_BODY_SIZE=${NGINX_MAX_BODY_SIZE}'
volumes:
- '${MUSIC_DIRECTORY_SERVE_PATH}:${MUSIC_DIRECTORY_PATH}:ro'
- '${MEDIA_ROOT}:${MEDIA_ROOT}:ro'
- '${STATIC_ROOT}:/usr/share/nginx/html/staticfiles:ro'
ports:
- '0.0.0.0:8138:80'
networks:
- funkwhale-net
networks:
funkwhale-net:
name: funkwhale-2.0.12-net
driver: bridge
ipam:
config:
- subnet: 10.89.61.0/24
三个值得注意的编排细节:
api的启动入口是command: gunicorn:官方 api 发行版内置了 entrypoint 分发器,gunicorn分支展开为gunicorn config.asgi:application --workers ${FUNKWHALE_WEB_WORKERS} --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:${FUNKWHALE_API_PORT},worker 数与端口都由 .env 驱动,无需改动编排文件;- 音乐目录以只读方式挂载(
...:ro),且挂载点路径来自MUSIC_DIRECTORY_PATH变量——原位导入的物理前提,6.3 节有踩坑实录; - api 与 worker 挂同一份
MEDIA_ROOT读写,front 只读挂载 media 与 static——文件由 api/worker 写、由 front 读,职责分明。
2.2 .env 全文与变量分组
FUNKWHALE_VERSION=2.0.12
FUNKWHALE_API_IP=0.0.0.0
FUNKWHALE_API_PORT=5000
FUNKWHALE_WEB_WORKERS=2
FUNKWHALE_HOSTNAME=funkwhale.lab
FUNKWHALE_PROTOCOL=http
LOGLEVEL=info
EMAIL_CONFIG=consolemail://
DEFAULT_FROM_EMAIL=noreply@funkwhale.lab
REVERSE_PROXY_TYPE=nginx
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_NAME=funkwhale
DATABASE_USER=funkwhale
DATABASE_PASSWORD=fw_db_2o12_Passw0rd
REDIS_PASSWORD=fw_redis_2o12_Passw0rd
CACHE_URL=redis://:fw_redis_2o12_Passw0rd@redis:6379/0
CELERY_BROKER_URL=redis://:fw_redis_2o12_Passw0rd@redis:6379/1
CELERYD_CONCURRENCY=2
MEDIA_ROOT=/opt/data/quzuo/tmp/funkwhale/stack/data/media
STATIC_ROOT=/opt/data/quzuo/tmp/funkwhale/stack/data/static
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY=Zx8vQ2mR5tY7uI9oP1aS3dF6gH4jK0lB2nC5vX8zQ1wErT4yU6iO0pA9sD3fG5h
DJANGO_ADMIN_URL=^api/admin/
MUSIC_DIRECTORY_PATH=/music
MUSIC_DIRECTORY_SERVE_PATH=/opt/data/quzuo/tmp/funkwhale/stack/data/music
FUNKWHALE_FRONTEND_PATH=/srv/funkwhale/front/dist
NGINX_MAX_BODY_SIZE=100M
按职责分组速读(逐项详解见《Funkwhale 配置参数详解》):
- 版本与身份:
FUNKWHALE_VERSION同时决定 api/worker/beat/front 四个服务的版本标签;FUNKWHALE_HOSTNAME+FUNKWHALE_PROTOCOL拼出实例的对外地址FUNKWHALE_URL(联邦 actor ID、密码重置链接、导入结果回显 URL 全部以它为根)。官方明确警告:实例上线后不可再改,否则联邦身份断裂; - 数据库:
DATABASE_HOST/PORT/NAME/USER/PASSWORD五件套。2.0.3 的更新日志明确要求 Docker 部署必须显式补齐这三个变量(DATABASE_HOST/DATABASE_USER/DATABASE_NAME)——不写会导致服务启动即失败,这是 2.x 升级迁移的高频翻车点; - 缓存与队列:
CACHE_URL用 0 号库、CELERY_BROKER_URL用 1 号库,同一 Redis 实例逻辑隔离;REDIS_PASSWORD供编排文件注入 Redis 服务端的--requirepass; - 存储路径:
MEDIA_ROOT/STATIC_ROOT用宿主机绝对路径;MUSIC_DIRECTORY_PATH=/music是服务内视角的挂载点,MUSIC_DIRECTORY_SERVE_PATH是宿主机真实目录,两者必须配对出现; - 安全杂项:
DJANGO_SECRET_KEY(生产模式必填,缺失直接启动失败)、DJANGO_ADMIN_URL(改掉默认管理后台路径)、EMAIL_CONFIG=consolemail://(测试期把系统邮件打印到 api 日志,不外发)。
2.3 拉起栈与状态确认
docker compose up -d
实测输出(节选;postgres/redis 健康检查通过后 api/worker/beat 才被拉起,这是编排里 depends_on: condition: service_healthy 的效果):
Container funkwhale-2.0.12-redis Healthy
Container funkwhale-2.0.12-postgres Healthy
随后确认整体状态:
docker compose ps --format 'table {{.Name}}\t{{.Status}}\t{{.Publishers}}'
实测输出:
NAME STATUS <no value>
funkwhale-2.0.12-api Up 3 minutes [5000/tcp -> 127.0.0.1:8140]
funkwhale-2.0.12-celery-beat Up 3 minutes []
funkwhale-2.0.12-celery-worker Up 3 minutes []
funkwhale-2.0.12-front Up 3 minutes [80/tcp -> 0.0.0.0:8138]
funkwhale-2.0.12-postgres Up 45 minutes (healthy) [5432/tcp -> :0]
funkwhale-2.0.12-redis Up 45 minutes (healthy) [6379/tcp -> :0]
此时浏览器访问 http://192.168.146.128:8138/(或把 funkwhale.lab 写进本机 hosts 后访问 http://funkwhale.lab:8138/)已经能看到 Funkwhale 登录页——但先别急着登录,数据库还是空的,继续第三节。
三、初始化:数据库迁移与静态资源
3.1 执行数据库迁移
官方升级文档给出的标准姿势是用一次性进程跑完迁移即退出:
docker compose run --rm api funkwhale-manage migrate
首次部署会依次应用全部 236 条迁移。完成后做两个验证——迁移账本计数与清单尾部:
docker exec funkwhale-2.0.12-postgres psql -U funkwhale -d funkwhale -t -c "SELECT count(*) FROM django_migrations;"
实测输出:
236
docker exec funkwhale-2.0.12-api funkwhale-manage showmigrations users
实测输出(尾部 6 行,[X] 表示已应用):
[X] 0023_merge_20221125_1902
[X] 0024_alter_accesstoken_user_and_more
[X] 0025_application_allowed_origins_and_more
[X] 0026_accesstoken_token_checksum_refreshtoken_token_family_and_more
[X] 0027_alter_user_privacy_level
[X] 0028_check_appliation_scope
迁移是幂等的,重复执行只会得到一句确认(升级演练时的实测回显):
Operations to perform:
Apply all migrations: account, admin, audio, auth, authtoken, common, contenttypes, dynamic_preferences, favorites, federation, history, moderation, music, playlists, portability, radios, requests, sessions, sites, socialaccount, tags, users
Running migrations:
No migrations to apply.
💡 技术拓展:为什么迁移必须显式跑? Funkwhale 2.0.0 对数据库层做了大规模重建,官方在发布博客中反复强调「迁移过程被打断有数据丢失风险」,并建议在 screen/tmux 这类持久会话中执行、升级前先停 worker 清空队列。把迁移做成一次性显式动作(而不是启动时自动跑),正是为了让运维者能在一个受控的时间窗里盯完全程。
3.2 收集静态资源:一个容易漏掉的步骤
本实例部署后访问 Django 管理后台时,样式资源请求返回异常——直接请求 API 侧的静态资源得到 500:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8140/staticfiles/rest_framework/css/default.css
实测输出:
500
而走 front 入口请求同名路径却显示 200——但细看响应头会发现问题:Content-Type: text/html,返回的其实是 Vue 单页应用的 index.html(front 对未知路径一律回退到 SPA 页面),是一个假 200。检查 STATIC_ROOT 指向的宿主机目录,确认是空的——静态资源从未被收集过。
官方 api 发行版的 entrypoint 分发器内置了 collectstatic 分支,直接调用:
docker exec funkwhale-2.0.12-api /app/entrypoint.sh collectstatic
实测输出(尾部):
174 static files copied to '/opt/data/quzuo/tmp/funkwhale/stack/data/static'.
复测,front 入口这次返回真正的 CSS 文件:
curl -s -o /dev/null -w '%{http_code} %{content_type} %{size_download}B\n' http://127.0.0.1:8138/staticfiles/rest_framework/css/default.css
实测输出:
200 text/css 1152B
管理后台 http://192.168.146.128:8138/api/admin/login/ 此刻样式完整(实测 HTTP 200)。结论:Docker 部署完成迁移后,务必补一步 collectstatic,否则管理后台无样式且极易被 SPA 兜底的假 200 糊弄过去。
3.3 应用服务启动形态自查
api 服务启动日志(docker logs funkwhale-2.0.12-api 头部)能确认 gunicorn 的真实参数来自 .env:
+ exec gunicorn config.asgi:application --workers 2 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:5000
[2026-10-28 15:31:15 +0000] [1] [INFO] Starting gunicorn 26.0.0
[2026-10-28 15:31:15 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
[2026-10-28 15:31:15 +0000] [1] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2026-10-28 15:31:15 +0000] [7] [INFO] Booting worker with pid: 7
[2026-10-28 15:31:15 +0000] [8] [INFO] Booting worker with pid: 8
--workers 2 对应 FUNKWHALE_WEB_WORKERS=2,0.0.0.0:5000 对应 FUNKWHALE_API_PORT=5000。日志里同时可见两条可预期的提示(非错误):未启用 Typesense 搜索、已加载 scrobbler/listenbrainz/maloja 三个听歌记录插件。
四、创建管理员与首次登录
4.1 创建超管:老命令已被官方拦截
Django 老兵会本能地敲 createsuperuser,但 2.0.12 会直接拒绝(实测回显原文):
CommandError: Running createsuperuser on your Funkwhale instance bypasses some of our checks which can lead to unexpected behavior of your instance. We therefore suggest to run `funkwhale-manage fw users create --superuser` instead.
这是官方有意加的引导:绕过它会跳过与联邦 actor 创建、权限初始化绑定的检查。改用官方推荐的新 CLI:
docker exec funkwhale-2.0.12-api funkwhale-manage fw users create \
--username admin --email admin@funkwhale.lab --password 'q1w2e3r4' --superuser
实测回显(以 opsadmin 演示的同型命令,admin 同法创建):
User opsadmin created!
本文实例的正式管理员即 admin / q1w2e3r4,实测可正常登录。
4.2 登录链路实测(curl 全流程复现)
Web 界面的登录实际走的是 API 会话认证,curl 可完整复现。第一步,先向任意匿名可达端点要 CSRF 凭证(nodeinfo 端点会种下 csrftoken):
curl -s -c /tmp/fw_cookies.txt -o /dev/null http://127.0.0.1:8140/api/v1/instance/nodeinfo/2.0/
CSRF=$(grep csrftoken /tmp/fw_cookies.txt | awk '{print $7}')
第二步,带 CSRF 头提交登录表单:
curl -s -b /tmp/fw_cookies.txt -c /tmp/fw_cookies.txt -o /dev/null -w 'HTTP %{http_code}\n' \
-X POST http://127.0.0.1:8140/api/v2/users/login \
-d 'username=admin&password=q1w2e3r4' \
-H "X-CSRFToken: $CSRF" -H 'Referer: http://funkwhale.lab/'
实测输出:
HTTP 200
第三步,用会话凭证取当前用户:
curl -s -b /tmp/fw_cookies.txt http://127.0.0.1:8140/api/v2/auth/user/
实测输出:
{"pk":1,"username":"admin","email":"admin@funkwhale.lab","first_name":"","last_name":""}
💡 技术拓展:API 版本 v1/v2 并存怎么选? 2.0.0 起官方 API 主版本为 v2(
/api/v2/...),v1 仍保留少量端点(如实例设置、nodeinfo)。登录端点在 v2 侧的/api/v2/users/login,而 nodeinfo 还挂在 v1 路径下——混用是正常现象,按端点各自的归属路径访问即可。
4.3 浏览器侧登录
浏览器打开 http://192.168.146.128:8138/,用 admin / q1w2e3r4 登录即可进入主界面。注意一个已知问题:登录跳转目标 LOGIN_REDIRECT_URL 指向一个不存在的路由名 users:redirect(详见 6.1 节排障实录),Web 界面日常登录不受影响(前端自行路由),但任何依赖服务端登录跳转的流程都可能在此栽跟头。
五、导入音乐与 Web 播放验证
5.1 曲库模型与原位导入
Funkwhale 是「导入式」曲库:音频文件要先归属到某个 library(曲库,带可见性级别),再由扫描任务解析元数据入库。本文在 Web 界面建了四个可见性各异的曲库实测(API 回显):
3e58bf4e | everyone | privacy: everyone | uploads: 0
a2b1e8f7 | instance | privacy: instance | uploads: 3
b8339304 | followers| privacy: followers | uploads: 0
cf381d61 | me | privacy: me | uploads: 0
导入方式有两种:复制导入(文件复制进 MEDIA_ROOT)与原位导入(--in-place,曲库只登记文件位置,适合大曲库省磁盘)。原位导入要求 .env 配好 MUSIC_DIRECTORY_PATH(服务内挂载点)与 MUSIC_DIRECTORY_SERVE_PATH(宿主机真实目录),且音乐目录对 api 服务是只读挂载——6.3 节有一个因此踩到的坑。
5.2 造一条测试音轨
准备一首带完整 ID3 标签的测试音轨。宿主机 ffmpeg 因系统库冲突不可用(GLIBCXX_3.4.32 not found,6.7 节),但 api 发行版内置了可用的 ffmpeg。三步走,全程在服务内的 /tmp 完成:
# 第 1 步:生成 20 秒 1100Hz 正弦波 WAV
docker exec funkwhale-2.0.12-api ffmpeg -loglevel error \
-f lavfi -i 'sine=frequency=1100:duration=20' \
-ar 44100 -ac 1 -b:a 192k /tmp/tone.wav
# 实测产物:/tmp/tone.wav 1764078 字节
# 第 2 步:WAV 转码为 MP3(PCM 不能 -c copy 直拷进 MP3,必须重编码)
docker exec funkwhale-2.0.12-api ffmpeg -loglevel error -y \
-i /tmp/tone.wav -b:a 192k /tmp/04tone.mp3
# 第 3 步:MP3 流内补写 ID3 标签(-c copy 不重编码)
docker exec funkwhale-2.0.12-api ffmpeg -loglevel error -y \
-i /tmp/04tone.mp3 -c copy \
-metadata title='Tone 1100Hz' -metadata artist='Lab Signal Generator' \
-metadata album='Lab Tones Vol.1' -metadata date='2026' -metadata track='4' \
/tmp/04tagged.mp3
把成品拷到宿主机音乐目录。注意 /music 在 api 服务内是只读挂载,不能从服务内直接写(实测报错原文见 6.3 节):
docker cp funkwhale-2.0.12-api:/tmp/04tagged.mp3 \
'/opt/data/quzuo/tmp/funkwhale/stack/data/music/04 - Tone 1100Hz.mp3'
成品实测为 481644 字节、20 秒、192kbps 单声道。
5.3 CLI 导入实测
把文件原位导入 instance 曲库(曲库 ID 取 uuid 前缀 a2b1e8f7 即可):
docker exec funkwhale-2.0.12-api funkwhale-manage import_files \
a2b1e8f7 '/music/04 - Tone 1100Hz.mp3' \
--in-place --username admin --reference ops-import-002 --noinput
实测输出全文:
Checking imported paths against settings.MUSIC_DIRECTORY_PATH
For details, please refer to import reference 'ops-import-002' or URL http://funkwhale.lab/library/a2b1e8f7-ecc5-4e1a-881e-73317f5630f6/upload?import=ops-import-002
Checking existing files for updates…
Found 0 files to check in database!
Handling batch 1 (0 items)
Existing files checked, moving on to next step!
Starting import of new files…
Handling batch 1 (1 items)
Import summary:
- 1 files found matching this pattern: ['/music/04 - Tone 1100Hz.mp3']
- 0 files already found in database
- 1 new files
Selected options: in place
2026-10-28 16:07:26,711 funkwhale_api.music.tasks INFO [Album 2] scanning covers from /music
Successfully imported 1 new tracks in 0s
💡 技术拓展:reference 是什么?
--reference给导入批次一个自定义引用号,导入结果页、任务日志、数据库查询都能凭它串起「一次批量导入」的完整轨迹。上面的回显 URL 里?import=ops-import-002就是 Web 界面查看该批次详情的入口。不加该参数时会自动生成一个随机引用号。
确认曲目入库(API 回显,count 从 3 变 4):
curl -s -b /tmp/fw_cookies.txt 'http://127.0.0.1:8140/api/v2/tracks/' | python3 -c "
import json,sys
d=json.load(sys.stdin)
print('count:', d['count'])
for t in d['results']: print(t['id'],'|',t['title'])"
实测输出:
count: 4
4 | Tone 1100Hz
3 | Tone 880Hz
2 | Tone 660Hz
1 | Tone 440Hz
Web 界面的「上传/导入」走的是同一套任务链(界面导入批次的引用号为 fs-import-001,worker 日志可见 Task music.fs_import[...] succeeded),大曲库批量灌库时 CLI 的 import_files 效率远高于网页逐批上传。
5.4 Web 播放验证
取曲目 1 的流地址并实测下载:
curl -s -b /tmp/fw_cookies.txt -o /tmp/fw_track1.mp3 \
-w 'listen HTTP %{http_code} type=%{content_type} size=%{size_download}\n' \
'http://192.168.146.128:8138/api/v2/listen/28719452-f674-4f16-bd27-86e024bba7de/'
file /tmp/fw_track1.mp3
实测输出:
listen HTTP 200 type=audio/mpeg size=841487
/tmp/fw_track1.mp3: Audio file with ID3 version 2.4.0, contains: MPEG ADTS, layer III, v1, 192 kbps, 44.1 kHz, Monaural
经 front 入口拉到完整音频(841487 字节与源文件一致),浏览器播放器与 Subsonic 客户端走的正是这条链路。至此部署闭环:登录 → 曲库 → 导入 → 播放全部可用。
六、排障实录
以下七个坑均为本实例部署/验证过程中的真实报错与结论,按踩雷概率排序。
6.1 登录跳转目标 users:redirect 是悬空引用(2.0.12 实锤)
现象:排查登录链路时发现配置里硬编码了一个登录后跳转目标,且它指向的路由并不存在。
定位:config/settings/common.py 第 740 行(2.0.12 发行版原文):
LOGIN_REDIRECT_URL = "users:redirect"
LOGIN_URL = "account_login"
在整个设置树中 grep 确认:LOGIN_REDIRECT_URL 仅此一处赋值,不读取任何环境变量,无法通过 .env 修正。再在 api 进程环境里实测反向解析这个路由名:
NoReverseMatch : 'users' is not a registered namespace
即 users 这个 URL 命名空间根本没注册,users:redirect 是一个悬空引用。
影响与规避:Web 界面日常登录不受影响——前端登录成功后由 Vue 路由自行跳转(API 侧只返回 200 + 会话 cookie,实测响应头 set-cookie: sessionid=...; csrftoken=...)。受影响的是任何依赖服务端登录跳转目标的流程(如直接访问 /accounts/ 系表单流、邮件验证/密码流中回落到 LOGIN_REDIRECT_URL 的分支)。规避方式:一律走 Web 界面或 /api/v2/users/login 的 API 登录;升级版本时留意官方是否修正该引用。
6.2 静态资源 500 与 SPA 兜底假 200
现象:/staticfiles/ 下请求真实 CSS,直连 API 得 500,经 front 得 200——但 200 的 Content-Type 是 text/html,内容是 SPA 的 index.html。
结论:collectstatic 未执行时 STATIC_ROOT 为空,Django 侧 500;front 对未知路径一律回退 SPA 页面,制造了「看起来正常」的假 200。解法与复测见 3.2 节。排障教训:判断静态资源是否正常必须看 Content-Type 与内容本体,不能只看状态码。
6.3 音乐目录只读挂载
现象:在 api 服务内直接往 /music 写文件报错:
Error opening output /music/04 - Tone 1100Hz.mp3: Read-only file system
结论:官方编排把音乐目录挂为只读(...:ro),这是刻意设计——播放服务不需要写权限,只读能防误删曲源。生成/修改测试文件先写 /tmp 再用 docker cp 落到宿主机目录。
6.4 API 登录的 CSRF 403
现象:直接 POST /api/v2/users/login 不带 CSRF 凭证,两种报错形态:
- 完全不带 cookie:
HTTP 403,DRF 风格拒绝; - 带了 cookie 但没带
X-CSRFToken头:返回 Django 的整页 403,标题CSRF verification failed. Request aborted.。
结论:登录是「cookie + 请求头」双因子,先 GET nodeinfo 拿 csrftoken cookie,再回填同名请求头(4.2 节三步完整可用)。Web 界面由前端自动处理,无感知。
6.5 非交互环境的 CLI 中止
现象:非交互执行导入命令时,程序在「确认是否复查已导入文件」的交互提示处直接抛栈退出(实测尾部):
Are you sure you want to do this?
Type 'yes' to continue, or 'no' to skip checking for updates in already imported files: Traceback (most recent call last):
...
if options["interactive"] and input("".join(message)) != "yes":
EOFError: EOF when reading a line
结论:脚本化调用一律带上免交互参数——import_files 加 --noinput;fw users create 缺省会在终端交互要密码,务必显式给 -p,否则在无终端环境同样中止(实测 GetPassWarning: Can not control echo on the terminal 后 Aborted!)。
6.6 数据库三变量缺失即启动失败(2.0.3 起)
结论:2.0.3 的更新日志明确要求 Docker 部署在 .env 显式补齐 DATABASE_HOST/DATABASE_USER/DATABASE_NAME(1.x 时代的老编排模板没有这三项)。从 1.4 升 2.x 时沿用旧 .env 是高频翻车点;另外 1.4.x → 2.x 必须直接升到 2.0.4 以上,不可停在 2.0.0~2.0.2(有迁移缺陷,选型篇有详述)。
6.7 宿主机 ffmpeg 库冲突
现象:宿主机直接调 ffmpeg 报一大片 GLIBCXX_3.4.xx not found(系统被第三方运行时注入了旧版 libstdc++)。结论:api 发行版自带完整可用的 ffmpeg,转码、造测试文件都在服务内完成即可,不必修宿主机。
七、部署完成度自查清单
收尾按此清单逐项打勾(全部为本实例实测通过项):
# 1. 实例身份与版本
curl -s http://127.0.0.1:8140/api/v1/instance/nodeinfo/2.0/ | python3 -m json.tool | grep -A1 software
实测输出:
"software": {
"name": "funkwhale",
"version": "2.0.12"
},
| 检查项 | 通过标准 | 本实例实测 |
|---|---|---|
| 六服务全部运行 | docker compose ps 全 Up,postgres/redis healthy | ✅ |
| 迁移账本 | django_migrations 计数 236 | ✅ |
| 静态资源 | /staticfiles/... 经 front 返回 text/css | ✅(200 text/css 1152B) |
| 管理后台 | /api/admin/login/ 可达 | ✅(HTTP 200) |
| 管理员登录 | admin / q1w2e3r4 会话登录 200 | ✅ |
| 曲库导入 | tracks 计数 4,原位引用 file:///music/... | ✅ |
| Web 播放 | listen 地址 200 + audio/mpeg + 字节数一致 | ✅(841487B) |
后续动作:环境变量逐项调优见《Funkwhale 配置参数详解》;日常管理命令、Celery 任务排查与备份恢复见《Funkwhale 常用命令与运维实战(核心篇)》。
信息来源:本文全部命令与回显来自 2026-10-28 在 192.168.146.128 上的实测(Funkwhale 2.0.12);官方部署与升级流程参考 docs.funkwhale.audio 的《Install Funkwhale using Docker》《Upgrade your Docker Funkwhale installation》;2.0.3 数据库变量要求引自官方 changelog(选型篇已核验)。