...
πŸš€ 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