Files
..
2026-03-18 15:35:20 -05:00
2026-03-18 15:42:18 -05:00

Self-Hosted Media Server and Aggregation

Make sure to review everything here and if you have any issues please submit it as an issue. Also, we are more than open to any suggestions or edits. Also, checkout the Servarr Docker Setup for more details on installing the stack.

Caution

Some MAJOR Updates! Moved the VPN configuration and some of the env variables to a .env file. If you're watching the current live video it's a huge change. Will be uploading a new one in the next few days.

Navigation

Companion Video

# Updated video coming soon
[![alt text](image url)](video link)

Updates Since Video Publish

  • Added ytdl-sub to the compose.yaml. Remove if unwanted.

Media Server

Media Servers have their own guides! Check the link below and it will take you to the folder for the guides.

Data Directory

Folder Mapping

It's good practice to give all containers the same access to the same root directory or share. This is why all containers in the compose file have the bind volume mount /data:/data. It makes everything easier, plus passing in two volumes such as the commonly suggested /tv, /movies, and /downloads makes them look like two different file systems, even if they are a single file system outside the container. See my current setup below.

data
├── books
├── downloads
│   ├── qbittorrent
│   │   ├── completed
│   │   ├── incomplete
│   │   └── torrents
│   └── nzbget
│       ├── completed
│       ├── intermediate
│       ├── nzb
│       ├── queue
│       └── tmp
├── movies
├── music
├── shows
└── youtube

Here is an easy command to create the download directory scheme. Run within the /data directory.

mkdir -p downloads/qbittorrent/{completed,incomplete,torrents} && mkdir -p downloads/nzbget/{completed,intermediate,nzb,queue,tmp}

Network Share

I generally install Docker on the same LXC that I have my media server on as well as all my data. This, however, is not recommended by Proxmox. Going forward you should create a separate VM for all your docker containers and mount the data directory we created in the storage guide with the share. You can also use this method if you're using a separate share on another machine running something like Unraid or TrueNAS.

Within the VM install cifs-utils

sudo apt install cifs-utils

Now, edit the fstab file and add the following lines editing them to match your information:

sudo nano /etc/fstab
//10.0.0.100/data /data cifs x-systemd.automount,uid=1000,gid=1000,username=user,password=password,iocharset=utf8 0 0

Storing the user credentials within this file isn't the best idea. Check out this question on Stack Exchange to learn more.

Tip

The x-systemd.automount option mounts the share on first access instead of at boot. This prevents mount error(111): Connection refused errors caused by fstab running before the network is ready.

Now reload the configuration and mount the shares with the following commands.

sudo systemctl daemon-reload
sudo mount -a

User Permissions

Using bind mounts (path/to/config:/config) may lead to permission conflicts between the host operating system and the container. To avoid this problem, you can specify the user ID (PUID) and group ID (PGID) to use within some of the containers. This will give your user permissions to read and write configuration files, etc.

In the compose file I use PUID=1000 and PGID=1000, as those are generally the default IDs in most Linux systems, but depending on your setup you may need to change this.

id your_user

This command will return something like the following:

uid=1000(your_user) gid=1000(your_user) groups=1000(your_user),27(sudo),24(cdrom),30(dip),46(plugdev),108(lxd)

If you are using a network share mounted though /etc/fstab match the permissions there. Learn more above.

If you run into errors after creating all the folders you can assign the permissions using chown. For example:

sudo chown -R 1000:1000 /data

Also, I like to store all my Docker configurations in a root /docker directory on my Linux system. These can go wherever you prefer whether that be your home directory or somewhere else. Do note, many Docker apps may have issues if you're trying to store you Docker configurations in a SMB network share.

mkdir /docker
sudo chown -R 1000:1000 /docker

Docker Compose and .env

Navigate to the directory you want to spin up the servarr stack in. I run mine from /docker/servarr but you can run it from anywhere you'd like such as /home/user/docker/servarr. Then download the compose.yaml and .env files from this repo.

wget https://github.com/TechHutTV/homelab/raw/refs/heads/main/media/compose.yaml && wget https://github.com/TechHutTV/homelab/raw/refs/heads/main/media/.env

Most of our editing is going to be done in the .env file. Here you change your UID and GID, timezone, and add all your VPN keys and info. You can also make edits to the compose.yaml file such as the mount point locations, for example, if you are using something other than /data:/data or even changing the docker network IP addresses for your services.

Gluetun VPN

Setup and Configuration

I like to set this out with AirVPN (referral link). I'm not affiliated with them in any way other than the referral link. I've tried a few other providers and they're my preference. If you already have a VPN checkout the providers page on their wiki.

On AirVPN navigate to the Client Area from here select the Config Generator. Now in the options select Linux then toggle the WireGuard option. Select New device and then scroll down to By single server and select a server that is best for you. For example, Titawin (Vancouver) was my selection because, at the time, it had the fewest users with good speeds. Scroll all the way to the bottom and select Generate. This will download a conf file with all of your information.

