...
🚀 how to deploy funkwhale on debian vps
Learn how to deploy funkwhale on debian vps!

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.

Launch 100% ssd debian vps from $2. 49/mo!

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
  1. 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
    
  2. 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
    
  3. 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
    
    1. Create an .env file

      Replace 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 the openssl rand command manually and paste its output into DJANGO_SECRET_KEY.

  4. Write the docker-compose.yml

    sudo -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
    
    1. 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;
        }
      }
      NGINX
      

      We 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.

  5. 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”.

  6. 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
    
  7. 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 nginx
    

    Request 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.com serving Funkwhale.

  8. Configure Funkwhale (admin UI)

    1. Visit https://music.example.com/admin/ and log in with the superuser you created.
    2. Set instance name, description, registrations, and other preferences.
    3. Verify email delivery (password resets, notifications) from Settings → Emails or by using the app (invite, etc.).
    4. Add libraries, start importing music (UI → Libraries).
  9. 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
    
  10. Federation & headers

    Funkwhale uses ActivityPub over HTTPS on standard ports. With reverse proxying:

    • Keep X-Forwarded-Proto and Host headers (as configured).
    • Ensure FUNKWHALE_PROTOCOL=https and FUNKWHALE_HOSTNAME=music.example.com in .env.
    • Open 80/443, and keep valid TLS.
  11. Optional: object storage for media (S3-compatible)

    If you prefer S3/MinIO for media, add variables to .env and 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.yml to avoid binding local ./data to /data/media (since media will live in S3) and redeploy:

    docker compose up -d
    
  12. 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.sh
    

    Media (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/funkwhale and SSL keys (or rely on Certbot renewal)
  13. 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
    
  14. 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 api is healthy; inspect api logs; increase proxy_read_timeout.
      • Uploads failing: Increase client_max_body_size (already set to 200M in 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.
  15. Security hardening checklist

    • Use strong secrets (DB, Django SECRET_KEY, SMTP).
    • Restrict Docker socket access (already limited to docker group).
    • 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.
  16. 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.

Launch 100% ssd debian vps from $2. 49/mo!

Conclusion

You now know how to deploy Funkwhale on Debian VPS.


Compare Debian VPS Plans

KVM-SSD-1
KVM-SSD-8
KVM-SSD-16
KVM-SSD-32
CPU
1 Core
2 Cores
4 Cores
8 Cores
Memory
1 GB
8 GB
16 GB
32 GB
Storage
16 GB NVMe
128 GB NVMe
256 GB NVMe
512 GB NVMe
Bandwidth
1 TB
4 TB
8 TB
16 TB
Network
1 Gbps
1 Gbps
1 Gbps
1 Gbps
Delivery Time
⏱️ Instant
⏱️ Instant
⏱️ Instant
⏱️ Instant
Location
US/FR
US/FR
US/FR
US/FR
Price
$7.58*
$39.50*
$79.40*
$151.22*
KVM-SSD-1
CPU: 1 Core
Memory: 2 GB
Storage: 16 GB NVMe
1 TB
KVM-SSD-8
CPU: 2 Cores
Memory: 8 GB
Storage: 128 GB NVMe
4 TB
KVM-SSD-16
CPU: 4 Cores
Memory: 16 GB
Storage: 256 GB NVMe
8 TB
KVM-SSD-32
CPU: 8 Cores
Memory: 32 GB
Storage: 512 GB NVMe
16 TB

Avatar of editorial staff

Editorial Staff

Rad Web Hosting is a leading provider of web hosting, Cloud VPS, and Dedicated Servers in Dallas, TX.
lg