Automated post-install configuration for a dual HD/4K Servarr media stack on Ultra.cc. Runs entirely from GitHub Actions -- no SSH, no self-hosted runners.
Two GitHub Actions workflows configure your full stack via each service's API:
| Service | What Gets Configured |
|---|---|
| qBittorrent | Save path, automatic torrent management, 4 download categories |
| Sonarr (x2) | Root folder, qBit download client, media management, tags |
| Radarr (x2) | Root folder, qBit download client, media management, tags |
| Prowlarr | App connections to all 4 Arr instances, indexer sync |
| Jellyfin | 4 media libraries (TV, TV UHD, Movies, Movies UHD) |
| Jellyseerr | 2 Sonarr + 2 Radarr servers with default/4K assignments |
| Recyclarr | Quality profiles and custom formats from TRaSH Guides |
Every operation is idempotent. Running the workflow again skips anything already configured correctly.
| Instance | Purpose | qBit Category | Root Folder |
|---|---|---|---|
| Sonarr | TV Shows (HD) | tv-hd |
~/media/tv |
| Sonarr2 | TV Shows (4K) | tv-uhd |
~/media/tv-uhd |
| Radarr | Movies (HD) | movies-hd |
~/media/movies |
| Radarr2 | Movies (4K) | movies-uhd |
~/media/movies-uhd |
Anime goes into the TV libraries. No separate instance needed.
Complete these steps before running any workflow.
Install all of the following through the Ultra.cc control panel:
- qBittorrent
- Sonarr + Sonarr2
- Radarr + Radarr2
- Prowlarr
- Jellyfin
- Jellyseerr
Open https://{username}.{servername}.usbx.me/jellyfin and walk through the first-time setup:
- Create an admin user (username + password)
- Set preferred language and region
- Skip adding media libraries (the automation handles this)
- Finish the wizard
Then create an API key:
- Go to Administration > Dashboard > Advanced > API Keys
- Click + to create a new key
- Save the key for the secrets step below
Open https://{username}.{servername}.usbx.me/jellyseerr and walk through setup:
- Select Jellyfin as the media server type
- Enter the Jellyfin connection details:
- Hostname:
{username}.{servername}.usbx.me - Port:
443 - SSL: on
- URL Base:
/jellyfin
- Hostname:
- Sign in with your Jellyfin admin credentials
- Sync and enable libraries
- Skip the Sonarr/Radarr setup page (the automation handles this)
- Finish the wizard
- Go to Settings > General and note the API key
| App | Where to Find It |
|---|---|
| Sonarr | Settings > General > Security > API Key |
| Sonarr2 | Settings > General > Security > API Key |
| Radarr | Settings > General > Security > API Key |
| Radarr2 | Settings > General > Security > API Key |
| Prowlarr | Settings > General > Security > API Key |
| qBittorrent | Username/password from the Ultra.cc control panel |
| Jellyfin | Administration > Dashboard > Advanced > API Keys |
| Jellyseerr | Settings > General > API Key |
Secrets must be stored as environment secrets under an environment named ultra-prod. Repository-level secrets will not work because the workflows reference this specific environment.
- Go to Settings > Environments and click New environment
- Name it
ultra-prodand click Configure environment - Under Environment secrets, click Add secret and create each one:
| Secret | Value |
|---|---|
ULTRA_USERNAME |
Your Ultra.cc username |
ULTRA_SERVERNAME |
Your Ultra.cc server name (e.g. lw902) |
SONARR_API_KEY |
Sonarr API key |
SONARR2_API_KEY |
Sonarr2 API key |
RADARR_API_KEY |
Radarr API key |
RADARR2_API_KEY |
Radarr2 API key |
PROWLARR_API_KEY |
Prowlarr API key |
QBIT_USER |
qBittorrent Web UI username |
QBIT_PASS |
qBittorrent Web UI password |
JELLYFIN_API_KEY |
Jellyfin API key |
JELLYSEERR_API_KEY |
Jellyseerr API key |
Run the workflows in this order. All three steps are required on first setup.
- Go to Actions > Setup Servarr Stack
- Click Run workflow
- Leave services as
alland dry_run asfalse - Click Run workflow
This configures qBittorrent, all Arr instances, Prowlarr, Jellyfin, and Jellyseerr.
- Go to Actions > Recyclarr Sync
- Click Run workflow
This creates the quality profiles and custom formats on each Arr instance:
| Instance | Profile Created |
|---|---|
| Sonarr | WEB-1080p |
| Sonarr2 | WEB-2160p |
| Radarr | HD Bluray + WEB |
| Radarr2 | UHD Bluray + WEB |
- Go to Actions > Setup Servarr Stack
- Click Run workflow
- Set services to
jellyseerr - Click Run workflow
This assigns the quality profiles that Recyclarr just created to the Jellyseerr server connections. Without this step, Jellyseerr would fall back to whichever profile happened to exist first.
On subsequent runs, the profiles already exist and this extra step is not needed. Just run Setup and Recyclarr Sync in either order.
File: .github/workflows/setup.yml
Trigger: Manual (workflow_dispatch)
| Input | Type | Default | Description |
|---|---|---|---|
services |
string | all |
Comma-separated list of services, or all |
dry_run |
boolean | false |
Preview changes without applying them |
Valid service names: qbittorrent, sonarr, sonarr2, radarr, radarr2, prowlarr, jellyfin, jellyseerr.
File: .github/workflows/recyclarr-sync.yml
Trigger: Manual (workflow_dispatch)
Downloads the Recyclarr binary, builds service URLs from secrets, and syncs quality profiles + custom formats to all four Arr instances. Run this whenever you change config/recyclarr.yml.
Set dry_run to true when running the Setup workflow to preview what would change without modifying anything. In this mode:
- All connectivity checks and GET requests run normally
- All mutations (POST/PUT/DELETE) are logged but skipped
- The summary shows what would have changed
Useful for verifying secrets are correct and services are reachable before committing to changes.
If a service fails, re-run only the failed services instead of the full pipeline. The summary output tells you exactly what to pass:
============================================================
SETUP SUMMARY
============================================================
[OK] qbittorrent
[OK] sonarr
[FAIL] sonarr2
! ConnectionError: timeout
[OK] radarr
[OK] radarr2
[OK] prowlarr
[OK] jellyfin
[SKIP] jellyseerr
+ Skipped: unreachable
------------------------------------------------------------
Result: 5 succeeded, 1 failed, 1 skipped
Re-run with: --services sonarr2
============================================================
To retry, run the Setup workflow with services set to sonarr2,jellyseerr.
All paths live under /home/{ULTRA_USERNAME} on the same filesystem, which is required for hardlinks to work.
downloads/
qbittorrent/ # qBit default save path
tv-hd/ # Sonarr HD downloads
tv-uhd/ # Sonarr2 4K downloads
movies-hd/ # Radarr HD downloads
movies-uhd/ # Radarr2 4K downloads
media/
tv/ # Sonarr root folder (HD TV)
tv-uhd/ # Sonarr2 root folder (4K TV)
movies/ # Radarr root folder (HD Movies)
movies-uhd/ # Radarr2 root folder (4K Movies)
Torrents download in-place and seed from the same location. No incomplete/completed split, per TRaSH Guides recommendation.
Non-sensitive settings: paths, categories, instance definitions, media management preferences, and tags. Paths in this file are relative to /home/{ULTRA_USERNAME} and get resolved to absolute paths at runtime.
The api_key_secret field on each instance is the name of the GitHub Secret that holds the actual key, not the key itself.
Quality profile definitions using Recyclarr's !env_var syntax to read URLs and API keys from environment variables. The workflow injects these at runtime from GitHub Secrets.
Optimizations applied:
| Feature | Purpose |
|---|---|
reset_unmatched_scores |
Prevents stale CF score drift from manual changes |
delete_old_custom_formats |
Keeps instances clean when CFs are removed from guides |
| Safety CFs | Blocks bad dual groups, no-RlsGroup, obfuscated, retags, scene at -10000 |
| DV (w/o HDR fallback) | Blocked on 4K -- prevents purple/green on incompatible devices |
| SDR on 4K | Blocked -- no point grabbing SDR at 4K resolution |
| Upscaled on Radarr 4K | Blocked -- prevents fake 4K from HD sources |
| IMAX / IMAX Enhanced | Preferred at +800 -- up to 26% more picture |
| No audio CFs | Not needed for TV speakers / soundbar setups |
- Python +
requestshandles all API interactions - YAML config in the repo holds non-sensitive settings
- GitHub Secrets hold credentials and identity
- Standard GitHub-hosted runners connect to Ultra.cc services over HTTPS (all services are internet-accessible at
https://{username}.{servername}.usbx.me/{app})
The orchestrator (scripts/setup.py) runs services in dependency order:
- Validate -- hit each service's health endpoint; skip unreachable services
- qBittorrent -- set preferences (auto TMM, save path), create categories
- Sonarr -- root folder, qBit download client (tvCategory), media management, tags
- Sonarr2 -- same as Sonarr, different instance config
- Radarr -- root folder, qBit download client (movieCategory), media management, tags
- Radarr2 -- same as Radarr, different instance config
- Prowlarr -- connect all 4 Arr instances, trigger indexer sync
- Jellyfin -- create 4 media libraries
- Jellyseerr -- add 2 Sonarr + 2 Radarr servers, set default/4K flags
Each service runs in a try/except. Failures are collected, not thrown -- the pipeline continues and the summary reports what failed. Exit code is 0 if everything succeeded, 1 if anything failed.
Ordering matters: Prowlarr runs after the Arr instances (needs their configs to exist), and Jellyseerr runs after Jellyfin.
| Client | Auth Method | Used By |
|---|---|---|
ArrClient |
X-Api-Key header |
Sonarr, Radarr, Prowlarr |
QbitClient |
Session cookie via form login | qBittorrent |
JellyfinClient |
MediaBrowser Token="{key}" header |
Jellyfin |
JellyseerrClient |
X-Api-Key header |
Jellyseerr |
All clients share retry with exponential backoff on 5xx and connection errors (3 attempts), and dry-run support that allows GETs but logs and skips all mutations.
.github/
workflows/
setup.yml # Main infrastructure setup workflow
recyclarr-sync.yml # Quality profile sync workflow
config/
config.yml # Non-sensitive settings (paths, categories, toggles)
recyclarr.yml # Recyclarr quality profile definitions
scripts/
requirements.txt # Python dependencies (requests, pyyaml)
setup.py # Main orchestrator / entry point
validate.py # Connectivity validation
lib/
__init__.py
api_client.py # HTTP clients (auth, retries, dry-run)
config_loader.py # YAML config + secrets merging + path resolution
logger.py # Structured summary output
services/
__init__.py
qbittorrent.py # Preferences, categories
sonarr.py # Root folders, download client, media mgmt, tags
radarr.py # Root folders, download client, media mgmt, tags
prowlarr.py # App connections to all Arr instances
jellyfin.py # Library creation
jellyseerr.py # Sonarr/Radarr server connections
Hardlinks enabled -- Downloads and media folders are on the same filesystem (/home/{user}/). Sonarr/Radarr create hardlinks instead of copying, so files don't consume double disk space while seeding.
Video analysis disabled -- enableMediaInfo is set to false. On Ultra.cc shared hosting, media analysis causes unnecessary disk I/O strain with no practical benefit.
Propers/repacks set to "Do Not Prefer" -- Repack/proper scoring is handled through Recyclarr custom formats instead, which gives finer control.
No incomplete/completed download split -- Torrents download in-place in category subdirectories and seed from the same location. This is the recommended setup per TRaSH Guides.
Recyclarr is a separate workflow -- Quality profiles change less frequently than infrastructure. Running Recyclarr independently avoids unnecessary syncs and makes it easy to iterate on profiles without re-running the full setup.
Jellyseerr runs last -- It references quality profile names that Recyclarr creates. On first setup this requires a specific three-step order (Setup, Recyclarr, Setup with services=jellyseerr). On subsequent runs, the profiles already exist and order doesn't matter.