Back in AirVPN navigate to the Client Area from here select Manage under Ports. If you already have a port open click on Test open otherwise click the plus button under Add a new port then click Test open for that port. Here you will find the specific servers that you can use your port on. If there is a Connection refused warning next the server you generated your configuration for change the port until the warning goes away. For example, in my case the 'Titawin (Vancouver) server that I selected with my port is good to use.

Caution

Do NOT forward on your router the same ports you use on your listening services while connected to the VPN.

Now, in the same directory as your docker compose.yaml file create a .env file. Paste in the variables below and then add all the information from your downloaded .conf file.

nano .env
# General UID/GID and Timezone
TZ=America/Los_Angeles
PUID=1000
PGID=1000

# Input your VPN provider and type here
VPN_SERVICE_PROVIDER=airvpn
VPN_TYPE=wireguard

# Mandatory, airvpn forwarded port
FIREWALL_VPN_INPUT_PORTS=port # mandatory, airvpn forwarded port

# Copy all these variables from your generated configuration file
WIREGUARD_PUBLIC_KEY=key
WIREGUARD_PRIVATE_KEY=key
WIREGUARD_PRESHARED_KEY=key
WIREGUARD_ADDRESSES=ipv4

# Optional location variables, comma separated list, no spaces after commas, make sure it matches the config you created
# NOTE: These can cause connection failures with some providers. Remove or comment out if Gluetun won't connect.
#SERVER_COUNTRIES=country
#SERVER_CITIES=city

# Health check duration
HEALTH_VPN_DURATION_INITIAL=120s

Testing Gluetun Connectivity

Once your containers are up and running, you can test your connection is correct and secured. This assumes you keep the gluetun container name. Learn more at the gluetun wiki.

Note

If you run into issues try restarting the stack with docker compose restart.

docker run --rm --network=container:gluetun alpine:3.18 sh -c "apk add wget && wget -qO- https://ipinfo.io"

If you'd like to test Gluetun connectivity from a container using the service jump into the docker compose exec console and run the wget command below. Tested with nzbget, qbittorrent, and prowlarr containers. Ensure you open the ports through the the gluetun container.

docker exec -it container_name bash
wget -qO- https://ipinfo.io

Passing Through Containers

When containers are in the same docker compose then all you need to add is a network_mode: service:container_name and open the ports through the the gluetun container. See example with a different torrent client below.

services:
  gluetun:
    image: qmcgaw/gluetun
    container_name: gluetun
    ...
    ports:
      - 8888:8112 # deluge web interface
      - 58846:58846 # deluge RPC
  deluge:
    image: linuxserver/deluge:latest
    container_name: deluge
    ...
    network_mode: service:gluetun

External Container to Gluetun

Add the following when launching the container, provided Gluetun is already running on the same machine.

--network=container:gluetun

If the container is in another docker compose.yaml, assuming Gluetun is already running add the following network mode. Ensure you open the ports through the the gluetun container.

network_mode: "container:gluetun"

Gluetun Proxmox LXC Setup

Errors like cannot Unix Open TUN device file: operation not permitted and cannot create TUN device file node: operation not permitted may happen if you're running this on LXC containers.

Find your container number, for example mine is 101

Edit /etc/pve/lxc/101.conf and add:

lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net dev/net none bind,create=dir
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file

Make sure you pass through the tun device (/dev/net/tun:/dev/net/tun) as shown in my compose file.

Reduce Gluetun Ram Usage

As mentioned in this issue there is a feature request on the Gluetun Github page to help reduce ram usage. Gluetun bundles a recursive caching DNS resolver called unbound for handling domain name requests securely. Over time the cache size, which rests in RAM, can balloon to gigabytes.

You can do this by adding the following to your docker compose.yaml file under the gluetun environment variables.

  gluetun:
    ...
    environment:
      - BLOCK_MALICIOUS=off # Disable unbound DNS resolver

This may not be an issue as DNS over HTTPS in Go to replace Unbound is implemented, but it's worth the mention.

Gluetun Container Unhealthy

If you see dependency failed to start: container gluetun is unhealthy, follow these steps:

  1. Check the logs: docker logs gluetun
  2. Verify .env file: Ensure ALL placeholder values (key, port, country, city) are replaced with your actual VPN configuration
  3. Remove SERVER_COUNTRIES and SERVER_CITIES: These optional variables can cause connection failures with some providers. Comment them out or remove them from your .env file and try again.
  4. Verify /dev/net/tun exists: Run ls -la /dev/net/tun
  5. For LXC users: Enable TUN device passthrough in your container config

Gluetun DNS Timeout / i/o Timeout

