---
title: "KVM Media VM: migrating Sonarr, Radarr and SABnzbd into an isolated VM"
description: "Complete tutorial to create a dedicated KVM Ubuntu 24.04 VM for the media stack (Sonarr, Radarr, SABnzbd) and migrate data from the host, with NFS mounts and nginx reverse proxy."
url: "https://www.arleo.eu/en/posts/media-vm-migration/"
language: "en"
datePublished: "2026-05-03T18:00:00+02:00"
dateModified: "2026-05-15T23:51:25+02:00"
tags: ["homelab","nginx","infrastructure"]
categories: ["infrastructure"]
contentSignal: "ai-train=no, search=yes, ai-input=yes"
---

# KVM Media VM: migrating Sonarr, Radarr and SABnzbd into an isolated VM
Complete tutorial to create a dedicated KVM Ubuntu 24.04 VM for the media stack (Sonarr, Radarr, SABnzbd) and migrate data from the host, with NFS mounts and nginx reverse proxy.



## ⚡ 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.

```bash
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

```bash
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.

```bash
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

```bash
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

```bash
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

```bash
curl -o- https://raw.githubusercontent.com/Sonarr/Sonarr/develop/distribution/debian/install.sh | sudo bash
# User: jm / Group: jm
```

### 🎬 Radarr

```bash
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

```bash
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:

```bash
sudo systemctl stop sonarr radarr
# Change <BindAddress>0.0.0.0</BindAddress> in both config.xml files
sudo systemctl start sonarr radarr
```

And for SABnzbd in `/home/jm/.sabnzbd/sabnzbd.ini`:

```ini
host = 0.0.0.0
host_whitelist = media-vm, sabnzbd.yourdomain.com,
```

## 🔧 Step 7 — Migrate data

```bash
# 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/
```

```bash
# 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:

```bash
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-data` and `meta-data`
- 💡 `fallocate` does not work on exFAT — use `dd`
- 💡 Sonarr/Radarr listen on `127.0.0.1` by default — change `BindAddress` in `config.xml`
- 💡 Add the vhost hostname to SABnzbd's `host_whitelist`



## Tags

- homelab
- nginx
- infrastructure

## Categories

- infrastructure
