KVM Media VM: migrating Sonarr, Radarr and SABnzbd into an isolated VM

โก TL;DR
Migrating the media stack (Sonarr, Radarr, SABnzbd) into a dedicated KVM VM running Ubuntu 24.04: security isolation, easy snapshots, QNAP NFS mounted inside the VM, nginx reverse proxy on the host. The migration fully preserves SQLite databases and existing configuration.
Target stack:
- ๐ฅ๏ธ Host: NUC8i3BEH, Ubuntu Server, nginx reverse proxy, Plex (GPU transcoding)
- ๐ฆ VM media-vm: Ubuntu 24.04, 2 vCPU, 8 GB RAM, 120 GB (X5 NVMe), QNAP NFS
- ๐ฌ Migrated services: Sonarr (port 8989), Radarr (port 7878), SABnzbd (port 6789)
๐ง Why isolate the media stack in a VM
Sonarr, Radarr and SABnzbd present a significant attack surface: network calls to external indexers, post-download script execution, filesystem access to the media library. Confining them in a VM provides:
- Security isolation โ a compromised Sonarr cannot reach the host directly
- Snapshots before updates โ via
virsh snapshot-create-as, rollback in 30 seconds if an update breaks the SQLite database - Plex stays on the host โ access to the Intel GPU (
/dev/dri) for hardware transcoding, not easily virtualizable
Since the QNAP NFS was already mounted on the host, downloads already transit over the network โ the VM changes nothing in this regard, it mounts the same shares directly.
๐ง Step 1 โ Prepare the libvirt pool on the X5
The virtual disk is stored on the X5 NVMe SSD (exFAT filesystem), via an ext4 loop image. fallocate is not supported on exFAT โ use dd instead.
dd if=/dev/zero of=/mnt/X5/libvirt-media.img bs=1G count=150 status=progress
mkfs.ext4 /mnt/X5/libvirt-media.img
mkdir -p /mnt/libvirt-media
mount -o loop /mnt/X5/libvirt-media.img /mnt/libvirt-media
echo '/mnt/X5/libvirt-media.img /mnt/libvirt-media ext4 loop 0 0' >> /etc/fstab
systemctl daemon-reload
virsh pool-define-as media-pool dir --target /mnt/libvirt-media
virsh pool-autostart media-pool
virsh pool-start media-pool๐ง Step 2 โ Download the Ubuntu 24.04 cloud image
wget -P /mnt/libvirt-media \
https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
qemu-img resize /mnt/libvirt-media/noble-server-cloudimg-amd64.img 120G๐ง Step 3 โ Create the cloud-init image
Files must be named exactly user-data and meta-data โ without any prefix โ otherwise cloud-init will not detect them.
โ ๏ธ Common pitfall: always include two SSH keys in
authorized_keysโ the laptop key AND the NUCโVM key. Without the NUCโVM key, SSH from the host is impossible.
cat > /tmp/user-data <<'EOF'
#cloud-config
hostname: media-vm
users:
- name: jm
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
lock_passwd: false
passwd: $YOUR_HASH$...
ssh_authorized_keys:
- ssh-ed25519 AAAA... laptop-key
- ssh-ed25519 AAAA... nuc-to-vm-key
chpasswd:
expire: false
ssh_pwauth: true
package_update: true
packages:
- curl
- wget
- nfs-common
runcmd:
- systemctl enable nfs-client.target
instance-id: media-vm-001
local-hostname: media-vm
EOF
cat > /tmp/meta-data <<'EOF'
instance-id: media-vm-001
local-hostname: media-vm
EOF
genisoimage -output /mnt/libvirt-media/media-vm-cloud-init.img \
-volid cidata -joliet -rock \
/tmp/user-data /tmp/meta-data๐ง Step 4 โ Create the VM
virt-install \
--name media-vm \
--ram 8192 \
--vcpus 2 \
--disk path=/mnt/libvirt-media/noble-server-cloudimg-amd64.img,format=qcow2 \
--disk path=/mnt/libvirt-media/media-vm-cloud-init.img,device=cdrom \
--os-variant ubuntu24.04 \
--network network=default \
--graphics none \
--noautoconsole \
--import
virsh autostart media-vm
virsh net-dhcp-leases default
ssh -i ~/.ssh/nuc-to-vm-key jm@192.168.122.X๐ง Step 5 โ Mount QNAP NFS in the VM
sudo mkdir -p /mnt/Animes /mnt/Series /mnt/Films
sudo tee -a /etc/fstab <<'EOF'
192.168.1.11:/Animes /mnt/Animes nfs defaults,auto,vers=4.1,rw,nofail,bg,intr,rsize=262144,wsize=262144,noatime,_netdev 0 0
192.168.1.11:/Series /mnt/Series nfs defaults,auto,vers=4.1,rw,nofail,bg,intr,rsize=262144,wsize=262144,noatime,_netdev 0 0
192.168.1.11:/Films /mnt/Films nfs defaults,auto,vers=4.1,rw,nofail,bg,intr,rsize=262144,wsize=262144,noatime,_netdev 0 0
EOF
sudo systemctl daemon-reload && sudo mount -a๐ง Step 6 โ Install Sonarr, Radarr, SABnzbd
๐ฌ Sonarr
curl -o- https://raw.githubusercontent.com/Sonarr/Sonarr/develop/distribution/debian/install.sh | sudo bash
# User: jm / Group: jm๐ฌ Radarr
curl -o servarr-install-script.sh \
https://raw.githubusercontent.com/Servarr/Wiki/master/servarr/servarr-install-script.sh
sudo bash servarr-install-script.sh
# Choose 3 (Radarr) โ User: jm / Group: jm๐ฅ SABnzbd
sudo apt install -y sabnzbdplus
# Edit /etc/default/sabnzbdplus: USER=jm
sudo systemctl restart sabnzbdplusโ๏ธ Listen on all interfaces
By default, Sonarr and Radarr only listen on 127.0.0.1. Edit config.xml for both:
sudo systemctl stop sonarr radarr
# Change <BindAddress>0.0.0.0</BindAddress> in both config.xml files
sudo systemctl start sonarr radarrAnd for SABnzbd in /home/jm/.sabnzbd/sabnzbd.ini:
host = 0.0.0.0
host_whitelist = media-vm, sabnzbd.yourdomain.com,๐ง Step 7 โ Migrate data
# On the host
sudo systemctl stop sonarr radarr sabnzbdplus
scp -i ~/.ssh/nuc-to-vm-key -r /mnt/X5/config/sonarr jm@192.168.122.X:/tmp/
scp -i ~/.ssh/nuc-to-vm-key -r /mnt/X5/config/radarr jm@192.168.122.X:/tmp/
scp -i ~/.ssh/nuc-to-vm-key -r /mnt/X5/config/sabnzbd jm@192.168.122.X:/tmp/# In the VM
sudo systemctl stop sonarr radarr sabnzbdplus
sudo rm -rf /var/lib/sonarr /var/lib/radarr /home/jm/.sabnzbd
sudo cp -r /tmp/sonarr /var/lib/sonarr
sudo cp -r /tmp/radarr /var/lib/radarr
sudo cp -r /tmp/sabnzbd /home/jm/.sabnzbd
sudo chown -R jm:jm /var/lib/sonarr /var/lib/radarr /home/jm/.sabnzbd
sudo systemctl start sonarr radarr sabnzbdplus
# On the host โ disable old services
sudo systemctl disable --now sonarr radarr sabnzbdplus๐ง Step 8 โ nginx reverse proxy on the host
The VM runs on the libvirt NAT network 192.168.122.0/24. Update existing vhosts to point to the VM:
sed -i 's/http:\/\/127\.0\.0\.1:8989/http:\/\/192.168.122.X:8989/g' \
/etc/nginx/sites-enabled/sonarr.yourdomain.com
sed -i 's/http:\/\/127\.0\.0\.1:7878/http:\/\/192.168.122.X:7878/g' \
/etc/nginx/sites-enabled/radarr.yourdomain.com
nginx -t && systemctl reload nginx๐ Result
The media stack runs in an isolated VM with full access to QNAP NFS shares. Sonarr and Radarr see the intact library and SABnzbd receives downloads on the VM’s local disk before import.
| Service | URL | Internal port |
|---|---|---|
| Sonarr | https://sonarr.arleo.eu | 192.168.122.X:8989 |
| Radarr | https://radarr.arleo.eu | 192.168.122.X:7878 |
| SABnzbd | https://sabnzbd.arleo.eu | 192.168.122.X:6789 |
Key takeaways:
- ๐ก Always include two SSH keys in cloud-init (laptop + NUCโVM)
- ๐ก cloud-init files must be named exactly
user-dataandmeta-data - ๐ก
fallocatedoes not work on exFAT โ usedd - ๐ก Sonarr/Radarr listen on
127.0.0.1by default โ changeBindAddressinconfig.xml - ๐ก Add the vhost hostname to SABnzbd’s
host_whitelist