
This article provides a guide demonstrating how to deploy Funkwhale on Debian VPS.
What is Funkwhale?
Funkwhale is a self-hosted, federated audio platform for streaming and sharing music & podcasts. It uses ActivityPub so libraries can follow/feature content across instances (like Mastodon), and it offers a web UI plus Subsonic-compatible APIs for many mobile/desktop clients.
Typical stack: Python/Django app, PostgreSQL, Redis, and a media backend (local or object storage) behind a reverse proxy (e.g., Nginx). Licensed AGPL-3. Features include user libraries, playlists, podcast hosting/subscriptions, federation, permissions/moderation, and import from local files or remote feeds.
How to Deploy Funkwhale on Debian VPS
Below is a production-ready, step-by-step guide that uses Docker + Docker Compose on Debian 12. It sets up a fully working Funkwhale instance with PostgreSQL, Redis, Nginx (as a container, proxied by host Nginx with Letβs Encrypt), Celery workers, and persistent volumes. It also covers SMTP, federation, backups, upgrades, and troubleshooting.
Prerequisites
- A fresh Debian 12 VPS (2+ vCPU, 4+ GB RAM recommended)
- A domain/subdomain ready, e.g.
music.example.com - DNS A/AAAA records pointing to your VPS IP(s)
- SSH access as a sudo user
- Ports 80 and 443 open to the internet
If you use UFW:
sudo apt update sudo apt install -y ufw sudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable sudo ufw status
-
Create a system user and basic packages
sudo adduser --disabled-password --gecos "" funk sudo usermod -aG sudo funk sudo apt update && sudo apt -y upgrade sudo apt install -y git curl ca-certificates gnupg lsb-release
-
Install Docker Engine & Compose
# Docker repo sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) 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 # Let our user run docker sudo usermod -aG docker funk newgrp docker
-
Prepare Funkwhale project layout
sudo -u funk mkdir -p /opt/funkwhale/{config,nginx,data,postgres,redis,logs} sudo -u funk chmod -R 750 /opt/funkwhale cd /opt/funkwhale-
Create an
.envfileReplace values as needed (domain, passwords, email, etc.).
sudo -u funk tee /opt/funkwhale/.env >/dev/null <<'EOF' # --- General --- FUNKWHALE_HOSTNAME=music.example.com FUNKWHALE_PROTOCOL=https DJANGO_SECRET_KEY=$(openssl rand -base64 50) DJANGO_DEBUG=false INSTANCE_NAME=My Funkwhale # --- Database --- POSTGRES_DB=funkwhale POSTGRES_USER=funkwhale POSTGRES_PASSWORD=SuperStrongDBPass POSTGRES_HOST=postgres POSTGRES_PORT=5432 DATABASE_URL=postgresql://funkwhale:SuperStrongDBPass@postgres:5432/funkwhale # --- Cache --- REDIS_HOST=redis REDIS_PORT=6379 CACHE_URL=redis://redis:6379/0 # --- Media/Static --- MEDIA_ROOT=/data/media STATIC_ROOT=/app/staticfiles # --- Emails (SMTP) --- EMAIL_ENABLED=true EMAIL_HOST=smtp.example.com EMAIL_PORT=587 EMAIL_HOST_USER=postmaster@example.com EMAIL_HOST_PASSWORD=ChangeThisPassword EMAIL_USE_TLS=true DEFAULT_FROM_EMAIL="Funkwhale <noreply@music.example.com>" # --- Federation/ActivityPub --- ALLOW_REGISTRATION=false FEDERATION_ENABLED=true # --- Nginx (container) --- NGINX_LISTEN=80 # --- Timezone/Locale --- TZ=UTC LANG=C.UTF-8 EOF
Tip: If your shell doesnβt expand
$(openssl ...)in a heredoc, run theopenssl randcommand manually and paste its output intoDJANGO_SECRET_KEY.
-
-
Write the
docker-compose.ymlsudo -u funk tee /opt/funkwhale/docker-compose.yml >/dev/null <<'YML' version: "3.9" services: postgres: image: postgres:15-alpine restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - ./postgres:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 10 redis: image: redis:7-alpine restart: unless-stopped command: ["redis-server", "--appendonly", "yes"] volumes: - ./redis:/data api: image: ghcr.io/funkwhale/funkwhale:latest restart: unless-stopped env_file: .env depends_on: postgres: condition: service_healthy redis: condition: service_started volumes: - ./data:${MEDIA_ROOT} - ./logs:/logs command: /usr/local/bin/gunicorn config.wsgi:application -b 0.0.0.0:8000 --workers 3 --timeout 120 expose: - "8000" celeryworker: image: ghcr.io/funkwhale/funkwhale:latest restart: unless-stopped env_file: .env depends_on: api: condition: service_started redis: condition: service_started command: celery -A config.celery_app worker -l info celerybeat: image: ghcr.io/funkwhale/funkwhale:latest restart: unless-stopped env_file: .env depends_on: api: condition: service_started redis: condition: service_started command: celery -A config.celery_app beat -l info nginx: image: nginx:stable-alpine restart: unless-stopped depends_on: - api volumes: - ./nginx/funkwhale.conf:/etc/nginx/conf.d/default.conf:ro - ./data:${MEDIA_ROOT}:ro - ./logs:/logs ports: - "127.0.0.1:8080:80" YML-
Nginx (in-container) proxy config
sudo -u funk tee /opt/funkwhale/nginx/funkwhale.conf >/dev/null <<'NGINX' server { listen 80; server_name _; client_max_body_size 200M; # Static/media (served directly by Nginx) location /media/ { alias /data/media/; add_header Cache-Control "public, max-age=86400"; access_log off; } location /static/ { alias /app/staticfiles/; add_header Cache-Control "public, max-age=86400"; access_log off; } # Proxy API/app location / { proxy_pass http://api:8000; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 120; } } NGINXWe bind this containerβs port 80 to localhost:8080. The public TLS termination will happen on the host (next step) and proxy to
127.0.0.1:8080.
-
-
Start the stack
cd /opt/funkwhale docker compose pull docker compose up -d docker compose ps
Wait for Postgres healthcheck to pass and containers to show as βUpβ.
-
Initialize the database, collect static files, create admin
# Migrations docker compose exec api python manage.py migrate # Collect static docker compose exec api python manage.py collectstatic --noinput # Create superuser (you'll be prompted for email/password) docker compose exec api python manage.py createsuperuser
-
Host Nginx + Letβs Encrypt TLS
Install host Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginx
Create a site config to proxy 443 β 127.0.0.1:8080:
sudo tee /etc/nginx/sites-available/funkwhale >/dev/null <<'NGINX' server { listen 80; server_name music.example.com; location /.well-known/acme-challenge/ { root /var/www/letsencrypt; } location / { return 301 https://$host$request_uri; } } server { listen 443 ssl http2; server_name music.example.com; # Temporary self-signed; Certbot will replace ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; # Proxy to the containerized Nginx location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 120; } } NGINX sudo ln -s /etc/nginx/sites-available/funkwhale /etc/nginx/sites-enabled/funkwhale sudo nginx -t && sudo systemctl reload nginxRequest a real certificate:
sudo certbot --nginx -d music.example.com --redirect --agree-tos -m you@example.com -n sudo systemctl status nginx
You now have a valid HTTPS endpoint at
https://music.example.comserving Funkwhale. -
Configure Funkwhale (admin UI)
- Visit
https://music.example.com/admin/and log in with the superuser you created. - Set instance name, description, registrations, and other preferences.
- Verify email delivery (password resets, notifications) from Settings β Emails or by using the app (invite, etc.).
- Add libraries, start importing music (UI β Libraries).
- Visit
-
Make it persistent with systemd (optional)
Create a unit so Docker Compose starts after reboots:
sudo tee /etc/systemd/system/funkwhale.service >/dev/null <<'UNIT' [Unit] Description=Funkwhale (Docker Compose) Requires=docker.service After=docker.service network-online.target Wants=network-online.target [Service] Type=oneshot User=funk WorkingDirectory=/opt/funkwhale RemainAfterExit=true ExecStart=/usr/bin/docker compose up -d ExecStop=/usr/bin/docker compose down [Install] WantedBy=multi-user.target UNIT sudo systemctl daemon-reload sudo systemctl enable --now funkwhale
-
Federation & headers
Funkwhale uses ActivityPub over HTTPS on standard ports. With reverse proxying:
- Keep
X-Forwarded-ProtoandHostheaders (as configured). - Ensure
FUNKWHALE_PROTOCOL=httpsandFUNKWHALE_HOSTNAME=music.example.comin.env. - Open 80/443, and keep valid TLS.
- Keep
-
Optional: object storage for media (S3-compatible)
If you prefer S3/MinIO for media, add variables to
.envand rebuild:# Example (adjust for your provider) AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=... AWS_STORAGE_BUCKET_NAME=funkwhale-media AWS_S3_ENDPOINT_URL=https://sfo3.digitaloceanspaces.com AWS_S3_REGION_NAME=us-east-1 USE_S3_MEDIA=true
Then update your
docker-compose.ymlto avoid binding local./datato/data/media(since media will live in S3) and redeploy:docker compose up -d
-
Backups
PostgreSQL
# Daily dump (retain 7 days) sudo tee /usr/local/bin/backup-funkwhale-db.sh >/dev/null <<'BASH' #!/usr/bin/env bash set -euo pipefail DST=/var/backups/funkwhale-db mkdir -p "$DST" STAMP=$(date +%F_%H%M%S) docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-funkwhale}" "${POSTGRES_DB:-funkwhale}" \ | gzip > "$DST/${STAMP}.sql.gz" find "$DST" -type f -mtime +7 -delete BASH sudo chmod +x /usr/local/bin/backup-funkwhale-db.shMedia (if using local storage):
sudo rsync -a --delete /opt/funkwhale/data/ /var/backups/funkwhale-media/
Cron
(crontab -l 2>/dev/null; echo "15 2 * * * /usr/local/bin/backup-funkwhale-db.sh") | crontab - (crontab -l 2>/dev/null; echo "45 2 * * * rsync -a --delete /opt/funkwhale/data/ /var/backups/funkwhale-media/") | crontab -
Also back up:
/opt/funkwhale/.env/opt/funkwhale/docker-compose.yml/etc/nginx/sites-available/funkwhaleand SSL keys (or rely on Certbot renewal)
-
Upgrades
cd /opt/funkwhale docker compose pull docker compose up -d # Run migrations when changelogs indicate docker compose exec api python manage.py migrate docker compose exec api python manage.py collectstatic --noinput
If youβve adjusted environment variables:
docker compose down docker compose up -d
-
Logs & troubleshooting
- Container health:
docker compose ps docker compose logs -f api docker compose logs -f nginx docker compose logs -f postgres docker compose logs -f redis docker compose logs -f celeryworker docker compose logs -f celerybeat
- Nginx (host):
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log
- Common issues:
- 502/504: Check that
apiis healthy; inspectapilogs; increaseproxy_read_timeout. - Uploads failing: Increase
client_max_body_size(already set to200Min container Nginx). Adjust as needed in both container and host if you proxy large files. - Emails not sending: Verify SMTP vars and try:
docker compose exec api python - <<'PY'\nfrom django.core.mail import send_mail\nsend_mail('Test','Body','noreply@music.example.com',['you@example.com'])\nPY - Federation not working: Confirm valid TLS, correct
FUNKWHALE_HOSTNAME/PROTOCOL, headers forwarded, and public reachability on 443.
- 502/504: Check that
- Container health:
-
Security hardening checklist
- Use strong secrets (DB, Django SECRET_KEY, SMTP).
- Restrict Docker socket access (already limited to
dockergroup). - Keep Debian and images updated (
apt upgrade,docker compose pull). - Consider fail2ban (for host Nginx) and rate limits.
- Regularly prune unused images:
docker system prune -f.
-
Quick tear-down / redeploy
cd /opt/funkwhale docker compose down docker compose up -d
To completely remove data (careful!):
docker compose down -v sudo rm -rf /opt/funkwhale/postgres /opt/funkwhale/data /opt/funkwhale/redis
Youβre set! Visit https://music.example.com, sign in with your superuser, and start importing your library.
Conclusion
You now know how to deploy Funkwhale on Debian VPS.









