MAATRIX / Блог / Funkwhale в Docker Compose: готовый файл

Funkwhale в Docker Compose: готовый файл

MAATRIX

Если SoundCloud кажется вам слишком закрытым, а свой Navidrome — слишком личным (никакого сообщества, никаких подписок на других пользователей), Funkwhale закрывает эту нишу: это федеративная музыкальная платформа на протоколе ActivityPub, то есть ваш инстанс общается с другими Funkwhale, Mastodon и Pleroma напрямую, без централизованного сервера-посредника. Ставится он не в один контейнер, как большинство self-hosted приложений в этом блоге, а как связка из пяти-шести сервисов — и именно поэтому людям, которые его пробуют впервые, нужен рабочий docker-compose.yml, а не абстрактная инструкция. Ниже — конфиг, который поднимается с первого раза, и объяснение, зачем там каждая часть.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что такое Funkwhale и чем он отличается от Navidrome

Funkwhale — это не просто музыкальный сервер для прослушивания своей коллекции (для этого у нас есть отдельная статья про Navidrome в Docker Compose), а социальная платформа поверх музыки. У Funkwhale есть:

  • Федерация через ActivityPub — можно подписаться на библиотеку другого Funkwhale-инстанса или на артиста в Mastodon, и его релизы появятся у вас в ленте;
  • Загрузка и лайблы — пользователи (или сообщество) заливают треки, поддерживаются альбомы, плейлисты, радио по жанрам;
  • Разграничение видимости — трек можно опубликовать только для себя, для подписчиков или для всего федиверса;
  • Подкасты — начиная с относительно свежих версий Funkwhale умеет работать и с подкастами, не только с музыкой.

Если вам нужен просто плеер для личной фонотеки — берите Navidrome, он легче и проще. Если вы хотите площадку для музыкального сообщества, лейбла, подкаст-сети или просто пространство без алгоритмической ленты и рекламы — это к Funkwhale.

Архитектура: из чего состоит стек

В отличие от одноконтейнерных приложений, Funkwhale в продакшен-конфигурации — это набор ролей вокруг общего кода:

СервисРоль
postgresосновная база данных (метаданные, пользователи, треки)
redisочередь задач и кэш
apiDjango-приложение, обслуживает REST API
celeryworkerфоновые задачи: импорт музыки, обработка тегов, федерация
nginxотдаёт фронтенд (SPA) и проксирует /api на api

Официальный образ funkwhale/api и funkwhale/front собираются под конкретную версию — важно, чтобы теги api и front совпадали, иначе фронтенд может ждать эндпоинты, которых ещё нет в API. Ниже используется фиксированная версия, а не latest — это осознанный выбор: latest в мультисервисной схеме, где фронт и бэкенд обновляются раздельно, слишком легко ломает совместимость при случайном docker compose pull.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Готовый docker-compose.yml

Создайте директорию проекта и положите туда файл .env и docker-compose.yml:

mkdir -p ~/funkwhale/{data/music,data/media,data/static,nginx}
cd ~/funkwhale

Файл .env:

# Версия — фиксируйте конкретный релиз, не latest
FUNKWHALE_VERSION=1.4.2

# База данных
POSTGRES_DB=funkwhale
POSTGRES_USER=funkwhale
POSTGRES_PASSWORD=замените-на-длинный-случайный-пароль

# Домен и протокол
FUNKWHALE_HOSTNAME=music.example.com
FUNKWHALE_PROTOCOL=https

# Django secret — сгенерируйте: openssl rand -base64 45
DJANGO_SECRET_KEY=замените-на-случайную-строку

# Почта для системных писем (можно оставить консольный backend на старте)
EMAIL_CONFIG=consolemail://

# Часовой пояс
TZ=Europe/Moscow

Файл docker-compose.yml:

services:
  postgres:
    image: postgres:15-alpine
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - ./data/redis:/data

  api:
    image: funkwhale/api:${FUNKWHALE_VERSION}
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      CACHE_URL: redis://redis:6379/0
      DJANGO_ALLOWED_HOSTS: ${FUNKWHALE_HOSTNAME}
      FUNKWHALE_HOSTNAME: ${FUNKWHALE_HOSTNAME}
      FUNKWHALE_PROTOCOL: ${FUNKWHALE_PROTOCOL}
      DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
      EMAIL_CONFIG: ${EMAIL_CONFIG}
      MEDIA_ROOT: /app/media
      STATIC_ROOT: /app/static
      MUSIC_DIRECTORY_PATH: /music
    volumes:
      - ./data/music:/music
      - ./data/media:/app/media
      - ./data/static:/app/static
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started

  celeryworker:
    image: funkwhale/api:${FUNKWHALE_VERSION}
    restart: unless-stopped
    command: celery -A funkwhale_api.taskapp worker -l INFO
    env_file: .env
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      CACHE_URL: redis://redis:6379/0
      DJANGO_ALLOWED_HOSTS: ${FUNKWHALE_HOSTNAME}
      FUNKWHALE_HOSTNAME: ${FUNKWHALE_HOSTNAME}
      FUNKWHALE_PROTOCOL: ${FUNKWHALE_PROTOCOL}
      DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
      MEDIA_ROOT: /app/media
      MUSIC_DIRECTORY_PATH: /music
    volumes:
      - ./data/music:/music
      - ./data/media:/app/media
      - ./data/static:/app/static
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started

  front:
    image: funkwhale/front:${FUNKWHALE_VERSION}
    restart: unless-stopped

  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:5000:80"
    volumes:
      - ./nginx/funkwhale.conf:/etc/nginx/conf.d/default.conf:ro
      - ./data/media:/protected/media:ro
      - ./data/static:/staticfiles:ro
      - ./data/music:/music:ro
      - front_dist:/frontend:ro
    depends_on:
      - api
      - front

