Cloudron package for Funkwhale v2 — a self-hosted, federated music streaming platform.
Funkwhale lets you listen, upload, and share music and audio within a decentralized network using ActivityPub federation. It supports the Subsonic API for compatibility with existing music apps.
This package runs Funkwhale v2 inside a single Cloudron container with four processes:
| Process | Role | Details |
|---|---|---|
| nginx | Reverse proxy | Routes requests to the API, serves frontend SPA and static/media files |
| gunicorn | ASGI web server | Django REST API with Uvicorn workers on port 5000 |
| celery worker | Task processor | Background jobs (imports, federation, transcoding) |
| celery beat | Task scheduler | Periodic tasks (federation polling, cleanup) |
| Addon | Purpose |
|---|---|
| PostgreSQL | Primary database for users, music metadata, playlists |
| Redis | Cache and Celery message broker |
| Local Storage | Persistent storage for media, music files, and Django static files |
| Sendmail | Outgoing email (password resets, notifications) |
- A Cloudron instance (min box version 8.1.0)
- The Cloudron CLI installed on your local machine
git clone git@github.com:rmdes/funkwhale-cloudron.git
cd funkwhale-cloudron
# Build the Docker image (requires Docker)
cloudron build
# Install on your Cloudron (replace 'fw' with your desired subdomain)
cloudron install --location fwAfter the initial install, use cloudron update for subsequent deployments:
cloudron build
cloudron update --app fw.example.comSet the memory limit to 1GB in the Cloudron UI under Resource Limits after installation.
After installation, open a Web Terminal for the app from the Cloudron dashboard and run:
source /app/code/venv/bin/activate
funkwhale-manage fw users create --superuser --username yourname --email you@example.com --password yourpasswordNote: Funkwhale does not allow "admin" as a username. Change your password after first login.
Cloudron (handles TLS, DNS, backups, authentication)
└── Container
├── nginx (:8000) ← Cloudron routes traffic here
│ ├── / → Frontend SPA (Vue.js)
│ ├── /api/ → gunicorn (:5000)
│ ├── /federation/ → gunicorn (:5000)
│ ├── /rest/ → gunicorn (:5000) [Subsonic API]
│ ├── /.well-known/ → gunicorn (:5000) [WebFinger/nodeinfo]
│ ├── /media/ → /app/data/media/ (direct serve)
│ ├── /_protected/media/ → /app/data/media/ (auth via X-Accel-Redirect)
│ ├── /_protected/music/ → /app/data/music/ (auth via X-Accel-Redirect)
│ └── /staticfiles/ → /app/data/static/
├── gunicorn (:5000) ← Django ASGI app (main process, PID 1)
├── celery worker ← Background task processing
└── celery beat ← Periodic task scheduling
/app/code/ # Immutable application code (rebuilt on updates)
├── api/ # Django REST API (Python)
├── front/dist/ # Vue.js frontend (pre-built static files)
└── venv/ # Python virtual environment
/app/data/ # Persistent data (survives updates, backed up by Cloudron)
├── config/.secret_key # Django secret key (auto-generated on first run)
├── media/ # User uploads (avatars, playlist covers, attachments)
├── music/ # Music library files
└── static/ # Django collectstatic output
/app/pkg/ # Package scripts
├── start.sh # Startup script
├── manage.sh # Wrapper for funkwhale-manage (used by scheduler)
└── nginx.conf # nginx config template
On every container start, start.sh:
- Creates persistent directories under
/app/data/if they don't exist - Generates a Django secret key on first run (persisted in
/app/data/config/.secret_key) - Maps Cloudron addon environment variables (
CLOUDRON_*) to Funkwhale equivalents (DATABASE_URL,CACHE_URL,FUNKWHALE_HOSTNAME, etc.) - Runs
collectstaticand database migrations (idempotent) - Substitutes the app domain into the nginx config
- Starts nginx, celery worker, and celery beat as background processes
- Starts gunicorn as PID 1 (Cloudron monitors this for health)
| Cloudron Addon | Funkwhale Variable |
|---|---|
CLOUDRON_APP_DOMAIN |
FUNKWHALE_HOSTNAME |
CLOUDRON_POSTGRESQL_* |
DATABASE_URL (constructed as connection string) |
CLOUDRON_REDIS_* |
CACHE_URL, CELERY_BROKER_URL |
CLOUDRON_MAIL_SMTP_* |
EMAIL_CONFIG |
CLOUDRON_MAIL_FROM |
DEFAULT_FROM_EMAIL |
If you have an existing Funkwhale instance and want to migrate to Cloudron, follow these steps.
# Database dump (on your existing server)
sudo -u postgres pg_dump -Fc funkwhale > funkwhale.dump
# Copy media and music files
tar czf funkwhale-media.tar.gz -C /srv/funkwhale/data media/
tar czf funkwhale-music.tar.gz -C /srv/funkwhale/data music/Copy funkwhale.dump, funkwhale-media.tar.gz, and funkwhale-music.tar.gz to a location accessible from the Cloudron app's terminal (e.g., /tmp/ inside the container).
Open a Web Terminal for the Funkwhale app in the Cloudron dashboard:
# Stop background services first
pkill -f celery || true
# Restore the database
pg_restore -h "${CLOUDRON_POSTGRESQL_HOST}" \
-p "${CLOUDRON_POSTGRESQL_PORT}" \
-U "${CLOUDRON_POSTGRESQL_USERNAME}" \
-d "${CLOUDRON_POSTGRESQL_DATABASE}" \
--clean --if-exists /tmp/funkwhale.dump
# Extract media and music
tar xzf /tmp/funkwhale-media.tar.gz -C /app/data/
tar xzf /tmp/funkwhale-music.tar.gz -C /app/data/
# Fix permissions
chown -R cloudron:cloudron /app/data
# Run migrations in case versions differ
source /app/code/venv/bin/activate
funkwhale-manage migrate --noinputRestart the app from the Cloudron dashboard after the import.
- Existing users can log in
- Music library appears intact
- Media files (avatars, covers) display correctly
- Playlists, favorites, and listening history are preserved
To update to a new Funkwhale version:
- Edit the
FUNKWHALE_VERSIONARG in theDockerfile - Update
upstreamVersioninCloudronManifest.json - Bump
versioninCloudronManifest.json - Add a changelog entry in
CHANGELOG.md - Rebuild and update:
cloudron build
cloudron update --app fw.example.comDatabase migrations run automatically on startup.
- Music Streaming — Upload, organize, and stream your music library
- Federation — Share and discover music across Funkwhale instances via ActivityPub
- Subsonic API — Use existing Subsonic-compatible apps (DSub, Ultrasonic, Clementine, etc.)
- Podcasts — Subscribe to and manage podcast feeds
- Channels — Publish audio content with RSS feeds
- Playlists & Radio — Create playlists, favorites, and auto-generated radio stations
From the Cloudron web terminal:
# Check which processes are running
ps aux
# Check nginx logs
cat /var/log/nginx/error.log
# Check Funkwhale API logs (gunicorn output goes to Cloudron logs)
# View from Cloudron dashboard → App → LogsThe Cloudron manifest includes a Scheduler/Cron dropdown in the app terminal with 17 Funkwhale management tasks. Click any task in the dropdown to populate the terminal command.
Safe tasks (run automatically on schedule):
| Task | Schedule | Description |
|---|---|---|
clear_sessions |
Daily 3:23 AM | Remove expired Django sessions |
clear_expired_tokens |
Daily 3:43 AM | Remove expired OAuth tokens |
collect_static |
Weekly (Sun 4 AM) | Rebuild Django static files |
On-demand tasks (yearly schedule — use via dropdown, not auto-run):
| Task | Description |
|---|---|
check_preferences |
Verify instance preferences |
rebuild_music_permissions |
Rebuild library access permissions |
run_migrations |
Apply database migrations |
Destructive tasks (split into check/apply pairs):
| Check task (dry-run) | Apply task (real) | Description |
|---|---|---|
prune_library_check |
prune_library_apply |
Remove orphaned artists/albums/tracks |
prune_non_mbid_check |
prune_non_mbid_apply |
Remove tracks without MusicBrainz IDs |
prune_skipped_uploads_check |
prune_skipped_uploads_apply |
Remove failed/skipped uploads |
check_inplace_files_check |
check_inplace_files_apply |
Verify in-place imported files |
fix_uploads_check |
fix_uploads_apply |
Fix upload metadata (mimetype, size, checksum) |
Safety note: All
_checktasks default to dry-run mode — they show what would change without modifying the database. Only_applytasks make real changes. Add--helpto any command before running it to see available options.
A 00_read_me_first entry is included at the top of the dropdown as a quick reference for this safety model.
Use the manage.sh wrapper to run funkwhale-manage commands with the correct environment. This is what the scheduler tasks use:
/app/pkg/manage.sh <command> [args...]For example:
/app/pkg/manage.sh prune_library --help
/app/pkg/manage.sh createsuperuserThe wrapper maps all CLOUDRON_* environment variables to their Funkwhale equivalents (DATABASE_URL, CACHE_URL, FUNKWHALE_HOSTNAME, etc.) so you don't need to set them manually.
Alternatively, you can set up the environment by hand in the web terminal:
source /app/code/venv/bin/activate
export DJANGO_SETTINGS_MODULE=config.settings.production
export DJANGO_SECRET_KEY=$(cat /app/data/config/.secret_key)
export DATABASE_URL="postgresql://${CLOUDRON_POSTGRESQL_USERNAME}:${CLOUDRON_POSTGRESQL_PASSWORD}@${CLOUDRON_POSTGRESQL_HOST}:${CLOUDRON_POSTGRESQL_PORT}/${CLOUDRON_POSTGRESQL_DATABASE}"
export CACHE_URL="redis://:${CLOUDRON_REDIS_PASSWORD}@${CLOUDRON_REDIS_HOST}:${CLOUDRON_REDIS_PORT}/0"
funkwhale-manage <command>The health check endpoint is /api/v2/instance/nodeinfo/2.1/. If the app shows as unhealthy:
- Check that gunicorn is running:
ps aux | grep gunicorn - Check that nginx is running:
ps aux | grep nginx - Test the API directly:
curl -s http://localhost:5000/api/v2/instance/nodeinfo/2.1/ - Test through nginx:
curl -s http://localhost:8000/api/v2/instance/nodeinfo/2.1/
Funkwhale runs multiple Python processes, each loading the full Django app (~150MB). The default configuration (2 gunicorn + 2 celery workers + beat + nginx) fits within 1GB. If you experience OOM kills:
- Set the memory limit to at least 1GB in Cloudron UI (Resource Limits)
- Reduce worker counts via environment variables in start.sh:
FUNKWHALE_WEB_WORKERS=1(gunicorn workers, default: 2)CELERYD_CONCURRENCY=1(celery workers, default: 2)
Important: Never set CELERYD_CONCURRENCY=0 — Celery will auto-detect CPU cores and spawn too many workers for the container's memory budget.
Large file uploads (up to 2GB) are supported. If uploads fail:
- Check available disk space:
df -h /app/data/ - Verify permissions:
ls -la /app/data/music/ - Check nginx body size limit is applied:
grep client_max_body_size /run/nginx.conf
Ensure your Cloudron domain has proper DNS and that /.well-known/ endpoints are accessible:
curl -s https://your-domain/.well-known/nodeinfo
curl -s https://your-domain/.well-known/webfinger?resource=acct:user@your-domaindocker build -t funkwhale-cloudron .
docker run --rm -it funkwhale-cloudron /bin/bash
# Inspect: venv exists, funkwhale-manage available, front/dist has filesfunkwhale-cloudron/
├── CloudronManifest.json # Cloudron app metadata, addons, scheduler, health check
├── Dockerfile # Build: cloudron/base + Funkwhale artifacts + venv
├── start.sh # Startup: env mapping, migrations, 4-process launch
├── manage.sh # Wrapper: runs funkwhale-manage with Cloudron env vars
├── nginx.conf # Internal routing (no TLS — Cloudron handles that)
├── DESCRIPTION.md # App store listing
├── POSTINSTALL.md # Post-install instructions
├── CHANGELOG.md # Version history
└── logo.png # App icon
- LDAP/SSO integration (Funkwhale has native LDAP support)
- OIDC single sign-on via Cloudron
- Dynamic worker count based on container memory
- Stable release packaging (currently tracking v2 release candidates)
MIT