How to Install Funkwhale on Ubuntu 24.04 — Self-Hosted Music Streaming on Your VPS
Funkwhale turns your VPS into a private music streaming service that you actually own. You import your own FLAC and MP3 library, subscribe to any podcast RSS feed, and federate with the rest of the Fediverse over ActivityPub — the same protocol that powers Mastodon and PeerTube. No algorithm decides what you hear next, no label can revoke a track you paid for, and no provider can change the terms of service on a library you already built. This guide walks you through a full Funkwhale install on Ubuntu 24.04 using Docker Compose, from a fresh SSH session to a federated, TLS-secured instance playing your first album.
Need the hardware? Funkwhale's Docker stack runs comfortably on our Professional VPS plan with 6 vCPU and 12 GB RAM. The 200 GB NVMe gives you room for a real music collection before you ever need object storage.
Table of Contents
What is Funkwhale?
Funkwhale is an open-source, community-driven audio platform that combines three things most commercial services keep separate: a personal music library, a podcast player, and a federated social layer. It is built in Python (Django) on the backend, Vue.js on the frontend, and uses PostgreSQL, Redis, and Celery for persistence and background work. Federation is handled through ActivityPub, so a track or a podcast you publish on your instance can be discovered and followed by users on any other Funkwhale, Mastodon, or compatible Fediverse server.
The project started in 2017 as a GPL-licensed alternative to Grooveshark and has grown into a mature platform with a mobile app, a Subsonic-compatible API (so clients like Ultrasonic, DSub, and Symfonium work out of the box), and a clean web player. Documentation lives at docs.funkwhale.audio, and the reference Docker Compose deployment is maintained as part of every release.
Typical use cases for a self-hosted Funkwhale instance include archiving a personal collection of FLAC rips that you never want a streaming service to lose, running a small community radio where multiple users share channels and playlists, hosting your own podcast with an RSS feed that listeners can subscribe to from any podcatcher, or bridging a DJ or label catalogue into the Fediverse so new releases surface in Mastodon timelines.
Why Self-Host Music Streaming Instead of Using Spotify?
Commercial music streaming is cheap and convenient, but the tradeoffs are structural, not accidental. Running Funkwhale on your own VPS fixes several of them at once.
- You own the library — Every track you upload stays on disk at a path you control. If Funkwhale's maintainers disappear tomorrow, your files are still there and playable with VLC, mpv, or any other audio tool.
- No DRM, no regional blocking — Files are stored as plain FLAC, MP3, Ogg Vorbis, or Opus. There is no encryption layer, no license server, and no country gate between you and the audio.
- Tracks cannot be removed remotely — On commercial platforms, albums disappear when licensing deals expire or when artists are delisted. On your Funkwhale instance, a track is gone only when you delete the file.
- Predictable flat-rate cost — A VPS plus bandwidth costs the same whether you stream one hour a month or ten hours a day. There is no per-stream payout model quietly reshaping what gets recommended.
- Privacy by default — Your listening history, likes, and playlists live in a PostgreSQL database on a server you own. No advertiser buys it, no recommendation engine trains on it without your consent.
- Podcasts in the same player — Funkwhale fetches podcast RSS feeds directly, so your music and your shows live in one library with one set of playlists, offline downloads, and bookmarks.
- Federation is optional and granular — You can keep the instance fully private, open it to a handful of friends, or federate with the broader Fediverse. The choice is per-library, not platform-wide.
Cost Comparison: Self-Hosted Funkwhale vs. Commercial Streaming
| Scenario | Spotify Family | Apple Music | Self-Hosted Funkwhale (VPS) |
|---|---|---|---|
| Monthly cost | ~EUR 17.99 | ~EUR 16.99 | EUR 19.99 (flat) |
| Max users per account | 6 | 6 | Unlimited |
| Tracks you can lose to licensing changes | All of them | All of them | None |
| DRM on downloads | Yes | Yes | No |
| Podcasts included | Yes | Yes | Yes |
| Custom uploads of personal FLACs | No | Limited (Match) | Yes, unlimited |
| Fediverse integration | No | No | Yes (ActivityPub) |
| Mobile client | Proprietary | Proprietary | Official app + any Subsonic client |
Prerequisites
Before you start, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access.
- A domain name (for example
music.example.com) with an A record pointing at your VPS's public IPv4 address and, optionally, a AAAA record for IPv6. - SSH access to the server.
- At least 8 GB of RAM (12 GB recommended once transcoding and federation are active).
- At least 100 GB of disk for the system, Postgres, and your music. Plan more if you have a large FLAC collection.
- Ports 80 and 443 open in your provider's firewall.
Recommended Plan: Professional>
For a comfortable Funkwhale instance that can federate, transcode on the fly, and serve multiple concurrent listeners without stuttering, we recommend the Professional VPS plan:>
- 6 vCPU cores
- 12 GB RAM
- 200 GB NVMe SSD
- Unmetered bandwidth
- EUR 19.99/month>
The 200 GB NVMe holds roughly 4,000 CD-quality FLAC albums or 40,000 well-encoded MP3s before you need to attach object storage.
Connect to your server:
ssh root@your-server-ipStep 1: Update the System and Create a Service User
Start with a clean, patched base:
apt update && apt upgrade -y
apt install -y ca-certificates curl gnupg lsb-release ufwCreate a dedicated non-root user that will own the Funkwhale files and run the Compose stack:
adduser --disabled-password --gecos "" funkwhale
usermod -aG sudo funkwhaleEnable a basic firewall. Funkwhale only needs SSH, HTTP, and HTTPS exposed to the public:
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enableSwitch to the service user for the rest of the install:
su - funkwhaleStep 2: Install Docker Engine and the Compose Plugin
Funkwhale's reference deployment assumes Docker Engine with the Compose v2 plugin. Install the official Docker Debian repository (Ubuntu 24.04 Noble is supported):
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpgecho "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu noble stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Add the funkwhale user to the docker group so you do not need sudo for every compose command:
sudo usermod -aG docker funkwhale
exitLog back in to apply the new group membership:
ssh funkwhale@your-server-ip
docker version
docker compose versionYou should see client and server versions printed without any permission errors.
Step 3: Download the Funkwhale Compose Bundle
Create the instance directory and pull the current release's deployment artifacts. Check docs.funkwhale.audio for the latest stable version tag before running this — substitute it into the FUNKWHALE_VERSION variable below.
export FUNKWHALE_VERSION="1.4.0" mkdir -p ~/funkwhale && cd ~/funkwhalecurl -L -o docker-compose.yml \ "https://dev.funkwhale.audio/funkwhale/funkwhale/-/raw/${FUNKWHALE_VERSION}/deploy/docker-compose.yml"
curl -L -o .env.example \ "https://dev.funkwhale.audio/funkwhale/funkwhale/-/raw/${FUNKWHALE_VERSION}/deploy/env.prod.sample"
curl -L -o nginx.template \ "https://dev.funkwhale.audio/funkwhale/funkwhale/-/raw/${FUNKWHALE_VERSION}/deploy/docker.nginx.template"
curl -L -o nginx.proxy.conf \ "https://dev.funkwhale.audio/funkwhale/funkwhale/-/raw/${FUNKWHALE_VERSION}/deploy/docker.proxy.conf"
cp .env.example .env
The docker-compose.yml defines six services that work together:
postgres— PostgreSQL 15, holds users, tracks metadata, federation state, and listening history.redis— Cache, session store, and Celery broker.api— The Django application server (runs on gunicorn) that serves the REST and ActivityPub APIs.celeryworker— Background worker for imports, transcoding jobs, and federation deliveries.celerybeat— Scheduler that periodically fetches podcast feeds and runs cleanup tasks.front— Nginx container serving the compiled Vue.js frontend and proxying the API.
Step 4: Configure the .env File
Open the .env file and edit the important values:
nano .envSet the hostname, generate a strong DJANGO_SECRET_KEY, and pick a database password. A one-liner to generate the secret key:
openssl rand -base64 45Then set these keys in .env:
# The public hostname users will type in their browser
FUNKWHALE_HOSTNAME=music.example.com
FUNKWHALE_PROTOCOL=httpsDjango
DJANGO_SECRET_KEY=<paste the openssl output here>
DJANGO_ALLOWED_HOSTS=music.example.com
DJANGO_SETTINGS_MODULE=config.settings.productionDatabase
DATABASE_URL=postgresql://funkwhale@postgres:5432/funkwhale
POSTGRES_USER=funkwhale
POSTGRES_PASSWORD=<strong password>
POSTGRES_DB=funkwhaleCache
CACHE_URL=redis://redis:6379/0
CELERY_BROKER_URL=redis://redis:6379/0Email (adjust or leave the default console backend for now)
EMAIL_CONFIG=consolemail://
[email protected]Paths
MEDIA_ROOT=/srv/funkwhale/data/media
STATIC_ROOT=/srv/funkwhale/data/static
MUSIC_DIRECTORY_PATH=/music
MUSIC_DIRECTORY_SERVE_PATH=/srv/funkwhale/data/musicReverse proxy
FUNKWHALE_WEB_WORKERS=4
NGINX_MAX_BODY_SIZE=100MFederation
FEDERATION_ENABLED=TrueA couple of notes on the trickier values:
FUNKWHALE_HOSTNAMEbecomes part of federated actor URLs and is effectively permanent. Choose it carefully — changing it after you start federating breaks every remote follow you have.MUSIC_DIRECTORY_PATHis the path inside theapicontainer where your music lives.MUSIC_DIRECTORY_SERVE_PATHis the path on the host. The volume mount indocker-compose.ymlbridges the two.FEDERATION_ENABLED=Trueturns on ActivityPub. Keep it off initially if you only want a private instance for yourself.
Step 5: Prepare the Music Import Directory
Create the host directories the containers will mount:
sudo mkdir -p /srv/funkwhale/data/{media,static,music}
sudo chown -R 1000:1000 /srv/funkwhalePlace your music under /srv/funkwhale/data/music/, organised however you like — Funkwhale reads ID3 tags and Vorbis comments rather than relying on folder structure, but a typical layout of Artist/Album/Track.flac works well. Supported formats include FLAC, MP3, Ogg Vorbis, Opus, AAC, and WAV.
You can symlink an existing collection in:
sudo ln -s /mnt/nas/flac-library/* /srv/funkwhale/data/music/For object storage (S3, Backblaze B2, etc.), see the S3_* variables in .env.example and the object storage guide in the official docs.
Step 6: Launch the Stack
Pull the images and bring everything up in the background:
cd ~/funkwhale
docker compose pull
docker compose up -dFirst boot takes a minute or two. Watch the logs until the Django migrations and static asset collection finish:
docker compose logs -f apiYou are ready to move on when you see Listening at: http://0.0.0.0:5000 from gunicorn and no red ERROR lines.
Check that all six containers are running:
docker compose psExpected output:
NAME IMAGE STATUS
funkwhale-postgres-1 postgres:15-alpine Up (healthy)
funkwhale-redis-1 redis:7-alpine Up
funkwhale-api-1 funkwhale/funkwhale:1.4.0 Up
funkwhale-celeryworker-1 funkwhale/funkwhale:1.4.0 Up
funkwhale-celerybeat-1 funkwhale/funkwhale:1.4.0 Up
funkwhale-front-1 funkwhale/funkwhale:1.4.0 UpStep 7: Create the Superuser
Create the first admin account by running manage.py createsuperuser inside the api container:
docker compose run --rm api python manage.py createsuperuserYou will be prompted for a username, email address, and password:
Username: admin
Email address: [email protected]
Password:
Password (again):
Superuser created successfully.This account can log in at https://music.example.com once the reverse proxy is up, and can access the Django admin at /api/admin/ for low-level database operations.
Step 8: Install the Nginx Reverse Proxy
Funkwhale ships with a tested Nginx configuration. You can either run Nginx inside Docker (included in the Compose file as the front container, which already listens on port 80 of the host) or use a host-level Nginx, which is simpler for adding Let's Encrypt.
Install host Nginx:
sudo apt install -y nginxRender the Funkwhale Nginx template with your hostname baked in:
sudo mkdir -p /etc/nginx/funkwhale sudo cp ~/funkwhale/nginx.template /etc/nginx/funkwhale/funkwhale.template sudo cp ~/funkwhale/nginx.proxy.conf /etc/nginx/funkwhale/funkwhale_proxy.confsudo tee /etc/nginx/sites-available/funkwhale > /dev/null <<'EOF' upstream funkwhale-api { server 127.0.0.1:5000; }
server { listen 80; listen [::]:80; server_name music.example.com;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; } }
server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name music.example.com;
ssl_certificate /etc/letsencrypt/live/music.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/music.example.com/privkey.pem;
# Provided by Funkwhale — handles /api, /federation, /media, static, and the SPA include /etc/nginx/funkwhale/funkwhale.template;
set $funkwhale_url http://funkwhale-api; client_max_body_size 100M; } EOF
sudo ln -s /etc/nginx/sites-available/funkwhale /etc/nginx/sites-enabled/
Edit the Compose file so the front and api containers only listen on loopback — the host Nginx fronts them. Find the ports: block on the front service and change 80:80 to 127.0.0.1:5000:80, then:
docker compose up -dStep 9: Obtain a TLS Certificate
Install Certbot and request a Let's Encrypt certificate:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d music.example.com --redirect --agree-tos -m [email protected] --non-interactiveCertbot edits the Nginx config in place, installs the certificate, and sets up the automatic renewal timer. Confirm the timer is active:
systemctl status certbot.timerReload Nginx and hit your instance:
sudo nginx -t && sudo systemctl reload nginx
curl -I https://music.example.comA 200 OK response with a valid strict-transport-security header means you are live. Open the URL in a browser and log in with the superuser credentials from Step 7.
Step 10: Enable Federation via ActivityPub
With FEDERATION_ENABLED=True in .env, your instance already publishes a /.well-known/webfinger endpoint and serves ActivityPub actors under /federation/actors/<user>. To verify federation is reachable from the outside:
curl "https://music.example.com/.well-known/nodeinfo"
curl "https://music.example.com/.well-known/webfinger?resource=acct:[email protected]"Both requests should return JSON documents describing your server and the admin actor.
Inside the Funkwhale web UI, open Settings → Federation and:
[email protected] for the flagship community instance.#Funkwhale and your hostname usually gets you discovered within a day.Federation traffic happens over standard HTTPS on port 443, so there is nothing extra to open in the firewall. Posts, likes, and library announcements are signed with per-actor RSA keys that Funkwhale generates automatically.
If you also run a Mastodon instance, users there can follow your Funkwhale channels directly. The same is true for PeerTube video channels — the Fediverse treats all three the same way.
Step 11: Subscribe to Podcasts
Funkwhale treats podcasts as a specialised kind of channel. To subscribe:
https://feeds.simplecast.com/54nAGcIl for The Changelog).From now on, celerybeat refreshes every subscribed feed on a schedule (default: every 30 minutes). New episodes appear in your home timeline and are auto-downloaded if you enable that in Settings → Subscriptions.
You can also use the Subsonic API endpoint (/api/subsonic/rest/) to connect any Subsonic-compatible podcatcher — Symfonium, DSub, play:Sub, Ultrasonic — using your Funkwhale username and a generated Subsonic password from Settings → Subsonic API.
Importing Your Music Library
With music placed under /srv/funkwhale/data/music/, trigger an in-place import that reads the files without copying them:
docker compose run --rm api python manage.py import_files \
"$(python3 -c 'print(open(\"/app/config/settings/common.py\").read())' 2>/dev/null; echo /music)" \
"/music//.flac" "/music//.mp3" "/music//.ogg" "/music//.opus" \
--recursive --noinput --in-place --library-id <LIBRARY_UUID>Get the <LIBRARY_UUID> by creating a new library first via the web UI under Library → + New library, then copying the UUID from its URL.
For smaller one-off imports, the web UI's drag-and-drop uploader is often easier — it kicks off the same Celery pipeline behind the scenes.
Backup and Maintenance
Back up three things regularly: the Postgres database, the .env file (it contains your DJANGO_SECRET_KEY), and the media directory if you rely on it for uploads.
A nightly database dump:
docker compose exec -T postgres pg_dump -U funkwhale funkwhale \
| gzip > ~/backups/funkwhale-$(date +%F).sql.gzUpgrade to a new Funkwhale release by editing the image tag in docker-compose.yml, then:
docker compose pull
docker compose run --rm api python manage.py migrate --noinput
docker compose up -dAlways read the release notes at docs.funkwhale.audio before a major version bump — migrations can take several minutes on large databases.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| 502 Bad Gateway from Nginx | api container not running or still migrating | docker compose logs api — wait for gunicorn to bind, or restart the stack |
DisallowedHost error in logs | DJANGO_ALLOWED_HOSTS missing your hostname | Add it to .env, then docker compose up -d |
| Files uploaded but no audio on playback | Host path not mounted into api or permissions wrong | ls -l /srv/funkwhale/data/music — must be owned by UID 1000, readable by others |
| Federation requests return 401 | HTTP signature mismatch — usually a clock skew | sudo timedatectl set-ntp true and timedatectl status to confirm sync |
Celery tasks stuck in PENDING | Redis unreachable or worker crashed | docker compose logs celeryworker redis, then docker compose restart celeryworker |
| Podcast feed fails to update | Feed server blocks Funkwhale's User-Agent | Set RSS_FEED_REFRESH_DELAY higher and check the feed in a browser |
| Music imports silently skip files | Missing or malformed ID3 tags | Tag files with MusicBrainz Picard before import |
docker compose logs -f celeryworkerFAQ
Can I keep my Funkwhale instance fully private and skip federation entirely?
Yes. Set FEDERATION_ENABLED=False in .env and restart the stack. Your instance will still serve the web UI, the Subsonic API, and the podcast player, but it will not publish ActivityPub actors or accept follows from remote servers. You can toggle federation on later without losing any data — Funkwhale generates actor keys on first use.
How much disk space do I need for a serious music collection?
A rough rule: 500 MB per hour of lossless FLAC at 16-bit/44.1 kHz, or about 80 MB per hour at 192 kbps MP3. A 200 GB Professional plan holds roughly 400 hours of FLAC or 2,500 hours of MP3 before you need to add a block storage volume or enable S3. Funkwhale's in-place import means you can mount a separate larger disk at /srv/funkwhale/data/music without moving files around.
How does Funkwhale compare to Navidrome for a pure personal library?
Navidrome is a lighter, single-binary server focused entirely on the Subsonic API. If all you want is to stream your own FLACs to a Subsonic client on your phone, Navidrome is simpler to run and uses less RAM. Funkwhale is the better choice when you also want podcasts in the same player, a full web UI for guests, federation with Mastodon and PeerTube, or multi-user channels with per-library permissions. Both can coexist on the same VPS if you have the RAM.
What clients can I use with Funkwhale?
The official Funkwhale Android and iOS apps work over the native API. Any Subsonic-compatible client also works via /api/subsonic/rest/ — the popular choices are Symfonium and Substreamer on Android, play:Sub on iOS, and Sublime Music on Linux desktop. For browser use, the bundled Vue.js web UI is fully responsive and works well on mobile.
Does Funkwhale transcode on the fly for slower connections?
Yes. When a client requests a track in a format the server supports for transcoding (controlled by LISTEN_TRANSCODE settings), the api container uses ffmpeg via Celery to produce the requested format — typically to Ogg Vorbis or MP3 at a lower bitrate for mobile data. Transcoded chunks are cached, so subsequent listens do not re-run ffmpeg. Transcoding is the main reason to run 6 vCPU: a single CPU core can transcode roughly two streams in parallel without audible glitches.
Can multiple users upload to the same library?
Libraries in Funkwhale have granular permissions: each one is owned by a user and can be shared with specific collaborators, opened to all registered users, or published for public and federated access. A common pattern for households is one "Main Library" owned by the admin with upload rights for family members, plus private per-user libraries for personal collections. Permissions are set per-library in Library → settings, not globally.
Is Funkwhale GDPR-compliant out of the box?
Funkwhale's design minimises personal data collection — there are no analytics beacons, no third-party trackers, and no ad networks. As the operator, you are the data controller for anything your users upload and listen to, so you are responsible for your own privacy policy and for honouring deletion requests. The built-in Settings → Your data panel lets every user export their data as JSON and delete their account, which satisfies GDPR Article 17 (right to erasure) and Article 20 (right to data portability) by default.
Next Steps
With Funkwhale running, there are several directions to take the setup further:
- Run a companion Mastodon instance so you can post updates about new channels and new imports to the same Fediverse audience that follows your Funkwhale library.
- Pair it with PeerTube if you also publish music videos or concert recordings. PeerTube federates natively with Funkwhale over ActivityPub, and a single follow from a Mastodon user covers both services.
- Add Navidrome alongside Funkwhale if you want an ultra-lightweight Subsonic-only endpoint for your own phone while keeping Funkwhale for federation and podcasts.
- Move media to object storage — the official docs at docs.funkwhale.audio cover S3, Backblaze B2, and Wasabi configuration via the
S3_*env vars. This is the standard approach once your library exceeds the VPS's local NVMe.
- Set up off-site backups — pipe your nightly
pg_dumpplus arsyncof/srv/funkwhale/data/mediato a second VPS or a B2 bucket. Funkwhale data is all recoverable from those two sources.
Want Funkwhale Without the Install?>
Our Professional VPS plan gives you the hardware for a smooth Funkwhale instance — 6 vCPU, 12 GB RAM, 200 GB NVMe, unmetered bandwidth — for EUR 19.99/month. Deploy Ubuntu 24.04 in under two minutes and walk through this guide on a fresh server in well under an hour.>
Launch Your Funkwhale VPS Now