This commit is contained in:
Mike McFetridge
2026-07-20 09:23:17 -04:00
parent c1315882da
commit 72272e4006
3179 changed files with 562960 additions and 14 deletions
@@ -0,0 +1,431 @@
---
tags:
- Docker
- Media
---
Directory Structure
cd docker
mkdir -p ~/arr-stack/{config/{gluetun,qbittorrent,prowlarr,radarr,sonarr},data/{torrents,media/{movies,tv}}}
cd ~/arr-stack
touch docker-compose.yml .env
```
# Tom Spark's ARR Stack — Automated Media Server
# https://github.com/loponai/arrstack
#
# Usage:
# 1. Copy .env.example to .env and fill in your VPN credentials
# 2. Run: bash setup-folders.sh
# 3. Run: docker compose up -d
#
# All VPN-protected services (qBittorrent, Prowlarr, FlareSolverr) run
# through Gluetun. If the VPN drops, traffic stops. Zero leaks.
#
# Radarr, Sonarr, Lidarr, Bazarr, Jellyfin, and Seerr do NOT run through
# the VPN — they need direct network access for speed and local connectivity.
networks:
arrnetwork:
name: arrnetwork
ipam:
config:
- subnet: 172.39.0.0/24
services:
# ============================================================
# GLUETUN — VPN Container (kill switch + tunnel)
# All VPN-protected services route through this container.
# Ports for those services are mapped HERE, not on the services themselves.
# Docs: https://github.com/qdm12/gluetun-wiki
# ============================================================
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
networks:
arrnetwork:
ipv4_address: ${IP_GLUETUN}
ports:
- 8000:8000 # Gluetun Control Server
- 8080:8080 # qBittorrent WebUI
- 6881:6881 # qBittorrent torrenting port
- 6881:6881/udp
- 9696:9696 # Prowlarr
- 8191:8191 # FlareSolverr
volumes:
- ./gluetun:/gluetun
environment:
- VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
- VPN_TYPE=${VPN_TYPE}
# --- WireGuard credentials (most providers) ---
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
# - WIREGUARD_PUBLIC_KEY=${WIREGUARD_PUBLIC_KEY}
- WIREGUARD_PRESHARED_KEY=${WIREGUARD_PRESHARED_KEY}
# --- OpenVPN credentials (if using OpenVPN instead) ---
# - OPENVPN_USER=${OPENVPN_USER}
# - OPENVPN_PASSWORD=${OPENVPN_PASSWORD}
# --- Server selection ---
- SERVER_COUNTRIES=${SERVER_COUNTRIES}
# --- Port forwarding (ProtonVPN, AirVPN, PIA) ---
# - VPN_PORT_FORWARDING=${VPN_PORT_FORWARDING}
- FIREWALL_VPN_INPUT_PORTS=${FIREWALL_VPN_INPUT_PORTS}
# --- General ---
- TZ=${TZ}
- BLOCK_MALICIOUS=off
- HTTP_CONTROL_SERVER_ADDRESS=:8000
- HTTP_CONTROL_SERVER_LOG=on
- HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE={"auth":"none"}
healthcheck:
test: wget -qO /dev/null http://127.0.0.1:9999 || exit 1
interval: 20s
timeout: 10s
retries: 5
restart: unless-stopped
# ============================================================
# QBITTORRENT — Torrent Client (runs through Gluetun VPN)
# ALL traffic goes through the VPN tunnel. No direct internet.
# ============================================================
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: service:gluetun
depends_on:
gluetun:
condition: service_healthy
restart: true
labels:
- deunhealth.restart.on.unhealthy=true
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
- WEBUI_PORT=8080
- TORRENTING_PORT=${FIREWALL_VPN_INPUT_PORTS}
volumes:
- ./qbittorrent:/config
- /data:/data
healthcheck:
test: wget -q --spider http://localhost:8080 || exit 1
interval: 60s
timeout: 10s
retries: 3
start_period: 20s
restart: unless-stopped
# ============================================================
# DEUNHEALTH — Auto-restarts unhealthy containers
# If qBittorrent loses VPN connection, this restarts it automatically.
# ============================================================
deunhealth:
image: qmcgaw/deunhealth
container_name: deunhealth
network_mode: none
environment:
- LOG_LEVEL=info
- HEALTH_SERVER_ADDRESS=127.0.0.1:9999
- TZ=${TZ}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: always
# ============================================================
# PROWLARR — Indexer Manager (runs through Gluetun VPN)
# Manages torrent/usenet indexers. Syncs to Radarr/Sonarr/Lidarr.
# ============================================================
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
network_mode: service:gluetun
depends_on:
gluetun:
condition: service_healthy
restart: true
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- ./prowlarr:/config
restart: unless-stopped
# ============================================================
# FLARESOLVERR — Cloudflare Bypass (runs through Gluetun VPN)
# Some indexers use Cloudflare protection. This gets around it.
# ============================================================
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr
network_mode: service:gluetun
depends_on:
gluetun:
condition: service_healthy
restart: true
environment:
- LOG_LEVEL=info
- TZ=${TZ}
restart: unless-stopped
# ============================================================
# RADARR — Movie Manager (NOT behind VPN)
# Searches via Prowlarr, sends downloads to qBittorrent,
# renames and hard-links completed files to media folder.
# ============================================================
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- ./radarr:/config
- /data:/data
ports:
- 7878:7878
networks:
arrnetwork:
ipv4_address: ${IP_RADARR}
restart: unless-stopped
# ============================================================
# SONARR — TV Show Manager (NOT behind VPN)
# Same pattern as Radarr but for TV series.
# ============================================================
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- ./sonarr:/config
- /data:/data
ports:
- 8989:8989
networks:
arrnetwork:
ipv4_address: ${IP_SONARR}
restart: unless-stopped
# ============================================================
# LIDARR — Music Manager (NOT behind VPN)
# Optional. Comment out if you don't need music automation.
# ============================================================
lidarr:
image: lscr.io/linuxserver/lidarr:latest
container_name: lidarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- ./lidarr:/config
- /data:/data
ports:
- 8686:8686
networks:
arrnetwork:
ipv4_address: ${IP_LIDARR}
restart: unless-stopped
# ============================================================
# BAZARR — Subtitle Manager (NOT behind VPN)
# Automatically downloads subtitles for movies and TV shows.
# ============================================================
bazarr:
image: lscr.io/linuxserver/bazarr:latest
container_name: bazarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- ./bazarr:/config
- /data:/data
ports:
- 6767:6767
networks:
arrnetwork:
ipv4_address: ${IP_BAZARR}
restart: unless-stopped
# ============================================================
# JELLYFIN — Media Server (NOT behind VPN)
# Your personal streaming service. Plays movies, TV, music.
# Needs full bandwidth — never put this behind the VPN.
# ============================================================
jellyfin:
image: lscr.io/linuxserver/jellyfin:latest
container_name: jellyfin
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=${TZ}
volumes:
- ./jellyfin:/config
- /data/media:/data/media
ports:
- 8096:8096
# Uncomment the lines below to enable hardware transcoding (Intel Quick Sync / VAAPI).
# Only works if your system has Intel/AMD integrated graphics (/dev/dri must exist).
# If you get an error about /dev/dri not found, leave these commented out.
# devices:
# - /dev/dri:/dev/dri
networks:
arrnetwork:
ipv4_address: ${IP_JELLYFIN}
restart: unless-stopped
# ============================================================
# SEERR — Request System (NOT behind VPN)
# Netflix-like UI for requesting movies and TV shows.
# Share this with family — they never need to touch Radarr.
#
# Seerr is the unified successor to Overseerr and Jellyseerr
# (merged under seerr-team). Supports Plex, Jellyfin, and Emby.
#
# Config uses a NAMED Docker volume (not a bind mount). This is
# required: Seerr runs as the `node` user (UID 1000) and a
# bind-mounted host folder is created root-owned, causing a
# permission-denied crash loop. On Windows/WSL, bind mounts also
# corrupt the SQLite DB over SMB. Named volumes fix both cases
# (matches upstream Seerr docs).
#
# Migrating from ./jellyseerr or ./seerr bind mount? See README
# troubleshooting "Migrating Seerr config to a named volume".
# ============================================================
seerr:
image: ghcr.io/seerr-team/seerr:v3.0.1
init: true
container_name: seerr
environment:
- LOG_LEVEL=info
- TZ=${TZ}
- PORT=5055
volumes:
- seerr_config:/app/config
ports:
- 5055:5055
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:5055/api/v1/settings/public || exit 1
start_period: 20s
timeout: 3s
interval: 15s
retries: 3
networks:
arrnetwork:
ipv4_address: ${IP_SEERR}
restart: unless-stopped
# ============================================================
# AUDIO BookShelf — Server (NOT behind VPN)
# Your personal streaming service. Plays all audiobooks.
# Needs full bandwidth — never put this behind the VPN.
# ============================================================
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:latest
container_name: audiobookshelf
ports:
- 13378:80
volumes:
- /data/media/audiobookshelf/books:/audiobooks
- /data/media/audiobookshelf/podcasts:/podcasts
- /data/media/audiobookshelf/metadata:/metadata
- ./audiobookshelf/config:/config
restart: unless-stopped
networks:
arrnetwork:
ipv4_address: ${IP_AUDIO}
# ============================================================
# NAVIDROME — Music Media Server (NOT behind VPN)
# Your personal streaming service. Plays music.
# Needs full bandwidth — never put this behind the VPN.
# ============================================================
navidrome:
image: deluan/navidrome:latest
user: 1000:1000
container_name: navidrome
ports:
- 4533:4533
restart: unless-stopped
environment:
ND_SCANSCHEDULE: "1h"
ND_LOGLEVEL: info
ND_SESSIONTIMEOUT: "24h"
volumes:
- ./navi/data:/data
- /data/media/music:/music:ro
networks:
arrnetwork:
ipv4_address: ${IP_NAVI}
volumes:
seerr_config:
```
## 2. Step-by-Step qBittorrent Configuration
Once your stack is running via `docker compose up -d`, navigate to your qBittorrent interface at `http://localhost:8118` or your server's IP address (e.g., `[http://192.168.1.50:8118](http://192.168.1.50:8118)`).
### Step 2a: Log In and Find Credentials
- Look at your container logs (`docker logs qbittorrent`) to locate the **temporary password** generated automatically by linuxserver/qbittorrent for security.
- Use `admin` as the username and paste that password.
- _Recommended:_ Instantly go to **Tools > Options > Web UI** to change the password to a permanent one.
### Step 2b: Configure Connection & Port Forwarding
This ensures your traffic efficiently routes through AirVPN's system:
**1.Open Connection Settings:**Inside Web UI.
Go to **Tools** in the top navigation bar and select **Options**. In the sidebar menu that pops up, click on **Connection**.
**2.Set the Torrenting Port:**Match Gluetun mapping.
Locate **Port used for incoming connections** and type in exactly: `29261`.
**3.Disable UPnP / NAT-PMP:**Security step.
**Uncheck** the box next to _Use UPnP / NAT-PMP port forwarding from my router_. Your VPN handles port assignment, so your local router shouldn't intervene.
### Step 2c: Bind to the VPN Interface (The Hard Kill Switch)
Binding ensures that if Gluetun ever collapses, leaks, or drops connection, qBittorrent immediately stops downloading or seeding instead of trying to pass data via an unprotected network.
**1.Open Advanced Panel:**Scroll down sidebar.
While still in the Options menu, scroll down the left sidebar panel and select **Advanced**.
**2.Bind Network Interface:**Target VPN tunnel.
Look for **Network interface** near the top of the list. Change the dropdown menu from _Any interface_ to exactly **`tun0`**.
**3.Bind IP Address:**Optional but safer.
Look right underneath at the **Optional IP address to bind to** setting. Change it from _All addresses_ to **`All IPv4 addresses`**.
**4.Save and Restart:**Apply changes.
Click **Save / Apply** at the bottom right. Restart your container with `docker compose restart qbittorrent` to guarantee the network interfaces lock into place cleanly.
@@ -0,0 +1,254 @@
---
title: Configuring Arr Stack
source: https://mafyuh.com/posts/arr-stack-config-guide/
author:
- "[[Matt]]"
published: 2024-02-24
created: 2026-07-09
description: This is blog post 2/2 on setting up a Arr stack using docker. This post will touch on configuring these services and what I have learned when using these services. I will be referring to TRaSH-Guides often in this post. Its like the bible of the Arrs so I would look there for more options, especially if your setup differs from mine.PrerequisitesAny Usenet Server Subscription (preferred)Any Usenet Indexer Subscription (preferred)Real-Debrid Subscription (if you like torrents being fast)VPN Subscription (Bare minimum needed to download torrents)ConfigurationProwlarrWe will first start with Prowlarr and Sabnzbd to get all of this out of the way, and its the part where youre gonna spend some $
tags:
- clippings
- Media
---
This is blog post 2/2 on setting up a Arr stack using docker. This post will touch on configuring these services and what I have learned when using these services. I will be referring to [TRaSH-Guides](https://trash-guides.info/) often in this post. Its like the bible of the Arrs so I would look there for more options, especially if your setup differs from mine.
## Prerequisites
- Any Usenet Server Subscription (preferred)
- Any Usenet Indexer Subscription (preferred)
- Real-Debrid Subscription (if you like torrents being fast)
- VPN Subscription (Bare minimum needed to download torrents)
## Configuration
## Prowlarr
We will first start with Prowlarr and Sabnzbd to get all of this out of the way, and its the part where youre gonna spend some $
1st thing to do is load up your web browser and go to Prowlarr. In your URL bar put http://{IP-ADDRESS}:9696.
You should be prompted to create an account. It doesnt matter much if you choose Basic or Forms for authentication.
This guide is just going to be using Usenet, as I recommend it over torrenting.
Prowlarrs job is to search thru all the indexers and return those results over to Radarr/Sonarr.
### Indexers
You can find links to all the indexers out there on this Usenet [subreddit](https://www.reddit.com/r/usenet/wiki/indexers/). I will provide a screenshot of my Indexers list:
![Indexer List](https://mafyuh.com/assets/img/indexer-list.png)
I would say the most downloaded stuff I have is from NZBgeek, followed by DrunkenSlug then NZBFinder. If I were to only be able to have 1 it would be NZBgeek for sure.
Sign up for whatever service you want, once you pay they should give you your API key, usually under profile on the indexer website. Take that API over to Prowlarr and you should be able to select your indexer from the list, paste in your API key and save. You can also click on the wrench icon next to the indexer and set your VIP expiration date, so Prowlarr reminds you to renew.
I paid for as lifetime altHUB account as well as yearly for Slug and Geek, A lot of the others that are under Interactive Search Sync Profile are free accounts that they limit the amount of API hits per day. You can tell Prowlarr this to by setting up Sync Profiles like I have so it doesnt burn your API hits fast. [Link](https://trash-guides.info/Prowlarr/prowlarr-setup-limited-api/)
Thats pretty much it for indexers, now you just need a provider which is covered in the Sabnzbd portion.
### Usenet vs Torrenting
Usenet wins all day IMO. I started off with torrenting years ago and there would always be something I couldnt find. Once I found usenet that problem went away and hasnt returned. I still have a Real-Debrid subscription, however I am not planning on renewing when it runs up as I just dont need torrents anymore for media. The only exception being PPV fights, which at least with the indexers I use, I cant find much on usenet. Usenet grabs about 95% of what I need. And Im sure the other 5% would have still been found, but torrent won the algorithm battle. You also do not need to use a VPN when using Usenet as all big providers use HTTPS.
If you are new to Usenet, their [subreddit](https://www.reddit.com/r/usenet/wiki/index/) wiki will help you out. Unlike torrenting which is peer to peer and fully decentralized, usenet is more centralized and has many servers that host the content (Providers). There is a monthly fee for being able to access these servers. You also need an indexer in order to find NZB files, which is then sent to your provider for downloading. Indexers are much cheaper at usually a few bucks a year.
### Connecting Prowlarr to Radarr/Sonarr
For now skip to setting up Radarr and Sonarr, after setting up you will need their API keys which can be found under **Settings - General - API Key**.
In Prowlarr go to **Settings - Apps - Create an application** and click Radarr, filling in all api key and changing the host if needed. Do the same thing for Sonarr. This automatically syncs all your indexers into Radarr/Sonarr and if you want to add more indexers you just have to add it in Prowlarr and not 2 separate places.
## Sabnzbd
Sab is what connects to our Usenet providers and downloads the NZB files that Radarr/Sonarr gave to it.
First load up sab at http://{IP\_ADDRESS}:8080. One of the first things it has you do is enter you server details, which takes us into providers.
### Providers
These are the costliest part of the process, although for good reason. There are a bunch of providers out there, which all can be found [here](https://www.reddit.com/r/usenet/wiki/providers/).
I have used Newshosting, NewsDemon and UsenetNow. You only need 1 of these to download most stuff, but I prefer to have 2 as I have seen some servers dont have a file that another one did. Its rare but happens. As of the time of me writing this I am using UsenetNow for $6/month and Newsdemon $50 for 12 months using this [link](https://members.newsdemon.com/billinginfo.php?pricepointid=20230413) (Not referral just found a promo link on Reddit I saved)
Once you select your provider and pay, they should email you your login credentials that you just put into Sab. Remember to set the connections to whatever your provider allows to maximize download speeds.
### Folders
All we really need to do to configure Sab is to set the Temp and Completed Download folders. For Temporary:
```
/data/usenet/incomplete
```
and for complete:
```
/data/usenet/complete
```
Make sure you save changes.
## Radarr
### Initial Setup
1st thing to do is load up your web browser and go to Radarr. In your URL bar put http://{IP-ADDRESS}:7878.
Putting in the IP of you Ubuntu machine. If you are using something like WSL just use localhost as IP.
You should be prompted to create an account. It doesnt matter much if you choose Basic or Forms for authentication.
If you are starting fresh with no content, go to **Settings - Media Management - Root Folders** and add a root folder, you can use the movies folder that was located at /data/media/movies or you can go into /data/media and name your folders however you want to.
My media directory breaks down the movies by category: 4K, Movies, Marvel, DC, Kids, Stand-Up, Requested Content and Fights. They are added to Radarr individually as follows:
![Root Folders](https://mafyuh.com/assets/img/radarr-rootfolder.png#center)
If you already have media you want to import into Radarr, click **Movies - Library Import - Start Import**. Remember all you files are going to be under /data/media if you followed my installation guide. This also adds the chosen directory as a root folder.
### Connect Sab to Radarr
To connect Sab to Radarr, in Radarr go to **Settings - Download Clients** - add - Sabnzbd - fill in all your Sab details, getting the API key from **Sab - General - Api Key** (Not NZB Key)
### Naming Files
Its best practice to rename your files, Radarr does this automatically for you. To do this go to **Settings - Media Management - Movie Naming**. Make sure you check the Rename Movies checkbox. Everything should be fine by default besides Standard Movie Format, Jellyfin recommends this:
```
{Movie CleanTitle} {(Release Year)} [imdbid-{ImdbId}] - {Edition Tags }{[Custom Formats]}{[Quality Full]}{[MediaInfo 3D]}{[MediaInfo VideoDynamicRangeType]}{[Mediainfo AudioCodec}{ Mediainfo AudioChannels]}[{Mediainfo VideoCodec}]{-Release Group}
```
Make sure you hit save.
Note this only automatically renames the file if it was downloaded from Radarr, if you imported your own media you need to manually click rename files under **Movies** - Click Edit Movies - Select All - Rename Files on bottom of screen.
### Custom Formats
Every single one of these custom formats is from trash-guides, you can see how to import them [here](https://trash-guides.info/Radarr/Radarr-import-custom-formats/). ![Custom Formats](https://mafyuh.com/assets/img/radarr-customformats.png)
Add all custom formats that you want to look out for, whether good or bad. What custom formats do is essentially put a label on files just based off the files name, as well as provide these labels a score which is used when auto-searching for a movie. Just adding custom formats on its own does nothing, setting up a Quality profile to rank these custom formats is how we pick and choose what we want. Reading through trash guides will yield good results.
### Quality Profiles
Now you need to ask yourself what your plan here is, do you want the highest possible quality content with no regards to storage space? Do you want a giant library filled with lower quality content? Do you have a 4K TV? Do you have HDR/HDR10/Dolby Vision on your TV? Do you have a Dolby Atmos system and want the top notch audio? What about the people you maybe plan on sharing these files with, do you know their TVs capabilities? Are you gonna be using Plex or Jellyfin to stream this media?
You should consider all these things when setting this up so you dont have to manually search for files. Thats the point of this whole section is to automate the searching process and find the correct kinds of files every time. Trash-Guides put together multiple flow charts on what custom formats to use in certain situations which can be found [here](https://trash-guides.info/Radarr/radarr-setup-quality-profiles/#which-quality-profile-should-you-choose)
I am going to put my logic behind how I have my setup configured, although you should review [this Trash-Guide](https://trash-guides.info/Radarr/radarr-setup-quality-profiles/) for more details.
1080P: ![1080p Quality Profile](https://mafyuh.com/assets/img/hd-radarr.png) \*Not Shown custom format scores
- 3D:-10000
- BR-DISK:-10000
- Bad Dual Groups:-10000
- EVO (no WEBDL):-10000
- LQ (Release Title):-10000
- x265 (HD):-10000 (This is Trash-Guides Golden Rule, 1080p=x264 | 4k=x265)
Not much logic here other than not getting crappy releases and only downloading x264 if its 1080P. File size limits can be put to get smaller files. I just follow [this](https://trash-guides.info/Radarr/Radarr-Quality-Settings-File-Size/).
I want x264 as its gonna have the highest compatibility with the most devices and the less transcoding I need to do the better.
4K: ![4K Quality Profile](https://mafyuh.com/assets/img/4k-radarr.png) ![4k Radarr 2](https://mafyuh.com/assets/img/4k-radarr-2.png#center)
I have a 4K TV with Dolby Vision Support, along with a Dolby Atmos sound system. So making sure my 4K files have Dolby Vision is pretty important, also having the extended screen of IMAX on a home sized TV is amazing. Having both of those together is the perfect recipe IMO, yet not all movies support one or the other so not as common as you may think. But generally if I want something 4K it will have DV support. I read on trash guides that if you have DV files without DV HDR10 as a fallback, the video will appear off-color to TVs that dont have DV. So I try to make sure DV with HDR10 fallback is 1st. File size is not taken into consideration at all, I have multiple ~90GB movies.
Also if your TV doesnt have DV support, get one. In my experience the difference between HDR10/10+ and DV is night and day. And I usually am not a fan of proprietary software at all. The next best if you dont have DV is HDR10+. But ultimately having any HDR is gonna be better than SDR.
I ended up removing the Atmos custom format as most of the time the insane high quality files will have Atmos, and it could just be my setup but I dont notice too much a difference between DD+ and Atmos.
### Conclusion
With all this done you should be able to a add a new movie and watch it download over in Sab. Then when finished downloading it should be automatically moved to your target directory.
### Tips
1. If you ever find a file that works well and you dont want Radarr to mess with it, unmonitor it. That way Radarr doesnt upgrade it.
2. If you are brand new to Radarr I would learn the basics of it in the web-ui, then proceed to utilizing the API with apps like [Nzb360](https://nzb360.com/), [LunaSea](https://www.lunasea.app/), and [Overseerr](https://overseerr.dev/) / [Jellyseerr](https://github.com/Fallenbagel/jellyseerr) to request stuff with a better front end. Theres even [Doplarr](https://docs.linuxserver.io/images/docker-doplarr/#application-setup) the Discord bot.
3. Say you found a specific version of a movie, but the audio is bad or a different language, and you cant find another video quality the same, you can use a tool called [mkvtoolnix](https://mkvtoolnix.download/) to merge audio tracks from one file to anothers video track. You will probably run into this at some point if youre really looking for something specific.
4. You can manually import Boxing/WWE/UFC events into Radarr and the metadata will apply, but searching in Radarr doesnt work for these types of events. Manually finding the NZBs or torrents and moving files seems to be only way.
## Sonarr
### Initial Setup
Sonarr is Radarr but for TV Shows.
First thing load up Sonarr on your web browser at http://{IP\_ADDRESS}:8989.
Make an account. Then do the same thing as Radarr and add a new root folder if starting new, or import your existing media. The root folder location should be /data/media/tv
Then we need to connect Sab to Sonarr, to do this Go to **Settings - Download Clients - Add** just like we did with Radarr, fill in all your Sab details.
### Custom Formats
Again all these custom formats are on trash-guides. Link [here](https://trash-guides.info/Sonarr/sonarr-collection-of-custom-formats/). ![Sonarr Custom Format](https://mafyuh.com/assets/img/sonarr-cf.png) I just picked all major streaming services and the Tiers. Import them the same way you did Radarr.
### Quality Profile
Go to **Settings - Profiles - Quality Profiles** ![Sonarr 1080](https://mafyuh.com/assets/img/sonarr-1080p.png) \*Not Shown custom format scores
- x265 (HD):-10000
- BR-DISK:-10000
I only have this 1 1080p profile as I personally do not want to waste that storage space on a long TV series. I do not have any 4K TV shows at all. This generally always grabs the best available file. Again for Size Limit I follow [this trash-guide](https://trash-guides.info/Sonarr/Sonarr-Quality-Settings-File-Size/).
Having x265(HD) as -10000 means it will not download a 1080p file if it is encoded in x265, again this is for compatibility as x264 just works on more devices.
### Naming Files
I also rename Sonarrs files, to do this go to **Settings - Media Management** - check Rename Episodes and under Standard Episode format set:
```
{Series TitleYear} - S{season:00}E{episode:00} - {Episode CleanTitle} [{Custom Formats }{Quality Full}]{[MediaInfo VideoDynamicRangeType]}{[Mediainfo AudioCodec}{ Mediainfo AudioChannels]}{[MediaInfo VideoCodec]}{-Release Group}
```
Make sure you save changes.
### Tips
1. Dont download a whole series at once if it has many seasons, you may fill up on space on your VM. Which will stop everything on that VM. Usually they download faster than they transfer to target directory. So space can add up quick. I usually do 2-3 seasons at a time.
## Bazarr
Bazarr is used to get subtitles for all your content. Sometimes the files come with subtitle tracks, Bazarr covers you when they dont.
### Initial Setup
First thing load up Bazarr on your web browser at http://{IP\_ADDRESS}:6767.
First thing is to set your language, go to **Settings - Languages - Languages Filter** and set the filter to your language. I set to English.
Then under Language Profiles click Add New Profile:
- Name: English
- Click Add Language, english should pop up by default.
- Set the cutoff to en
- Save
At the bottom of this page there is Default Settings, check both boxes for Series and Movies and choose your language profile.
### Providers
Under **Settings - Providers** - Add a Provider:
![Bazarr Providers](https://mafyuh.com/assets/img/bazarr-prov.png)
YIFY, TVSubtitles, Supersubtitles, are all free and dont require an account.
opensubtitles.com requires you to create an account first but is free.
I have a OpenAI whisper model running on a separate VM which uses my GPU and AI to generate subtitles for content as well, And its pretty good even with the base model. Its rarely needed as usually I find better subtitles thru another provider first with a higher score. But for those times when subtitles cant be found its nice they can be generated. I found [this](https://wiki.bazarr.media/Additional-Configuration/Whisper-Provider/) in Bazarrs docs.
Make sure you save your changes.
### Connecting Bazarr to Radarr/Sonarr
Now you just need to tell Bazarr where your arrs are located. Go to **Settings - Sonarr** for Sonarr and **Settings - Radarr** for Radarr. Filling in all your details and saving.
## Conclusion
Congrats you now have a fully automated backend for downloading media! Good time to cancel those 10 streaming subscriptions and start downloading what you wanna watch. Theres not many guides out there about this sort of thing, as piracy leaves some ethical concerns. But idrc, Ive been a pirate my whole life, the fact that you can make a system like this all for way cheaper than streaming services is mind-blowing to me.
Now just hook up Jellyfin/Plex up to you /data/media directory and start watching with no ads!
@@ -0,0 +1,493 @@
> ## ⚡ Want the easy, full version? → **[SparkBox](https://tomsparkbox.com)**
>
> This is one of my original one-shot scripts. It still works — but these days everything I build goes into **[SparkBox](https://tomsparkbo>
>
> ### 👉 Get it free at **[tomsparkbox.com](https://tomsparkbox.com)**
>
> _Built by [Tom Spark Reviews](https://youtube.com/@TomSparkReviews)._
---
# Tom Spark's ARR Stack
One-command automated media server with VPN protection. Sonarr, Radarr, Prowlarr, qBittorrent, Gluetun, Jellyfin, and more.
**Full video tutorial:** [YouTube Link Coming Soon]
## What You Get
| Service | Port | Purpose |
|---------|------|---------|
| Gluetun | — | VPN tunnel with kill switch |
| qBittorrent | 8080 | Torrent client (VPN protected) |
| Prowlarr | 9696 | Indexer manager (VPN protected) |
| FlareSolverr | 8191 | Cloudflare bypass (VPN protected) |
| Radarr | 7878 | Movie automation |
| Sonarr | 8989 | TV show automation |
| Lidarr | 8686 | Music automation |
| Bazarr | 6767 | Subtitle automation |
| Jellyfin | 8096 | Media server / streaming |
| Seerr | 5055 | Netflix-like request UI (Overseerr/Jellyseerr successor) |
All download traffic routes through Gluetun's VPN tunnel. If the VPN drops, all traffic stops — zero leaks. The deunhealth container auto-r>
## Quick Start
### 1. Install Docker
```bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in for group change to take effect
```
### 2. Clone this repo
```bash
git clone https://github.com/loponai/arrstack.git
cd arrstack
```
### 3. Create folder structure
```bash
sudo bash setup-folders.sh
```
This creates:
```
/data/
├── torrents/ ← qBittorrent downloads here
│ ├── movies/
│ ├── tv/
│ └── music/
└── media/ ← Radarr/Sonarr organize files here (Jellyfin reads from here)
├── movies/
├── tv/
└── music/
```
> **Hard links:** Both folders MUST be on the same drive/filesystem. Radarr and Sonarr create hard links (not copies) — the file appears in>
### 4. Configure your VPN
```bash
cp .env.example .env
nano .env
```
Fill in your VPN provider and credentials. See [VPN Setup Guides](#vpn-setup-guides) below.
### 5. Launch
```bash
docker compose up -d
```
### 6. Verify everything is working
```bash
bash test-stack.sh
```
This runs a full health check — Docker status, VPN connection, IP leak test, service accessibility, hard link support, and folder permissio>
You can also check manually:
```bash
# Check Gluetun's IP (should be VPN, not your real IP)
docker exec gluetun wget -qO- ifconfig.me
# qBittorrent shares Gluetun's network, so the above proves both are tunneled.
docker exec qbittorrent wget -qO- ifconfig.me
# Check health status of all containers
docker ps --format "table {{.Names}}\t{{.Status}}"
```
### 7. Configure services
Open each service in your browser at `http://YOUR-SERVER-IP:PORT` and follow the video tutorial for step-by-step configuration.
**Quick reference:**
- **qBittorrent** (`:8080`) — Get temp password: `docker logs qbittorrent 2>&1 | grep "temporary password"`
- **Prowlarr** (`:9696`) — Add indexers, connect to Radarr/Sonarr. If an indexer is blocked by Cloudflare, set up FlareSolverr as a proxy: >
- **Radarr** (`:7878`) — Root folder: `/data/media/movies`, download client category: `movies`
- **Sonarr** (`:8989`) — Root folder: `/data/media/tv`, download client category: `tv`
- **Jellyfin** (`:8096`) — Add libraries: `/data/media/movies`, `/data/media/tv`, `/data/media/music`. To watch, open `http://YOUR-SERVER-I>
- **Seerr** (`:5055`) — Connect to Jellyfin, Radarr, and Sonarr. Seerr is the unified successor to Overseerr/Jellyseerr. If you previously >
**Internal Docker IPs — use these when connecting services to each other (NOT localhost):**
| IP | Service |
|----|---------|
| `172.39.0.2` | Gluetun (also qBittorrent, Prowlarr, FlareSolverr) |
| `172.39.0.3` | Radarr |
| `172.39.0.4` | Sonarr |
| `172.39.0.5` | Lidarr |
| `172.39.0.6` | Bazarr |
| `172.39.0.7` | Jellyfin |
| `172.39.0.8` | Seerr |
These IPs are the same for everyone — they're hardcoded in the docker-compose file.
**Common connections:**
- Radarr/Sonarr → Download Client → qBittorrent: host `172.39.0.2`, port `8080`
- Prowlarr → Apps → Radarr: server `http://172.39.0.3:7878`
- Prowlarr → Apps → Sonarr: server `http://172.39.0.4:8989`
- Prowlarr → Apps → Prowlarr Server: `http://172.39.0.2:9696`
- Seerr → Radarr: host `172.39.0.3`, port `7878`
- Seerr → Sonarr: host `172.39.0.4`, port `8989`
- Seerr → Jellyfin: host `172.39.0.7`, port `8096`
**Important Radarr/Sonarr settings:**
- Media Management → Show Advanced → **Use Hardlinks instead of Copy** → must be ON
- Media Management → **Rename Movies/Episodes** → recommended ON
**Recommended quality profile (1080p baseline, 4K preferred):**
Go to Settings → Profiles and edit or create a profile:
1. Uncheck everything below 1080p (720p, 480p, etc.)
2. Check/enable everything from **HDTV-1080p** up through **Bluray-2160p**
3. Set **Cutoff** to `Bluray-1080p` — this is the minimum quality Radarr/Sonarr will be happy with
4. Set **Upgrade Until** to `Bluray-2160p` — it will automatically upgrade to 4K if one becomes available
This means it grabs a 1080p release right away so you can start watching, then silently upgrades to 4K later if it finds one.
## VPN Setup Guides
### Surfshark (Recommended)
The best value for torrenting — cheapest long-term plans, fast WireGuard speeds, and easy setup with Gluetun. [Get Surfshark](https://get.s>
1. Go to [Surfshark Manual Setup](https://my.surfshark.com/vpn/manual-setup/main)
2. Select **WireGuard** and get your credentials (private key + address)
3. In your `.env`:
```
VPN_SERVICE_PROVIDER=surfshark
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=your_key_here
WIREGUARD_ADDRESSES=10.14.0.2/16
SERVER_COUNTRIES=United States
```
> The `.env.example` file is pre-configured for Surfshark. Just paste your private key and you're good to go.
### NordVPN
1. Go to [NordVPN Manual Setup](https://my.nordaccount.com/dashboard/nordvpn/manual-configuration/)
2. Select **NordLynx** (WireGuard) and generate a private key
3. In your `.env`:
```
VPN_SERVICE_PROVIDER=nordvpn
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=your_key_here
WIREGUARD_ADDRESSES=10.5.0.2/16
SERVER_COUNTRIES=United States
```
### ProtonVPN
1. Go to [ProtonVPN WireGuard Config](https://account.protonvpn.com/) → Downloads → WireGuard
2. Generate a config file, open it, copy `PrivateKey` and `Address`
3. In your `.env`:
```
VPN_SERVICE_PROVIDER=protonvpn
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=your_key_here
WIREGUARD_ADDRESSES=10.2.0.2/32
SERVER_COUNTRIES=United States
VPN_PORT_FORWARDING=on
```
### AirVPN
1. Go to [AirVPN Config Generator](https://airvpn.org/) → Client Area → Config Generator
2. Select Linux → WireGuard → pick server → Generate
3. In your `.env`:
```
VPN_SERVICE_PROVIDER=airvpn
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=your_key_here
WIREGUARD_PUBLIC_KEY=server_public_key
WIREGUARD_PRESHARED_KEY=your_preshared_key
WIREGUARD_ADDRESSES=your_ip/32
FIREWALL_VPN_INPUT_PORTS=your_port
VPN_PORT_FORWARDING=on
```
### Other Providers
Gluetun supports 30+ providers. Check the [full provider list](https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers).
## Starting and Stopping
**Start the stack:**
```bash
cd arrstack
docker compose up -d
```
**Stop the stack:**
```bash
cd arrstack
docker compose down
```
**Check status:**
```bash
docker ps --format "table {{.Names}}\t{{.Status}}"
```
### Auto-Start After Reboot
All containers are set to `restart: unless-stopped`, which means they automatically come back once Docker is running. You just need to make>
**Linux (dedicated server or VM):**
Run this once and you're done:
```bash
sudo systemctl enable docker
```
**Windows (running Docker inside WSL):**
WSL (Windows Subsystem for Linux) doesn't start Docker automatically when your PC boots. Here's how to fix that:
**Step 1:** Open your WSL terminal and run this command to edit the WSL config file:
```bash
sudo nano /etc/wsl.conf
```
**Step 2:** Your file might already have some lines in it (like `[boot]` or `[user]`). Look for a `[boot]` section. If it exists, add the `>
```ini
[boot]
command=service docker start
```
**Step 3:** Save the file by pressing `Ctrl+X`, then `Y`, then `Enter`.
**Step 4 (optional):** By default, WSL only starts when you open a terminal. If you want it to start automatically when Windows boots (so y>
1. Press `Win+R` on your keyboard
2. Type `shell:startup` and press Enter — this opens your Windows Startup folder
3. Right-click in the folder → New → Text Document
4. Name it `wsl.vbs` (make sure it ends in `.vbs`, not `.vbs.txt` — if you can't see file extensions, go to View → Show → File name extensi>
5. Right-click the file → Edit (or Open with Notepad) and paste this:
```vbs
Set ws = CreateObject("Wscript.Shell")
ws.Run "wsl -d Ubuntu -u root -- service docker start", 0
```
6. Save and close
That's it — next time your PC restarts, WSL starts Docker automatically and all your containers come back up on their own. No commands need>
## Updating
```bash
cd arrstack
docker compose pull
docker compose up -d
```
## Remote Access with Tailscale (Optional)
Want to access Jellyfin, Seerr, or any service from outside your home? [Tailscale](https://tailscale.com/) creates a private network betwee>
**On your server:**
```bash
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
```
**On your phone/laptop/TV:**
1. Install Tailscale from your app store
2. Sign in with the same account
**Access your services from anywhere:**
```
http://YOUR-TAILSCALE-IP:8096 ← Jellyfin
http://YOUR-TAILSCALE-IP:5055 ← Seerr
http://YOUR-TAILSCALE-IP:7878 ← Radarr
http://YOUR-TAILSCALE-IP:8989 ← Sonarr
```
Find your Tailscale IP with `tailscale ip -4` on the server.
Tailscale is free for personal use (up to 100 devices). Everything is encrypted with WireGuard — nobody can see your traffic, not even Tail>
> **Do NOT expose Jellyfin directly to the internet** (no port forwarding on your router). Use Tailscale or a reverse proxy instead. Direct>
### Sharing with Family and Friends
Your family and friends only need two things — **Seerr** to request movies/shows and **Jellyfin** to watch them. They never see Radarr, Son>
**Step 1: Invite them to your Tailscale network**
1. Go to the [Tailscale admin console](https://login.tailscale.com/admin/machines)
2. Click **Share** on your server's machine
3. Enter their email — they'll get an invite link
**Step 2: They install Tailscale**
1. Download Tailscale on their phone, laptop, or TV from [tailscale.com/download](https://tailscale.com/download)
2. Accept your invite and sign in
**Step 3: They access your services**
Give them these two links (replace with your Tailscale IP):
```
http://YOUR-TAILSCALE-IP:5055 ← Seerr (request movies and shows)
http://YOUR-TAILSCALE-IP:8096 ← Jellyfin (watch everything)
```
**For TVs and phones**, they can install the **Jellyfin app** (available on Roku, Fire TV, Apple TV, Android TV, iOS, Android) and enter yo>
That's it — they request, you automatically download, they watch. No technical knowledge needed on their end.
## Troubleshooting
**Gluetun unhealthy / won't connect:**
- Double-check VPN credentials in `.env` — these are NOT your login email/password
- Try removing the gluetun folder and restarting: `rm -rf gluetun && docker compose up -d`
- Check logs: `docker logs gluetun`
**qBittorrent can't connect:**
- Make sure Gluetun is healthy: `docker ps` (should show "healthy")
- Check qBit is using VPN: `docker exec gluetun wget -qO- ifconfig.me`
- In qBittorrent settings → Advanced → set Network Interface to `tun0`
**Movie or show not downloading:**
- **Quality profile too strict** — If Radarr/Sonarr can't find a release matching your quality profile, it won't download anything. Go to t>
- **Not enough indexers** — Public indexers have limited catalogs. If you only have one or two indexers in Prowlarr, add more (1337x, The P>
- **Not enough seeders** — Some torrents just don't have anyone sharing them, especially older or niche content. Check qBittorrent — if the>
- **Indexer blocked by Cloudflare** — See the Prowlarr setup note above about setting up FlareSolverr with tags.
**Hard links not working (files copying instead):**
- Both `/data/torrents` and `/data/media` must be on the same filesystem
- Check Radarr/Sonarr → Settings → Media Management → "Use Hardlinks" is checked
- Verify with: `ls -i /data/torrents/movies/yourfile` and `ls -i /data/media/movies/YourMovie/yourfile` — inode numbers should match
**Permission errors:**
- Run `id` and make sure PUID/PGID in `.env` match your user
- Re-run: `sudo chown -R $(id -u):$(id -g) /data`
**Service won't start — "port already in use":**
Another program on your system might be using the same port. This is common with port 5055 (Seerr) but can happen with any service.
1. Find what's using the port (replace `5055` with the port number from the error):
**Linux:**
```bash
sudo ss -tlnp | grep 5055
```
**Windows (WSL users) — run in PowerShell:**
```powershell
netstat -ano | findstr :5055
```
This gives you a PID (process ID). Find the program name:
```powershell
Get-Process -Id <PID> | Select-Object ProcessName, Id, Path
```
2. Either stop/disable that program, or change the port in `docker-compose.yml` to an unused one (e.g. `5056:5055`).
3. If it's a Windows service hogging the port, disable it in an admin PowerShell:
```powershell
Stop-Service <ServiceName> -Force
Set-Service <ServiceName> -StartupType Disabled
```
4. Then recreate the container:
```bash
docker compose up -d --force-recreate <service-name>
```
**Seerr stuck restarting / crash-looping:**
The current `docker-compose.yml` uses a named volume (`seerr_config`) for Seerr's config, which fixes both causes of the crash loop. If you>
Root cause (for reference):
- **Linux/macOS:** Seerr runs as the `node` user (UID 1000). A bind-mounted `./seerr` folder is created root-owned on first `docker compose>
- **Windows/WSL:** Bind mounts go through an SMB share inside Docker Desktop's VM, which doesn't support file locking. Seerr's SQLite DB co>
Named volumes sidestep both problems because Docker creates them with correct ownership inside its own managed storage.
If you still see it after pulling:
- Check logs: `docker logs seerr | tail -30`
- Nuke the volume and start fresh (you'll lose Seerr settings, not media):
```bash
docker compose down seerr && docker volume rm arrstack_seerr_config && docker compose up -d seerr
```
**Migrating Seerr config to a named volume (existing installs):**
If you had Seerr working on an older version of this repo with a bind-mounted `./jellyseerr` or `./seerr` folder and want to keep your sett>
```bash
docker compose down seerr
# pick whichever folder you actually have
SRC=./seerr
[ -d ./jellyseerr ] && SRC=./jellyseerr
docker volume create arrstack_seerr_config
docker run --rm -v "$(pwd)/${SRC#./}":/src -v arrstack_seerr_config:/dest alpine sh -c "cp -a /src/. /dest/ && chown -R 1000:1000 /dest"
docker compose up -d seerr
```
If you don't care about preserving settings, just `docker compose up -d seerr` — Seerr will start fresh and walk you through setup again.
**Can't log into qBittorrent:**
- qBittorrent generates a temporary password every time it starts. Get it with:
```bash
docker logs qbittorrent 2>&1 | grep "temporary password"
```
- Default username is `admin`. Once logged in, go to Tools → Options → Web UI and set a permanent password.
**Services can't connect to each other (connection refused, timeout):**
- Don't use `localhost` when connecting services together — that won't work across Docker containers.
- Use the internal Docker IPs instead:
- qBittorrent/Prowlarr/FlareSolverr: `172.39.0.2`
- Radarr: `172.39.0.3`
- Sonarr: `172.39.0.4`
- Jellyfin: `172.39.0.7`
- The one exception: Prowlarr → FlareSolverr can use `localhost:8191` because they both run through Gluetun and share the same network.
**"Root folder does not exist" in Radarr/Sonarr:**
- Make sure you ran `sudo bash setup-folders.sh` to create the `/data` directory structure.
- Double-check the root folder path — it should be `/data/media/movies` for Radarr and `/data/media/tv` for Sonarr (not `/movies` or `/data>
**Downloads stuck at "importing" or "waiting to import":**
- This is almost always a permissions issue. Fix it with:
```bash
sudo chown -R $(id -u):$(id -g) /data
sudo chmod -R 775 /data
```
- Make sure PUID/PGID in your `.env` match your user (check with `id`).
**Jellyfin library is empty after downloads finish:**
- Make sure your Jellyfin libraries point to the correct paths: `/data/media/movies`, `/data/media/tv`, `/data/media/music`
- Jellyfin doesn't scan instantly. Go to Dashboard → Libraries → click the `...` menu → **Scan Library** to force a refresh.
- You can also set up scheduled scans in Dashboard → Scheduled Tasks.
**Subtitles not downloading (Bazarr):**
- Bazarr needs to be connected to Radarr and Sonarr: Settings → Radarr / Sonarr → enter the IP (`172.39.0.3` / `172.39.0.4`) and API key.
- You also need at least one subtitle provider: Settings → Providers → Add → **OpenSubtitles.com** is the most popular (free account requir>
**Everything works but downloads are slow:**
- Your VPN server might be too far away. Change `SERVER_COUNTRIES` in your `.env` to a country closer to you, then restart:
```bash
docker compose down && docker compose up -d
```
- Check your VPN speed: `docker exec gluetun wget -qO- https://speed.cloudflare.com/__down?measId=10000000 > /dev/null` — if it's very slow>
**Disk space filling up:**
- By default, qBittorrent keeps torrents after Radarr/Sonarr imports them. To auto-clean:
- In Radarr/Sonarr → Settings → Download Clients → click on qBittorrent → enable **Remove Completed**
- This deletes the torrent from qBittorrent after the file has been imported (the hard link in your media folder is kept, so you don't lo>
**VPN IP leak — want to make sure your real IP isn't exposed:**
```bash
# Check the VPN container's IP (should NOT be your real IP)
docker exec gluetun wget -qO- ifconfig.me
# Compare with your real IP (run this outside Docker)
curl -s ifconfig.me
```
If both IPs are the same, your VPN isn't working — check Gluetun logs with `docker logs gluetun`.
## Credits
Built by [Tom Spark](https://youtube.com/@tomspark) following [Trash Guides](https://trash-guides.info/) and [Servarr Wiki](https://wiki.se>
Uses [Gluetun](https://github.com/qdm12/gluetun) for VPN, [LinuxServer.io](https://linuxserver.io) container images, and [Seerr](https://gi>
@@ -0,0 +1,66 @@
```
#!/bin/bash
# ============================================================
# Tom Spark's ARR Stack — Folder Structure Setup
# https://github.com/loponai/arrstack
#
# Creates the /data directory structure required for hard links
# to work correctly. Run this ONCE before starting the stack.
#
# Usage: sudo bash setup-folders.sh
# ============================================================
set -e
DATA_DIR="/data"
echo ""
echo "=== Tom Spark's ARR Stack — Folder Setup ==="
echo ""
echo "This will create the following structure:"
echo ""
echo " /data/"
echo " ├── torrents/"
echo " │ ├── movies/"
echo " │ ├── tv/"
echo " │ └── music/"
echo " └── media/"
echo " ├── movies/"
echo " ├── tv/"
echo " └── music/"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "ERROR: This script needs sudo to create /data and set permissions."
echo "Run: sudo bash setup-folders.sh"
exit 1
fi
# Get the real user (not root) for ownership
REAL_USER=${SUDO_USER:-$USER}
REAL_UID=$(id -u "$REAL_USER")
REAL_GID=$(id -g "$REAL_USER")
echo "Creating folders..."
mkdir -p "$DATA_DIR"/{torrents/{movies,tv,music},media/{movies,tv,music}}
echo "Setting ownership to $REAL_USER ($REAL_UID:$REAL_GID)..."
chown -R "$REAL_UID":"$REAL_GID" "$DATA_DIR"
echo "Setting permissions..."
chmod -R 775 "$DATA_DIR"
echo ""
echo "Done! Folder structure:"
if command -v tree &> /dev/null; then
tree "$DATA_DIR"
else
find "$DATA_DIR" -type d | head -20
fi
echo ""
echo "Your PUID=$REAL_UID and PGID=$REAL_GID"
echo "Make sure these match your .env file."
echo ""
```
@@ -0,0 +1,328 @@
```
#!/bin/bash
# ============================================================
# Tom Spark's ARR Stack — Health Check & Troubleshooting
# https://github.com/loponai/arrstack
#
# Run this after 'docker compose up -d' to verify everything
# is working correctly. It checks each service, tests VPN
# connectivity, and provides specific fixes for any issues.
#
# Usage: bash test-stack.sh
# ============================================================
set -o pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
PASS="${GREEN}✓ PASS${NC}"
FAIL="${RED}✗ FAIL${NC}"
WARN="${YELLOW}! WARN${NC}"
TOTAL_PASS=0
TOTAL_FAIL=0
TOTAL_WARN=0
pass() { echo -e " ${PASS} $1"; ((TOTAL_PASS++)); }
fail() { echo -e " ${FAIL} $1"; ((TOTAL_FAIL++)); }
warn() { echo -e " ${WARN} $1"; ((TOTAL_WARN++)); }
header() { echo -e "\n${CYAN}${BOLD}[$1]${NC}"; }
fix() { echo -e " ${YELLOW}Fix: $1${NC}"; }
echo ""
echo "========================================="
echo " Tom Spark's ARR Stack — Health Check"
echo "========================================="
echo ""
# ============================================================
# TEST 1: Docker running?
# ============================================================
header "Docker"
if docker info > /dev/null 2>&1; then
pass "Docker is running"
else
fail "Docker is not running"
fix "Start Docker: sudo systemctl start docker"
fix "Or install: curl -fsSL https://get.docker.com | sh"
echo ""
echo "Cannot continue without Docker. Exiting."
exit 1
fi
# ============================================================
# TEST 2: .env file exists and has VPN credentials?
# ============================================================
header "Configuration"
if [ -f .env ]; then
pass ".env file exists"
else
fail ".env file not found"
fix "Run: cp .env.example .env && nano .env"
fix "Then fill in your VPN credentials"
fi
if [ -f .env ]; then
VPN_KEY=$(grep -E "^WIREGUARD_PRIVATE_KEY=" .env 2>/dev/null | cut -d= -f2)
VPN_PROVIDER=$(grep -E "^VPN_SERVICE_PROVIDER=" .env 2>/dev/null | cut -d= -f2)
if [ -n "$VPN_KEY" ] && [ "$VPN_KEY" != "" ]; then
pass "VPN private key is set (provider: $VPN_PROVIDER)"
else
fail "VPN private key is empty"
fix "Edit .env and paste your WireGuard private key"
fix "Get it from your VPN provider's manual setup page"
fi
fi
# ============================================================
# TEST 3: Folder structure exists?
# ============================================================
header "Folder Structure"
ALL_FOLDERS_OK=true
for dir in /data/torrents/movies /data/torrents/tv /data/torrents/music /data/media/movies /data/media/tv /data/media/music; do
if [ -d "$dir" ]; then
pass "$dir exists"
else
fail "$dir missing"
ALL_FOLDERS_OK=false
fi
done
if [ "$ALL_FOLDERS_OK" = false ]; then
fix "Run: sudo bash setup-folders.sh"
fi
# Check permissions
if [ -d /data ]; then
OWNER=$(stat -c '%u' /data 2>/dev/null)
ENV_PUID=$(grep -E "^PUID=" .env 2>/dev/null | cut -d= -f2)
if [ "$OWNER" = "$ENV_PUID" ] || [ "$OWNER" = "$(id -u)" ]; then
pass "/data ownership matches PUID ($OWNER)"
else
warn "/data owned by $OWNER but PUID is ${ENV_PUID:-1000}"
fix "Run: sudo chown -R ${ENV_PUID:-1000}:${ENV_PUID:-1000} /data"
fi
fi
# ============================================================
# TEST 4: Container status
# ============================================================
header "Containers"
EXPECTED_SERVICES="gluetun qbittorrent deunhealth prowlarr flaresolverr radarr sonarr lidarr bazarr jellyfin seerr"
for svc in $EXPECTED_SERVICES; do
STATUS=$(docker inspect --format '{{.State.Status}}' "$svc" 2>/dev/null)
HEALTH=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$svc" 2>/dev/null)
if [ -z "$STATUS" ]; then
fail "$svc — not found (not created)"
fix "Run: docker compose up -d"
elif [ "$STATUS" = "running" ]; then
if [ "$HEALTH" = "healthy" ]; then
pass "$svc — running (healthy)"
elif [ "$HEALTH" = "unhealthy" ]; then
fail "$svc — running but UNHEALTHY"
if [ "$svc" = "gluetun" ]; then
fix "VPN probably can't connect. Check credentials in .env"
fix "Check logs: docker logs gluetun | tail -20"
fix "Try: rm -rf gluetun && docker compose up -d gluetun"
elif [ "$svc" = "qbittorrent" ]; then
fix "Usually means VPN dropped. Deunhealth should auto-restart it."
fix "Check: docker logs qbittorrent | tail -20"
fi
elif [ "$HEALTH" = "starting" ]; then
warn "$svc — running (health check starting, wait 30s and rerun)"
else
pass "$svc — running"
fi
elif [ "$STATUS" = "created" ]; then
warn "$svc — created but not started"
if [ "$svc" = "qbittorrent" ] || [ "$svc" = "prowlarr" ] || [ "$svc" = "flaresolverr" ]; then
fix "Waiting for Gluetun to be healthy. Check Gluetun status first."
fix "If Gluetun is healthy, try: docker compose up -d $svc"
elif [ "$svc" = "seerr" ]; then
fix "Port 5055 may be in use. Check: ss -tlnp | grep 5055"
fix "Or change the port in docker-compose.yml"
else
fix "Try: docker compose up -d $svc"
fi
elif [ "$STATUS" = "restarting" ]; then
fail "$svc — crash-looping (restarting)"
fix "Check logs: docker logs $svc | tail -30"
if [ "$svc" = "seerr" ]; then
fix "Seerr may have a corrupt config. Try: docker compose down seerr && rm -rf seerr && docker compose up -d seerr"
fix "WSL/Windows users: if it keeps crashing, try a named volume instead of a bind mount"
else
fix "Try: docker compose down $svc && docker compose up -d $svc"
fi
elif [ "$STATUS" = "exited" ]; then
fail "$svc — exited (crashed)"
fix "Check logs: docker logs $svc | tail -30"
fix "Try restarting: docker compose up -d $svc"
else
warn "$svc — status: $STATUS"
fi
done
# ============================================================
# TEST 5: VPN connectivity
# ============================================================
header "VPN Connection"
GLUETUN_STATUS=$(docker inspect --format '{{.State.Status}}' gluetun 2>/dev/null)
GLUETUN_HEALTH=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{end}}' gluetun 2>/dev/null)
if [ "$GLUETUN_STATUS" = "running" ] && [ "$GLUETUN_HEALTH" = "healthy" ]; then
# Get VPN IP
VPN_IP=$(docker exec gluetun wget -qO- --timeout=10 ipinfo.io/ip 2>/dev/null)
if [ -n "$VPN_IP" ]; then
pass "Gluetun VPN IP: $VPN_IP"
# Get VPN location
VPN_LOCATION=$(docker exec gluetun wget -qO- --timeout=10 "ipinfo.io/${VPN_IP}/city" 2>/dev/null)
VPN_COUNTRY=$(docker exec gluetun wget -qO- --timeout=10 "ipinfo.io/${VPN_IP}/country" 2>/dev/null)
if [ -n "$VPN_LOCATION" ]; then
pass "VPN location: $VPN_LOCATION, $VPN_COUNTRY"
fi
else
fail "Gluetun is healthy but can't reach the internet"
fix "Check logs: docker logs gluetun | tail -20"
fi
# Check if qBittorrent is tunneled
QBIT_STATUS=$(docker inspect --format '{{.State.Status}}' qbittorrent 2>/dev/null)
if [ "$QBIT_STATUS" = "running" ]; then
QBIT_IP=$(docker exec qbittorrent wget -qO- --timeout=10 ipinfo.io/ip 2>/dev/null)
if [ "$QBIT_IP" = "$VPN_IP" ]; then
pass "qBittorrent tunneled through VPN ($QBIT_IP)"
elif [ -n "$QBIT_IP" ]; then
fail "qBittorrent IP ($QBIT_IP) doesn't match VPN IP ($VPN_IP)!"
fix "This should not happen. Check network_mode in docker-compose.yml"
else
warn "Could not check qBittorrent IP (container may still be starting)"
fi
fi
# Check if Prowlarr is tunneled
PROWLARR_STATUS=$(docker inspect --format '{{.State.Status}}' prowlarr 2>/dev/null)
if [ "$PROWLARR_STATUS" = "running" ]; then
PROWLARR_IP=$(docker exec prowlarr wget -qO- --timeout=10 ipinfo.io/ip 2>/dev/null)
if [ "$PROWLARR_IP" = "$VPN_IP" ]; then
pass "Prowlarr tunneled through VPN ($PROWLARR_IP)"
elif [ -n "$PROWLARR_IP" ]; then
fail "Prowlarr IP ($PROWLARR_IP) doesn't match VPN IP ($VPN_IP)!"
fi
fi
# Verify your real IP is different
REAL_IP=$(wget -qO- --timeout=10 ipinfo.io/ip 2>/dev/null)
if [ -n "$REAL_IP" ] && [ "$REAL_IP" != "$VPN_IP" ]; then
pass "Real IP ($REAL_IP) differs from VPN IP — VPN is working!"
elif [ "$REAL_IP" = "$VPN_IP" ]; then
warn "Real IP matches VPN IP — are you already running a system-wide VPN?"
fi
else
if [ "$GLUETUN_HEALTH" = "unhealthy" ]; then
fail "Gluetun is unhealthy — VPN not connected"
fix "Check credentials in .env (these are NOT your VPN login email/password)"
fix "Check logs: docker logs gluetun 2>&1 | tail -30"
fix "Try resetting: docker compose down && rm -rf gluetun && docker compose up -d"
elif [ "$GLUETUN_HEALTH" = "starting" ]; then
warn "Gluetun health check still starting — wait 30-60 seconds and rerun"
else
warn "Gluetun not running — can't test VPN"
fix "Run: docker compose up -d"
fi
fi
# ============================================================
# TEST 6: Service web UI accessibility
# ============================================================
header "Web UI Access"
check_http() {
local name=$1 port=$2
local code=$(curl -sL -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost:$port" 2>/dev/null)
if [ "$code" = "200" ] || [ "$code" = "302" ] || [ "$code" = "301" ] || [ "$code" = "307" ]; then
pass "$name — http://localhost:$port (HTTP $code)"
elif [ "$code" = "000" ]; then
# Container might be behind gluetun, check if it's running
local status=$(docker inspect --format '{{.State.Status}}' "$name" 2>/dev/null)
if [ "$status" = "running" ]; then
warn "$name — container running but port $port not reachable from host"
fix "Port may be mapped on Gluetun. Try: http://localhost:$port"
else
fail "$name — not reachable (container not running)"
fi
else
warn "$name — http://localhost:$port returned HTTP $code"
fi
}
check_http qbittorrent 8080
check_http prowlarr 9696
check_http radarr 7878
check_http sonarr 8989
check_http lidarr 8686
check_http bazarr 6767
check_http jellyfin 8096
check_http seerr 5055
# ============================================================
# TEST 7: Hard link capability
# ============================================================
header "Hard Links"
if [ -d /data/torrents ] && [ -d /data/media ]; then
# Check if same filesystem
FS_TORRENTS=$(df /data/torrents --output=source 2>/dev/null | tail -1)
FS_MEDIA=$(df /data/media --output=source 2>/dev/null | tail -1)
if [ "$FS_TORRENTS" = "$FS_MEDIA" ]; then
pass "torrents/ and media/ are on the same filesystem ($FS_TORRENTS)"
pass "Hard links will work correctly"
else
fail "torrents/ ($FS_TORRENTS) and media/ ($FS_MEDIA) are on DIFFERENT filesystems!"
fix "Hard links only work on the same filesystem/drive"
fix "Move both directories to the same drive"
fi
# Quick hard link test
TEST_FILE="/data/torrents/.hardlink_test_$$"
TEST_LINK="/data/media/.hardlink_test_$$"
if touch "$TEST_FILE" 2>/dev/null && ln "$TEST_FILE" "$TEST_LINK" 2>/dev/null; then
pass "Hard link test succeeded"
rm -f "$TEST_FILE" "$TEST_LINK" 2>/dev/null
elif [ -f "$TEST_FILE" ]; then
fail "Hard link test failed — filesystem may not support hard links"
fix "Check filesystem type: df -T /data"
fix "Hard links work on ext4, btrfs, xfs. NOT on exFAT or ntfs-3g"
rm -f "$TEST_FILE" 2>/dev/null
else
warn "Could not write to /data/torrents (permission issue?)"
fix "Run: sudo chown -R $(id -u):$(id -g) /data"
fi
else
warn "Folder structure not found — skipping hard link test"
fix "Run: sudo bash setup-folders.sh"
fi
# ============================================================
# SUMMARY
# ============================================================
echo ""
echo "========================================="
echo -e " ${GREEN}Passed: $TOTAL_PASS${NC} ${RED}Failed: $TOTAL_FAIL${NC} ${YELLOW}Warnings: $TOTAL_WARN${NC}"
echo "========================================="
if [ $TOTAL_FAIL -eq 0 ] && [ $TOTAL_WARN -eq 0 ]; then
echo -e "\n ${GREEN}${BOLD}All checks passed! Your stack is ready to go.${NC}\n"
elif [ $TOTAL_FAIL -eq 0 ]; then
echo -e "\n ${YELLOW}${BOLD}No failures, but check the warnings above.${NC}\n"
else
echo -e "\n ${RED}${BOLD}Some checks failed. Follow the fix instructions above.${NC}"
echo -e " ${BOLD}If stuck, check: docker logs <container-name>${NC}\n"
fi
```
@@ -0,0 +1,546 @@
---
title: "loponai/arrstack: One-command media server (Sonarr/Radarr/Prowlarr/qBittorrent behind a VPN). Now maintained as SparkBox → tomsparkbox.com"
source: https://github.com/loponai/arrstack
author:
published:
created: 2026-07-09
description: One-command media server (Sonarr/Radarr/Prowlarr/qBittorrent behind a VPN). Now maintained as SparkBox → tomsparkbox.com - loponai/arrstack
tags:
- clippings
- Media
---
> ## ⚡ Want the easy, full version? → SparkBox
>
> This is one of my original one-shot scripts. It still works — but these days everything I build goes into **[SparkBox](https://tomsparkbox.com/)**: a free, self-hosted home server that sets up this whole stack (and a lot more — photos, files, password manager, ad-blocking) with **one command**, a real web dashboard, automatic updates with one-click rollback, and a built-in AI assistant that troubleshoots and fixes things for you. No hand-editing config files.
>
> ### 👉 Get it free at tomsparkbox.com
>
> *Built by [Tom Spark Reviews](https://youtube.com/@TomSparkReviews).*
---
## Tom Spark's ARR Stack
One-command automated media server with VPN protection. Sonarr, Radarr, Prowlarr, qBittorrent, Gluetun, Jellyfin, and more.
**Full video tutorial:** \[YouTube Link Coming Soon\]
## What You Get
| Service | Port | Purpose |
| --- | --- | --- |
| Gluetun | — | VPN tunnel with kill switch |
| qBittorrent | 8080 | Torrent client (VPN protected) |
| Prowlarr | 9696 | Indexer manager (VPN protected) |
| FlareSolverr | 8191 | Cloudflare bypass (VPN protected) |
| Radarr | 7878 | Movie automation |
| Sonarr | 8989 | TV show automation |
| Lidarr | 8686 | Music automation |
| Bazarr | 6767 | Subtitle automation |
| Jellyfin | 8096 | Media server / streaming |
| Seerr | 5055 | Netflix-like request UI (Overseerr/Jellyseerr successor) |
All download traffic routes through Gluetun's VPN tunnel. If the VPN drops, all traffic stops — zero leaks. The deunhealth container auto-restarts services that become unhealthy.
## Quick Start
### 1\. Install Docker
```
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in for group change to take effect
```
### 2\. Clone this repo
```
git clone https://github.com/loponai/arrstack.git
cd arrstack
```
### 3\. Create folder structure
```
sudo bash setup-folders.sh
```
This creates:
```
/data/
├── torrents/ ← qBittorrent downloads here
│ ├── movies/
│ ├── tv/
│ └── music/
└── media/ ← Radarr/Sonarr organize files here (Jellyfin reads from here)
├── movies/
├── tv/
└── music/
```
> **Hard links:** Both folders MUST be on the same drive/filesystem. Radarr and Sonarr create hard links (not copies) — the file appears in both locations but only uses disk space once.
### 4\. Configure your VPN
```
cp .env.example .env
nano .env
```
Fill in your VPN provider and credentials. See [VPN Setup Guides](#vpn-setup-guides) below.
### 5\. Launch
```
docker compose up -d
```
### 6\. Verify everything is working
```
bash test-stack.sh
```
This runs a full health check — Docker status, VPN connection, IP leak test, service accessibility, hard link support, and folder permissions. If anything is wrong, it tells you exactly what to fix.
You can also check manually:
```
# Check Gluetun's IP (should be VPN, not your real IP)
docker exec gluetun wget -qO- ifconfig.me
# qBittorrent shares Gluetun's network, so the above proves both are tunneled.
docker exec qbittorrent wget -qO- ifconfig.me
# Check health status of all containers
docker ps --format "table {{.Names}}\t{{.Status}}"
```
### 7\. Configure services
Open each service in your browser at `http://YOUR-SERVER-IP:PORT` and follow the video tutorial for step-by-step configuration.
**Quick reference:**
- **qBittorrent** (`:8080`) — Get temp password: `docker logs qbittorrent 2>&1 | grep "temporary password"`
- **Prowlarr** (`:9696`) — Add indexers, connect to Radarr/Sonarr. If an indexer is blocked by Cloudflare, set up FlareSolverr as a proxy: Settings → Indexers → Add Proxy → FlareSolverr → host `http://localhost:8191` → give it a tag (e.g. `flaresolverr`). Then edit the blocked indexer and add the **same tag** so Prowlarr routes it through FlareSolverr.
- **Radarr** (`:7878`) — Root folder: `/data/media/movies`, download client category: `movies`
- **Sonarr** (`:8989`) — Root folder: `/data/media/tv`, download client category: `tv`
- **Jellyfin** (`:8096`) — Add libraries: `/data/media/movies`, `/data/media/tv`, `/data/media/music`. To watch, open `http://YOUR-SERVER-IP:8096` in a browser or use the Jellyfin app (available on Roku, Fire TV, Apple TV, Android TV, iOS, Android). Find your server IP by running `hostname -I` in the terminal. If watching remotely with Tailscale, use your Tailscale IP instead.
- **Seerr** (`:5055`) — Connect to Jellyfin, Radarr, and Sonarr. Seerr is the unified successor to Overseerr/Jellyseerr. If you previously ran Jellyseerr here, your existing config is migrated automatically on first start.
**Internal Docker IPs — use these when connecting services to each other (NOT localhost):**
| IP | Service |
| --- | --- |
| `172.39.0.2` | Gluetun (also qBittorrent, Prowlarr, FlareSolverr) |
| `172.39.0.3` | Radarr |
| `172.39.0.4` | Sonarr |
| `172.39.0.5` | Lidarr |
| `172.39.0.6` | Bazarr |
| `172.39.0.7` | Jellyfin |
| `172.39.0.8` | Seerr |
These IPs are the same for everyone — they're hardcoded in the docker-compose file.
**Common connections:**
- Radarr/Sonarr → Download Client → qBittorrent: host `172.39.0.2`, port `8080`
- Prowlarr → Apps → Radarr: server `http://172.39.0.3:7878`
- Prowlarr → Apps → Sonarr: server `http://172.39.0.4:8989`
- Prowlarr → Apps → Prowlarr Server: `http://172.39.0.2:9696`
- Seerr → Radarr: host `172.39.0.3`, port `7878`
- Seerr → Sonarr: host `172.39.0.4`, port `8989`
- Seerr → Jellyfin: host `172.39.0.7`, port `8096`
**Important Radarr/Sonarr settings:**
- Media Management → Show Advanced → **Use Hardlinks instead of Copy** → must be ON
- Media Management → **Rename Movies/Episodes** → recommended ON
**Recommended quality profile (1080p baseline, 4K preferred):**
Go to Settings → Profiles and edit or create a profile:
1. Uncheck everything below 1080p (720p, 480p, etc.)
2. Check/enable everything from **HDTV-1080p** up through **Bluray-2160p**
3. Set **Cutoff** to `Bluray-1080p` — this is the minimum quality Radarr/Sonarr will be happy with
4. Set **Upgrade Until** to `Bluray-2160p` — it will automatically upgrade to 4K if one becomes available
This means it grabs a 1080p release right away so you can start watching, then silently upgrades to 4K later if it finds one.
## VPN Setup Guides
### Surfshark (Recommended)
The best value for torrenting — cheapest long-term plans, fast WireGuard speeds, and easy setup with Gluetun. [Get Surfshark](https://get.surfshark.net/aff_c?offer_id=1126&aff_id=9447&aff_sub=8amjxr)
1. Go to [Surfshark Manual Setup](https://my.surfshark.com/vpn/manual-setup/main)
2. Select **WireGuard** and get your credentials (private key + address)
3. In your `.env`:
```
VPN_SERVICE_PROVIDER=surfshark
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=your_key_here
WIREGUARD_ADDRESSES=10.14.0.2/16
SERVER_COUNTRIES=United States
```
> The `.env.example` file is pre-configured for Surfshark. Just paste your private key and you're good to go.
### NordVPN
1. Go to [NordVPN Manual Setup](https://my.nordaccount.com/dashboard/nordvpn/manual-configuration/)
2. Select **NordLynx** (WireGuard) and generate a private key
3. In your `.env`:
```
VPN_SERVICE_PROVIDER=nordvpn
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=your_key_here
WIREGUARD_ADDRESSES=10.5.0.2/16
SERVER_COUNTRIES=United States
```
### ProtonVPN
1. Go to [ProtonVPN WireGuard Config](https://account.protonvpn.com/) → Downloads → WireGuard
2. Generate a config file, open it, copy `PrivateKey` and `Address`
3. In your `.env`:
```
VPN_SERVICE_PROVIDER=protonvpn
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=your_key_here
WIREGUARD_ADDRESSES=10.2.0.2/32
SERVER_COUNTRIES=United States
VPN_PORT_FORWARDING=on
```
### AirVPN
1. Go to [AirVPN Config Generator](https://airvpn.org/) → Client Area → Config Generator
2. Select Linux → WireGuard → pick server → Generate
3. In your `.env`:
```
VPN_SERVICE_PROVIDER=airvpn
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=your_key_here
WIREGUARD_PUBLIC_KEY=server_public_key
WIREGUARD_PRESHARED_KEY=your_preshared_key
WIREGUARD_ADDRESSES=your_ip/32
FIREWALL_VPN_INPUT_PORTS=your_port
VPN_PORT_FORWARDING=on
```
### Other Providers
Gluetun supports 30+ providers. Check the [full provider list](https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers).
## Starting and Stopping
**Start the stack:**
```
cd arrstack
docker compose up -d
```
**Stop the stack:**
```
cd arrstack
docker compose down
```
**Check status:**
```
docker ps --format "table {{.Names}}\t{{.Status}}"
```
### Auto-Start After Reboot
All containers are set to `restart: unless-stopped`, which means they automatically come back once Docker is running. You just need to make sure Docker itself starts when your computer boots up.
**Linux (dedicated server or VM):**
Run this once and you're done:
```
sudo systemctl enable docker
```
**Windows (running Docker inside WSL):**
WSL (Windows Subsystem for Linux) doesn't start Docker automatically when your PC boots. Here's how to fix that:
**Step 1:** Open your WSL terminal and run this command to edit the WSL config file:
```
sudo nano /etc/wsl.conf
```
**Step 2:** Your file might already have some lines in it (like `[boot]` or `[user]`). Look for a `[boot]` section. If it exists, add the `command=` line under it. If it doesn't exist, add both lines. It should look like this when you're done:
```
[boot]
command=service docker start
```
**Step 3:** Save the file by pressing `Ctrl+X`, then `Y`, then `Enter`.
**Step 4 (optional):** By default, WSL only starts when you open a terminal. If you want it to start automatically when Windows boots (so your stack is always running), do this:
1. Press `Win+R` on your keyboard
2. Type `shell:startup` and press Enter — this opens your Windows Startup folder
3. Right-click in the folder → New → Text Document
4. Name it `wsl.vbs` (make sure it ends in `.vbs`, not `.vbs.txt` — if you can't see file extensions, go to View → Show → File name extensions in File Explorer)
5. Right-click the file → Edit (or Open with Notepad) and paste this:
```
Set ws = CreateObject("Wscript.Shell")
ws.Run "wsl -d Ubuntu -u root -- service docker start", 0
```
6. Save and close
That's it — next time your PC restarts, WSL starts Docker automatically and all your containers come back up on their own. No commands needed.
## Updating
```
cd arrstack
docker compose pull
docker compose up -d
```
## Remote Access with Tailscale (Optional)
Want to access Jellyfin, Seerr, or any service from outside your home? [Tailscale](https://tailscale.com/) creates a private network between your devices — no port forwarding, no exposing anything to the public internet.
**On your server:**
```
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
```
**On your phone/laptop/TV:**
1. Install Tailscale from your app store
2. Sign in with the same account
**Access your services from anywhere:**
```
http://YOUR-TAILSCALE-IP:8096 ← Jellyfin
http://YOUR-TAILSCALE-IP:5055 ← Seerr
http://YOUR-TAILSCALE-IP:7878 ← Radarr
http://YOUR-TAILSCALE-IP:8989 ← Sonarr
```
Find your Tailscale IP with `tailscale ip -4` on the server.
Tailscale is free for personal use (up to 100 devices). Everything is encrypted with WireGuard — nobody can see your traffic, not even Tailscale.
> **Do NOT expose Jellyfin directly to the internet** (no port forwarding on your router). Use Tailscale or a reverse proxy instead. Direct exposure is a security risk.
### Sharing with Family and Friends
Your family and friends only need two things — **Seerr** to request movies/shows and **Jellyfin** to watch them. They never see Radarr, Sonarr, qBittorrent, or any of the behind-the-scenes stuff.
**Step 1: Invite them to your Tailscale network**
1. Go to the [Tailscale admin console](https://login.tailscale.com/admin/machines)
2. Click **Share** on your server's machine
3. Enter their email — they'll get an invite link
**Step 2: They install Tailscale**
1. Download Tailscale on their phone, laptop, or TV from [tailscale.com/download](https://tailscale.com/download)
2. Accept your invite and sign in
**Step 3: They access your services**
Give them these two links (replace with your Tailscale IP):
```
http://YOUR-TAILSCALE-IP:5055 ← Seerr (request movies and shows)
http://YOUR-TAILSCALE-IP:8096 ← Jellyfin (watch everything)
```
**For TVs and phones**, they can install the **Jellyfin app** (available on Roku, Fire TV, Apple TV, Android TV, iOS, Android) and enter your Tailscale IP as the server address during setup.
That's it — they request, you automatically download, they watch. No technical knowledge needed on their end.
## Troubleshooting
**Gluetun unhealthy / won't connect:**
- Double-check VPN credentials in `.env` — these are NOT your login email/password
- Try removing the gluetun folder and restarting: `rm -rf gluetun && docker compose up -d`
- Check logs: `docker logs gluetun`
**qBittorrent can't connect:**
- Make sure Gluetun is healthy: `docker ps` (should show "healthy")
- Check qBit is using VPN: `docker exec gluetun wget -qO- ifconfig.me`
- In qBittorrent settings → Advanced → set Network Interface to `tun0`
**Movie or show not downloading:**
- **Quality profile too strict** — If Radarr/Sonarr can't find a release matching your quality profile, it won't download anything. Go to the movie/show → check if it says "No results found" or similar. Try lowering your cutoff temporarily (e.g. from Bluray-1080p to WEBDL-1080p) or enabling more quality tiers in your profile.
- **Not enough indexers** — Public indexers have limited catalogs. If you only have one or two indexers in Prowlarr, add more (1337x, The Pirate Bay, LimeTorrents, EZTV). The more indexers you have, the more results you'll get.
- **Not enough seeders** — Some torrents just don't have anyone sharing them, especially older or niche content. Check qBittorrent — if the torrent is stuck at 0% with 0 seeds, there's nothing to download. Try searching manually in Radarr/Sonarr for a different release with more seeders.
- **Indexer blocked by Cloudflare** — See the Prowlarr setup note above about setting up FlareSolverr with tags.
**Hard links not working (files copying instead):**
- Both `/data/torrents` and `/data/media` must be on the same filesystem
- Check Radarr/Sonarr → Settings → Media Management → "Use Hardlinks" is checked
- Verify with: `ls -i /data/torrents/movies/yourfile` and `ls -i /data/media/movies/YourMovie/yourfile` — inode numbers should match
**Permission errors:**
- Run `id` and make sure PUID/PGID in `.env` match your user
- Re-run: `sudo chown -R $(id -u):$(id -g) /data`
**Service won't start — "port already in use":**
Another program on your system might be using the same port. This is common with port 5055 (Seerr) but can happen with any service.
1. Find what's using the port (replace `5055` with the port number from the error):
**Linux:**
```
sudo ss -tlnp | grep 5055
```
**Windows (WSL users) — run in PowerShell:**
```
netstat -ano | findstr :5055
```
This gives you a PID (process ID). Find the program name:
```
Get-Process -Id <PID> | Select-Object ProcessName, Id, Path
```
2. Either stop/disable that program, or change the port in `docker-compose.yml` to an unused one (e.g. `5056:5055`).
3. If it's a Windows service hogging the port, disable it in an admin PowerShell:
```
Stop-Service <ServiceName> -Force
Set-Service <ServiceName> -StartupType Disabled
```
4. Then recreate the container:
```
docker compose up -d --force-recreate <service-name>
```
**Seerr stuck restarting / crash-looping:**
The current `docker-compose.yml` uses a named volume (`seerr_config`) for Seerr's config, which fixes both causes of the crash loop. If you're still hitting it, you're almost certainly on an older version of this repo that used a bind mount. `git pull` first.
Root cause (for reference):
- **Linux/macOS:** Seerr runs as the `node` user (UID 1000). A bind-mounted `./seerr` folder is created root-owned on first `docker compose up`, so the container can't write to `/app/config` and crash-loops. Upstream Seerr docs require `chown -R 1000:1000` on the config dir before first start.
- **Windows/WSL:** Bind mounts go through an SMB share inside Docker Desktop's VM, which doesn't support file locking. Seerr's SQLite DB corrupts on first write. Upstream Seerr docs explicitly say: **do not bind-mount `/app/config` on Windows** — use a named volume.
Named volumes sidestep both problems because Docker creates them with correct ownership inside its own managed storage.
If you still see it after pulling:
- Check logs: `docker logs seerr | tail -30`
- Nuke the volume and start fresh (you'll lose Seerr settings, not media):
```
docker compose down seerr && docker volume rm arrstack_seerr_config && docker compose up -d seerr
```
**Migrating Seerr config to a named volume (existing installs):**
If you had Seerr working on an older version of this repo with a bind-mounted `./jellyseerr` or `./seerr` folder and want to keep your settings, copy the data into the new named volume before starting:
```
docker compose down seerr
# pick whichever folder you actually have
SRC=./seerr
[ -d ./jellyseerr ] && SRC=./jellyseerr
docker volume create arrstack_seerr_config
docker run --rm -v "$(pwd)/${SRC#./}":/src -v arrstack_seerr_config:/dest alpine sh -c "cp -a /src/. /dest/ && chown -R 1000:1000 /dest"
docker compose up -d seerr
```
If you don't care about preserving settings, just `docker compose up -d seerr` — Seerr will start fresh and walk you through setup again.
**Can't log into qBittorrent:**
- qBittorrent generates a temporary password every time it starts. Get it with:
```
docker logs qbittorrent 2>&1 | grep "temporary password"
```
- Default username is `admin`. Once logged in, go to Tools → Options → Web UI and set a permanent password.
**Services can't connect to each other (connection refused, timeout):**
- Don't use `localhost` when connecting services together — that won't work across Docker containers.
- Use the internal Docker IPs instead:
- qBittorrent/Prowlarr/FlareSolverr: `172.39.0.2`
- Radarr: `172.39.0.3`
- Sonarr: `172.39.0.4`
- Jellyfin: `172.39.0.7`
- The one exception: Prowlarr → FlareSolverr can use `localhost:8191` because they both run through Gluetun and share the same network.
**"Root folder does not exist" in Radarr/Sonarr:**
- Make sure you ran `sudo bash setup-folders.sh` to create the `/data` directory structure.
- Double-check the root folder path — it should be `/data/media/movies` for Radarr and `/data/media/tv` for Sonarr (not `/movies` or `/data/movies`).
**Downloads stuck at "importing" or "waiting to import":**
- This is almost always a permissions issue. Fix it with:
```
sudo chown -R $(id -u):$(id -g) /data
sudo chmod -R 775 /data
```
- Make sure PUID/PGID in your `.env` match your user (check with `id`).
**Jellyfin library is empty after downloads finish:**
- Make sure your Jellyfin libraries point to the correct paths: `/data/media/movies`, `/data/media/tv`, `/data/media/music`
- Jellyfin doesn't scan instantly. Go to Dashboard → Libraries → click the `...` menu → **Scan Library** to force a refresh.
- You can also set up scheduled scans in Dashboard → Scheduled Tasks.
**Subtitles not downloading (Bazarr):**
- Bazarr needs to be connected to Radarr and Sonarr: Settings → Radarr / Sonarr → enter the IP (`172.39.0.3` / `172.39.0.4`) and API key.
- You also need at least one subtitle provider: Settings → Providers → Add → **OpenSubtitles.com** is the most popular (free account required).
**Everything works but downloads are slow:**
- Your VPN server might be too far away. Change `SERVER_COUNTRIES` in your `.env` to a country closer to you, then restart:
```
docker compose down && docker compose up -d
```
- Check your VPN speed: `docker exec gluetun wget -qO- https://speed.cloudflare.com/__down?measId=10000000 > /dev/null` — if it's very slow, try a different country.
**Disk space filling up:**
- By default, qBittorrent keeps torrents after Radarr/Sonarr imports them. To auto-clean:
- In Radarr/Sonarr → Settings → Download Clients → click on qBittorrent → enable **Remove Completed**
- This deletes the torrent from qBittorrent after the file has been imported (the hard link in your media folder is kept, so you don't lose anything).
**VPN IP leak — want to make sure your real IP isn't exposed:**
```
# Check the VPN container's IP (should NOT be your real IP)
docker exec gluetun wget -qO- ifconfig.me
# Compare with your real IP (run this outside Docker)
curl -s ifconfig.me
```
If both IPs are the same, your VPN isn't working — check Gluetun logs with `docker logs gluetun`.
## Credits
Built by [Tom Spark](https://youtube.com/@tomspark) following [Trash Guides](https://trash-guides.info/) and [Servarr Wiki](https://wiki.servarr.com/) best practices.
Uses [Gluetun](https://github.com/qdm12/gluetun) for VPN, [LinuxServer.io](https://linuxserver.io/) container images, and [Seerr](https://github.com/seerr-team/seerr) for the request system (the unified successor to Overseerr/Jellyseerr).
@@ -0,0 +1,135 @@
```
# ============================================================
# Tom Spark's ARR Stack — Environment Configuration
# https://github.com/loponai/arrstack
#
# INSTRUCTIONS:
# 1. Copy this file: cp .env.example .env
# 2. Fill in your VPN credentials below
# 3. Adjust timezone and user IDs if needed
# 4. Run: docker compose up -d
# ============================================================
# ============================================================
# SYSTEM SETTINGS
# ============================================================
# Your timezone (list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
TZ=America/New_York
# Your Linux user/group ID. Find yours with: id
# Most systems default to 1000. If yours is different, change these.
PUID=1000
PGID=1000
# ============================================================
# VPN SETTINGS — Pick your provider and fill in credentials
# Full provider list: https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers
#
# IMPORTANT: VPN credentials are NOT your login email/password!
# You need service credentials from your VPN provider's manual
# setup or API section. See the provider examples at the bottom of this file.
# ============================================================
# Your VPN provider (surfshark, nordvpn, protonvpn, airvpn, mullvad, private internet access, windscribe, etc.)
VPN_SERVICE_PROVIDER=airvpn
# Protocol: wireguard (recommended, faster) or openvpn
VPN_TYPE=wireguard
# --- WIREGUARD CREDENTIALS ---
# For Surfshark: go to https://my.surfshark.com/vpn/manual-setup/main → WireGuard
# For other providers: see the examples at the bottom of this file
WIREGUARD_PRIVATE_KEY=eDgf3GPFy2ltgx2RkD/Vx5wKZ4dVi28YbQmSJOrbWmk=
WIREGUARD_ADDRESSES=10.175.23.30
# Only needed for some providers (AirVPN). Leave blank if not required.
WIREGUARD_PUBLIC_KEY=
WIREGUARD_PRESHARED_KEY=+0+hrSdzRUxKDNk1Q37PNmNJ2jsj2EzF45JtbZad4lI=
# --- OPENVPN CREDENTIALS ---
# Only needed if VPN_TYPE=openvpn. Leave blank if using WireGuard.
OPENVPN_USER=
OPENVPN_PASSWORD=
# --- SERVER SELECTION ---
# Pick a country close to you for best speeds
SERVER_COUNTRIES=Canada
# --- PORT FORWARDING ---
# Supported by: ProtonVPN, AirVPN, PIA. Can help with upload speeds and seeding.
# Not required for downloading. Most users don't need this.
# Set to "on" if your provider supports it, leave blank otherwise (Surfshark, NordVPN, etc.).
VPN_PORT_FORWARDING=
# If your provider requires manually specifying a port (e.g. AirVPN):
FIREWALL_VPN_INPUT_PORTS=29261
# ============================================================
# NETWORK — Static IPs for each service
# You shouldn't need to change these unless you have a conflict.
# ============================================================
IP_GLUETUN=172.39.0.2
IP_RADARR=172.39.0.3
IP_SONARR=172.39.0.4
IP_LIDARR=172.39.0.5
IP_BAZARR=172.39.0.6
IP_JELLYFIN=172.39.0.7
IP_SEERR=172.39.0.8
IP_AUDIO=172.39.0.9
IP_NAVI=172.39.0.10
# ============================================================
# PROVIDER-SPECIFIC EXAMPLES
# Uncomment and fill in the section for your VPN provider.
# ============================================================
# --- NORDVPN ---
# 1. Go to: https://my.nordaccount.com/dashboard/nordvpn/manual-configuration/
# 2. Generate a WireGuard private key (NordLynx)
# 3. Paste the private key below
# VPN_SERVICE_PROVIDER=nordvpn
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_nordvpn_private_key_here
# WIREGUARD_ADDRESSES=10.5.0.2/16
# SERVER_COUNTRIES=United States
# --- PROTONVPN ---
# 1. Go to: https://account.protonvpn.com/ → Downloads → WireGuard configuration
# 2. Generate a config, open the file, copy the PrivateKey and Address
# 3. Port forwarding is supported on paid plans
# VPN_SERVICE_PROVIDER=protonvpn
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_proton_private_key_here
# WIREGUARD_ADDRESSES=10.2.0.2/32
# SERVER_COUNTRIES=United States
# VPN_PORT_FORWARDING=on
# --- SURFSHARK ---
# 1. Go to: https://my.surfshark.com/vpn/manual-setup/main
# 2. Get WireGuard credentials
# VPN_SERVICE_PROVIDER=surfshark
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_surfshark_private_key_here
# WIREGUARD_ADDRESSES=10.14.0.2/16
# SERVER_COUNTRIES=United States
# --- AIRVPN ---
# 1. Go to: https://airvpn.org/ → Client Area → Config Generator
# 2. Select Linux → WireGuard → pick a server → Generate
# 3. Copy all keys and the assigned IP
# VPN_SERVICE_PROVIDER=airvpn
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_airvpn_private_key_here
# WIREGUARD_PUBLIC_KEY=your_airvpn_public_key_here
# WIREGUARD_PRESHARED_KEY=your_airvpn_preshared_key_here
# WIREGUARD_ADDRESSES=your_assigned_ip/32
# FIREWALL_VPN_INPUT_PORTS=your_forwarded_port
# VPN_PORT_FORWARDING=on
# --- MULLVAD ---
# 1. Go to: https://mullvad.net/en/account → WireGuard configuration
# VPN_SERVICE_PROVIDER=mullvad
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_mullvad_private_key_here
# WIREGUARD_ADDRESSES=your_assigned_ip/32
# SERVER_COUNTRIES=United States
```