If Gluetun logs show repeated i/o timeout errors like dial tcp 1.1.1.1:853: i/o timeout, the VPN tunnel has lost connectivity. Try the following:

  1. Reset Gluetun state: Stop the stack, delete the gluetun folder, and recompose:
    docker compose down
    rm -rf ./gluetun
    docker compose up -d
    
  2. Check your VPN provider's server status: The server you selected may be down or overloaded. Try a different server.
  3. Try a different VPN protocol: Some providers (e.g., PIA) may work better with OpenVPN than WireGuard. Check the Gluetun provider docs for your specific provider's recommended setup.

Download Clients

NZBGet

NZBGet Login Credentials

The default credentials for NZBGet are a username of nzbget and a password of tegbzn6789. It's strongly recommended to change these default credentials for security reasons. This can be done under Settings > SECURITY, then change the ControlUsername and ControlPassword.

NZBGet Download Directories

If following the /data:/data directory scheme and used the command to setup the download directories open the qBittorrent Web UI and do under Settings > PATHS and change the paths.

MainDir: /data/downloads/nzbget

DestDir: ${MainDir}/completed

InterDir: ${MainDir}/intermediate

And keep everything else as is.

NZBGet with Network Shares (NAS)

If your /data directory is a mounted network share (e.g., Synology, TrueNAS, Unraid), NZBGet can fill up your VM's local disk if the NAS goes offline or enters standby. NZBGet will create the download directories locally when it can't reach the share, quickly consuming all available space and causing other services to fail.

To prevent this, set InterDir to a local path on the VM (not on the network share) so downloads happen locally first, and only the final move goes to the NAS:

InterDir: /docker/nzbget/intermediate (local to VM)

DestDir: ${MainDir}/completed (on network share)

If your VM disk does fill up, stop the stack, remove the locally created directories, bring the NAS back online, and restart.

Fix directory does not appear to exist inside the container error

This error may appear within Sonarr and Radarr. Once NZBGet is setup go to settings and under INCOMING NZBS change the AppendCategoryDir to No. This will prevent some potential mapping issues and save on unnecessary directories.

qBittorrent

qBittorrent Login Credentials

When you first launch qBittorrent it will generate a random password. To find this password you can view the logs to see what the password is.

docker container logs qbittorrent

Now, go to your settings and setup a new username and password under WebUI > Authentication.

Qbittorrent Download Directories

If following the /data:/data directory scheme and used the command to setup the download directories open the qBittorrent Web UI and do under Settings > Downloads and change the paths.

Default Save Path: /data/downloads/qbittorrent/completed

Keep incomplete torrents in: /data/downloads/qbittorrent/incomplete

Copy .torrent files to: /data/downloads/qbittorrent/torrents

qBittorrent Stalls with VPN Timeout

qBittorrent stalls out if there is a timeout or any type of interruption on the VPN. This is good because it drops connection, but we need it to fire back up when the connection is restored without manually restarting the container.

Solution #1: Within the WebUI of qBittorrent head over to advanced options and select tun0 as the networking interface. See image below for example.

Set Network Interface to tun0

Next, I added HEALTH_VPN_DURATION_INITIAL=120s to my gluetun environment variables as per this issue. I updated my compose.yaml above with this variable so you may already have this enabled. You can learn more about this on their wiki. If you continue to have issues continue to next solution.

Solution #2: Another solution, that can be used in conjunction with Solution #1 is using the deunhealth container to automatically restart qBittorrent when it gives an unhealthy status. We've added this to our compose.yaml for this stack.

  deunhealth:
    image: qmcgaw/deunhealth
    container_name: deunhealth
    network_mode: "none"
    environment:
      - LOG_LEVEL=info
      - HEALTH_SERVER_ADDRESS=127.0.0.1:9999
      - TZ=America/Los_Angeles
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Next we need to add a health check and label to our qbittorrent container. We add deunhealth.restart.on.unhealthy=true as a label and a simple ping health check as shown below.

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    restart: unless-stopped
    labels:
      deunhealth.restart.on.unhealthy=true # Label added for deunhealth monitoring
    ...

Relevant Resources: DBTech video on deunhealth, gluetun/issues/2442 and gluetun/issues/1277

FlareSolverr

FlareSolverr is a proxy server that bypasses Cloudflare and DDoS-GUARD protection on indexers. Some torrent and usenet indexers use Cloudflare anti-bot challenges which prevent Prowlarr from accessing them. FlareSolverr runs a headless browser to solve these challenges automatically.

It's included in the compose.yaml and routed through Gluetun like the rest of the stack. To connect it to Prowlarr:

  1. Open Prowlarr and go to Settings > Indexers.
  2. Click the + button under Indexer Proxies and select FlareSolverr.
  3. Set the Host to http://localhost:8191 (since it shares the Gluetun network).
  4. Click Test then Save.

Once added, Prowlarr will automatically route requests through FlareSolverr for indexers that require it.

*arr Apps

When connecting your *arr applications be sure to use the new configured IP addresses in the servarrnetwork. We will soon update this section with more text documentation.