Funkwhale Docker Compose 部署实战

Funkwhale 2.0.12 六服务编排 0→1 实录:端口/网段/目录规划、数据库迁移 236 条、collectstatic 补课、admin 创建与 API 会话登录、原位导入与 Web 播放验证、登录跳转悬空引用等七坑排障。

Funkwhale音频流媒体音乐流媒体2026/10/29

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 由六个协作服务组成:

服务使用版本职责端口规划
apifunkwhale/api:2.0.12Django/gunicorn 应用服务,REST API 与管理后台5000(内部),宿主机映射 127.0.0.1:8140
celeryworkerfunkwhale/api:2.0.12Celery 异步任务:导入扫描、缩略图、联邦投递无
celerybeatfunkwhale/api:2.0.12Celery 周期调度器无
frontfunkwhale/front:2.0.12Nginx:承载 Vue 前端 + 反代 API + 媒体文件保护80 → 宿主机 8138(对外唯一入口)
postgrespostgres:15-alpine唯一受支持的数据库5432(内部)
redisredis: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(选型篇已核验)。