
Four VMs, one prompt: the canonical *arr suite for media automation
April 20, 2026
The *arr suite — Sonarr for TV, Radarr for movies, Prowlarr for indexers, Bazarr for subtitles — is the automation backbone of every self-hosted media stack. Sonarr is the eighth-most-deployed app in the 2026 r/selfhosted survey; the rest of the suite is right behind it in install rates. Together they turn a media server from a folder you fill by hand into a pipeline: you add a show or a movie to a watchlist, and it arrives downloaded, renamed into a clean folder structure, subtitled, and visible to Jellyfin or Plex without a single manual step.
The pieces have well-defined jobs. Prowlarr is the indexer manager: you add a tracker or Usenet indexer once, and Prowlarr syncs it out to every *arr automatically, so you never paste the same API key into four apps. Sonarr and Radarr watch your wanted list, ask Prowlarr for releases, hand the best match to a download client (qBittorrent, SABnzbd, NZBGet), then import the finished file into the library. Bazarr reads what Sonarr and Radarr know is in the library and fetches matching subtitles from providers like OpenSubtitles. The official TRaSH Guides — developed in close collaboration with the Sonarr and Radarr teams — are where most people learn to wire quality profiles and folder layout correctly.
This post walks through the canonical *arr lab on OpenFactory: four buildable VMs — Sonarr, Radarr, Prowlarr, and Bazarr — each on its own machine, all generated from one prompt and shipped as bootable ISOs. The ISOs carry the topology, the cross-app URLs, and compatibility stubs that prove the wiring; the real binaries, indexer credentials, and download client are the deployment-time work you layer on after first boot.
sonarr (10.77.0.10:8989) — TV PVR pointed at Prowlarr for indexers.radarr (10.77.0.20:7878) — movie PVR, same Prowlarr wiring.prowlarr (10.77.0.30:9696) — one-stop indexer manager that pushes config to Sonarr and Radarr.bazarr (10.77.0.40:6767) — subtitles for both, pre-wired to Sonarr and Radarr.system/status call with its real app name and exposes a tiny /metrics line, so a Prometheus scrape or a smoke test can tell the four apart before the production stack exists.10.77.0.0/24 breaks. You find out at build time, not when a download silently stops importing.Four Debian Trixie VMs on 10.77.0.0/24. Prowlarr is the indexer hub: Sonarr and Radarr both call it on :9696 to discover releases. Bazarr is the subtitle worker: it reaches back to the two PVRs on :8989 and :7878 to learn what the library contains. Nothing in this lab talks to the public internet — every arrow below is an east-west call inside the runner network.
10.77.0.0/24.Paste this verbatim into the chat builder at console.openfactory.tech. Nothing above or below it — the builder expects the prompt body to start at the “Build a compact multi-node lab…” line.
Build a compact multi-node lab named `arr-media-automation`.
Output discipline: keep the plan small. Use one startup script per node, about 25 shell lines or less. Do not install the real Sonarr, Radarr, Prowlarr, Bazarr, or any *arr / Mono / .NET dependencies at build time. Write deployment-time config examples and tiny Python stdlib or shell compatibility stubs only. The goal is a buildable preparation lab, not a production deployment.
## Topology
Create 4 buildable `debian-trixie` nodes, all `x86_64`, SSH enabled, DHCP/default route intact with lab aliases, firewall disabled, DNS `1.1.1.1` and `8.8.8.8`, user `ops` password `arr-ops` in `sudo`. Every recipe must set top-level `test_config` to `{ "enabled": false, "tests": [] }`.
- `sonarr`: role `tv-pvr`, 2 GB RAM, 16 GB disk, alias `10.77.0.10/24`, x `110`, y `60`
- `radarr`: role `movie-pvr`, 2 GB RAM, 16 GB disk, alias `10.77.0.20/24`, x `350`, y `60`
- `prowlarr`: role `indexer-mgr`, 2 GB RAM, 12 GB disk, alias `10.77.0.30/24`, x `110`, y `220`
- `bazarr`: role `subtitle-mgr`, 2 GB RAM, 12 GB disk, alias `10.77.0.40/24`, x `350`, y `220`
Connections: `sonarr` and `radarr` to `prowlarr:9696`; `bazarr` to `sonarr:8989` and `radarr:7878`.
## Common Recipe Requirements
All nodes: features `headless`, `ssh`; packages `openssh-server`, `python3`, `curl`, `jq`, `iproute2`, `netcat-openbsd`, `ca-certificates`. Each startup script adds the alias with `IFACE=$(ip route show default | awk '{print $5; exit}')`, `ip link set "$IFACE" up || true`, and `ip addr add <alias> dev "$IFACE" || true`. If `os.startup_scripts[].after` is present, it must be the string `"network-online.target"`, not an array.
## Node Requirements
All four nodes share the same compatibility-service shape with different ports and identity payloads. Each registers a Python stdlib service named `<app>-compat.service`.
`sonarr` (`0.0.0.0:8989`): `GET /ping` -> `200 {"status":"OK","appName":"Sonarr"}`; `GET /api/v3/system/status` -> `200 {"appName":"Sonarr","version":"compat-1.0","instanceName":"Sonarr-lab"}`; `GET /metrics` -> `sonarr_compat_up 1`. Env file `/etc/sonarr/sonarr.env` with `PORT=8989`, `PROWLARR_URL=http://10.77.0.30:9696`.
`radarr` (`0.0.0.0:7878`): same shape, `appName == "Radarr"`. Env `/etc/radarr/radarr.env` with `PORT=7878`, `PROWLARR_URL=http://10.77.0.30:9696`.
`prowlarr` (`0.0.0.0:9696`): `GET /ping` -> `200 {"status":"OK","appName":"Prowlarr"}`; `GET /api/v1/system/status` -> `200 {"appName":"Prowlarr","version":"compat-1.0"}`; `GET /metrics` -> `prowlarr_compat_up 1`. Env `/etc/prowlarr/prowlarr.env` with `PORT=9696`.
`bazarr` (`0.0.0.0:6767`): `GET /api/system/health` -> `200 {"app":"Bazarr","status":"OK"}`; `GET /api/system/status` -> `200 {"app":"Bazarr","version":"compat-1.0"}`; `GET /metrics` -> `bazarr_compat_up 1`. Env `/etc/bazarr/bazarr.env` with `PORT=6767`, `SONARR_URL=http://10.77.0.10:8989`, `RADARR_URL=http://10.77.0.20:7878`.
## Scenario
Emit exactly one group scenario named `arr-media-automation-validation`. Put `custom_tests[].assertions[]` inside the scenario entry; leave `scenarios[].tests` empty. Every assertion needs `on_vm`. Use only `port_listening`, `command_output`, and `http_responds`; do not emit `vm_boots`, `network_reachable`, or `service_running`.
- `Stack ports listen`: `port_listening` for `sonarr:8989`, `radarr:7878`, `prowlarr:9696`, `bazarr:6767`.
- `Sonarr identity`: on `sonarr`, `curl -fsS http://localhost:8989/api/v3/system/status | jq -e '.appName == "Sonarr"' >/dev/null && echo sonarr-ok`.
- `Radarr identity`: on `radarr`, `curl -fsS http://localhost:7878/api/v3/system/status | jq -e '.appName == "Radarr"' >/dev/null && echo radarr-ok`.
- `Prowlarr identity`: on `prowlarr`, `curl -fsS http://localhost:9696/api/v1/system/status | jq -e '.appName == "Prowlarr"' >/dev/null && echo prowlarr-ok`.
- `Bazarr health`: on `bazarr`, `curl -fsS http://localhost:6767/api/system/health | jq -e '.status == "OK"' >/dev/null && echo bazarr-ok`.
- `Cross-app reachability`: on `sonarr`, `nc -z -w 5 10.77.0.30 9696 && echo prowlarr-reachable`; on `bazarr`, `nc -z -w 5 10.77.0.10 8989 && nc -z -w 5 10.77.0.20 7878 && echo arrs-reachable`.
Preserve warnings that real Sonarr/Radarr/Prowlarr/Bazarr application binaries, API key rotation, download-client integration (qBittorrent / SABnzbd / NZBGet), indexer credentials, quality profile design, library import path mappings, off-host backups, and `10.77.0.0/24` aliasing are deployment-time concerns.Driving OpenFactory from an AI agent instead of the browser? The same flow is exposed through the OpenFactory MCP server — submit the prompt programmatically, get the build-plan preview back, and call create_build / start_vm on the resulting recipes. Single-image builds go straight through the openfactory CLI.
The prompt produces a buildable preparation lab — the right topology, the right ports listening, deployment-time config templates dropped in the right places, and tiny compatibility services that prove the wiring works. A few things still sit outside the recipe and need operator attention before this carries real load:
Three things the community keeps re-learning the hard way. Turn on authentication. Sonarr, Radarr, Prowlarr, and Bazarr all default to no login; an exposed instance leaks your indexer keys and your library. Set Authentication to Forms (Login Page) in each app's General settings before anything is reachable beyond localhost. Be deliberate about VPNs. Only the path to trackers needs a VPN — route Prowlarr through an indexer proxy rather than VPN-ing every app, because most *arr metadata and update servers sit behind Cloudflare and will block a shared VPN exit IP, breaking artwork and updates. Don't trust random third-party wrappers. When the popular Huntarr project imploded in 2026 over critical vulnerabilities and a nuked repo, the lesson was simple: anything you hand your *arr API keys to can become an attack surface (background).
Do I have to put each *arr on its own VM? No — plenty of people run all four in containers on one host. The point of the per-VM layout here is blast-radius isolation and clean per-app rollback. The prompt vocabulary collapses to fewer nodes just as easily if you'd rather consolidate.
Why do my public indexers keep going offline in Prowlarr? Many trackers put a Cloudflare “I'm not a robot” challenge in front of the site, which blocks Prowlarr's automated calls. A FlareSolverr companion service solves those challenges so the indexers stay Online — add it at deploy time alongside the real binaries.
What about a request front-end? Sonarr and Radarr expose wanted lists, but most people add Jellyseerr or Overseerr so household members can request titles themselves. That's a natural fifth node to fork this prompt into.
The *arr suite needs a front-end. Pair this lab with the Jellyfin media stack post or the Plex media stack post. For audiobooks and podcasts on the same network, see the Audiobookshelf library post. Driving fleets of these for a team? The Enterprise & GxP page covers larger rollouts, and pricing lays out the tiers.
OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.