volumes:
  front_dist:

Обратите внимание: nginx слушает только 127.0.0.1:5000 — наружу контейнер не смотрит. Это осознанное решение: перед Funkwhale должен стоять внешний реверс-прокси с TLS, а не сам контейнерный nginx напрямую в интернет.

Конфиг nginx внутри стека

Funkwhale ожидает свой внутренний nginx с довольно специфичной маршрутизацией (статика, медиа с защитой скачивания, прокси на API). Минимальный рабочий nginx/funkwhale.conf:

server {
    listen 80;
    client_max_body_size 100M;

    location / {
        root /frontend;
        try_files $uri $uri/ /index.html;
    }

    location /front/ {
        alias /frontend/;
    }

    location /api/ {
        proxy_pass http://api:5000/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /federation/ {
        proxy_pass http://api:5000/federation/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /staticfiles/ {
        alias /staticfiles/;
    }

    location /protected/media/ {
        internal;
        alias /protected/media/;
    }
}

Это упрощённая версия официального конфига — для честности скажу прямо: если понадобится раздача аватаров, обложек альбомов и превью подкастов в высоком качестве, стоит свериться с актуальным docker/nginx/funkwhale.template в репозитории Funkwhale, там больше нюансов по кэшированию статики.

Первый запуск и создание администратора

docker compose up -d postgres redis
# дождитесь healthy у postgres, затем:
docker compose up -d api celeryworker front nginx

# применяем миграции базы
docker compose run --rm api python manage.py migrate

# создаём администратора
docker compose run --rm api python manage.py createsuperuser

После этого Funkwhale доступен на 127.0.0.1:5000 внутри сервера. Дальше нужен внешний TLS-терминатор.

TLS и публикация наружу

Публиковать nginx из compose напрямую в 443 порт можно, но удобнее держать перед ним отдельный реверс-прокси с автоматическим TLS — это упрощает жизнь, если на сервере крутится несколько сервисов одновременно. Мы разбирали два варианта:

Для Caddy конфиг сведётся к одному блоку:

music.example.com {
    reverse_proxy 127.0.0.1:5000
}

Важный момент для федерации: ActivityPub требует, чтобы FUNKWHALE_HOSTNAME и реальный домен, по которому к вам стучатся другие инстансы, совпадали — иначе подписки и федеративный поиск будут ломаться непредсказуемо. Если домен меняется — это не просто смена DNS-записи, а фактически смена идентичности инстанса в федиверсе.

Хранилище и производительность

Директория ./data/music — это место, откуда Funkwhale импортирует файлы (можно класть их туда напрямую и запускать python manage.py import_files), а ./data/media — куда система сохраняет транскодированные версии, обложки и аватары. На практике для активного инстанса с десятками пользователей место съедается быстрее, чем кажется на старте, особенно если включена транскодировка в несколько битрейтов.

Если у вас уже есть S3-совместимое хранилище (например, поднятый MinIO в Docker Compose), Funkwhale умеет отдавать медиа туда через AWS_* переменные окружения вместо локальной директории — это разгружает диск основного сервера и упрощает бэкапы, но требует отдельной настройки, которую я не привожу здесь целиком, чтобы не гадать на неточных деталях — сверяйтесь с актуальной документацией под вашу версию.

По ресурсам ориентировочно: для инстанса на 5-15 активных пользователей достаточно 2 vCPU и 4 ГБ RAM с запасом на Postgres и Celery-воркер; если планируете активный импорт большой библиотеки или несколько параллельных подкаст-фидов — закладывайте больше, особенно оперативной памяти под обработку метаданных.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Нужен ли отдельный SMTP-сервер для работы Funkwhale?

Не обязательно на старте — можно оставить EMAIL_CONFIG=consolemail://, письма будут падать в логи контейнера api. Но для подтверждения регистрации и восстановления пароля в реальной эксплуатации лучше подключить SMTP через тот же параметр EMAIL_CONFIG.

Можно ли обойтись без Celery-воркера?

Нет — без него не будет работать импорт файлов, обработка федеративных запросов и генерация превью. Это не опциональный компонент, а часть ядра.

Почему nginx слушает только localhost?

Чтобы не открывать нешифрованный HTTP наружу и не дублировать логику TLS в двух местах. Реверс-прокси перед контейнером терминирует TLS и проксирует уже на 127.0.0.1:5000.

Что будет при смене версии образа funkwhale/api?

Перед апгрейдом всегда смотрите changelog — иногда нужны дополнительные миграции или переменные окружения. Обновляйте front и api одновременно, на одну и ту же версию.

Можно ли запустить Funkwhale на той же машине, где уже стоит Navidrome?

Технически да, если развести порты и использовать общий реверс-прокси, но по смыслу это разные инструменты: Navidrome — для личной фонотеки, Funkwhale — для сообщества с федерацией. Держать оба одновременно имеет смысл, если вам нужны обе функции.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →