Self-hosted service
navilg/media-stack avatar
navilg/media-stack

WireGuard is on the list while every provider is OpenVPN

A self-hosted stack for media management and streaming, with AI-powered movie and show recommendations. Includes Sonarr, Radarr, qBitTorrent, Prowlarr, Jellyfin, Seerr, Recommendarr, and VPN support.

1,248 stars122 forksUnknownGPL-3.0

At a glance

What is it?
media-stack is a compose bundle of eight services for self-hosted media, with a VPN profile that is mandatory unless you opt out. Its provider list includes WireGuard next to four commercial VPNs, and the same paragraph states that all providers use the OpenVPN protocol.
Who is it for?
media-stack suits a self-hoster who wants the arr applications and Jellyfin wired together with one network and one compose file, and who will keep the whole stack on one host. It does not suit a fleet deploying each service separately, or anyone running without a VPN who has not thought about it.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
GitHub does not report a main language for this repository.

Answers come from the project's GitHub data, last synced on October 12, 2026, and from our analysis. They are not legal advice.

Editorial analysis

No profile means nothing is deployed

The install section has one instruction that will save you an hour of confusion: running the docker compose command without a profile will not deploy anything. There are three profiles. vpn is the recommended route, no-vpn is the opt-out, and recommendarr adds the recommendation service. If you are installing without a VPN you must use the no-vpn profile, and the reason given is that the requirement prevents accidental or unintentional deployment without a VPN. That is a deliberate safety interlock rather than a Docker default, and it is implemented with Compose profiles. The Recommendarr section shows the syntax for combining two at once, COMPOSE_PROFILES=vpn,recommendarr with docker compose up -d, so a VPN plus recommendations is a comma-separated list rather than a separate profile name. Running with no profile is not an error and not a warning; nothing starts.

The provider list and the protocol sentence disagree

When VPN is enabled, qBittorrent and Prowlarr run behind it, which is the arrangement the whole profile exists to create. By default NordVPN is used in docker-compose.yml, and the alternatives listed are ExpressVPN, SurfShark, ProtonVPN, a custom OpenVPN configuration, and WireGuard VPN. The sentence that follows says all providers use the OpenVPN protocol. Those two statements cannot both be complete descriptions of that list, because WireGuard is not OpenVPN and is usually configured as a tunnel type of its own rather than as an OpenVPN provider entry. The README points to the Gluetun wiki for the full list of supported providers, so the compose file and the wiki are where the detail actually lives. The other half of the VPN setup is credentials: you generate an OpenVPN username and password from your provider and pass them as OPENVPN_USER and OPENVPN_PASSWORD alongside the provider name and, in the example, a server country. Enabling the VPN itself is described as comment and uncomment in the compose file rather than as a flag.

Static container IPs exist because Prowlarr addresses peers by IP

One requirement in the VPN path is not optional and the reason is specific. A static container IP address is needed when Prowlarr is behind a VPN, because Prowlarr can only communicate with Radarr and Sonarr using their container IP addresses, and those must be assigned manually to avoid connection problems when containers restart. The two environment variables that do it are RADARR_STATIC_CONTAINER_IP and SONARR_STATIC_CONTAINER_IP, and the deploy command for the NordVPN example passes both on the same line as the provider name, the OpenVPN credentials, the server countries and the profile flag. Underneath that is a Docker network you create yourself before anything else:

bash
docker network create --subnet 172.20.0.0/16 mynetwork

The README notes you should update the CIDR range based on your available IP range. So the network is a manual prerequisite, the subnet is your choice, and two containers in it get addresses you pin rather than let Docker assign, because a container that comes back with a different address after a restart breaks the peer relationship.

Seerr absorbed two projects and the volume needs re-owning

The notice at the top of the install section is for anyone arriving from Jellyseerr or Overseerr. As of version 3 those two have been unified into a single project called Seerr, so the stack now includes one request manager rather than two names for the same software. The migration instruction has two parts. Use the same volume name, jellyseerr-config, to preserve the existing configuration and avoid data loss. Then, before starting the Seerr container, change ownership of the config volume to UID 1000, because Seerr runs as a non-root user where Jellyseerr previously ran as root:

bash
docker run --rm -v media-stack_jellyseerr-config:/data alpine chown -R 1000:1000 /data

That one-liner runs an Alpine container purely to chown a volume, which is the standard trick when the target image does not let you do it before start. Skip it and Seerr starts as a non-root user against a root-owned volume and cannot write its configuration, which is the kind of failure that looks like an empty first-run setup rather than a permissions problem.

Eight services, each with its own port and first-run step

The stack is eight containers and the configuration section walks through them one at a time, which is also a map of the ports you will need. qBittorrent is at http://localhost:5080, where the default username is admin and the temporary password is collected from the container log with docker logs qbittorrent, after which the instruction is to change it under Tools, Options, WebUI. Radarr is at http://localhost:7878, Sonarr follows with its own port, Prowlarr has its own indexer management UI, and Jellyfin is the media server the whole stack feeds. Seerr sits in front as the request manager, Recommendarr reads from Sonarr and Radarr libraries plus the Jellyfin watchlist and library and optionally a Trakt watchlist, and gluetun is the VPN container that appears in the topic tags rather than in the prose list. The qBittorrent step also shows the volume fix you have to run inside the container, creating the downloads and tvshows directories and chowning them to 1000:1000 so the arr applications can write there.

Two version floors and an untested caveat

The requirements are two numbers with a caveat attached. Docker 28.0.1 or later, and Docker compose version v2.33.1 or later. Older versions may work, but they have not been tested, which is the phrasing that tells you the floor is where the author tested rather than where something breaks. Those floors matter more than they might look, because the stack uses Compose profiles, a recent compose specification, and an environment-variable driven deploy command with several variables on one line. An older compose either lacks profile support or handles the COMPOSE_PROFILES variable differently, so the failure mode is a command that returns without deploying what you expected. There is also a reverse proxy variant, an nginx compose file referenced as an optional line in the deploy commands, for anyone who wants the services behind a hostname rather than on separate ports. The last push to the default branch was on 2026-08-26, and the repository publishes no tagged versions.

Where media-stack is the wrong choice

Four cases. The first is a machine without Docker, or with a compose older than v2.33.1. The entire deployment model is a compose file with profiles and environment variables, so there is no install path outside it. The second is anyone who wants to configure the arr applications independently. The stack wires them together with one network, shared volumes and static addresses, and configuration is done by editing docker-compose.yml, which is fine for a single homelab and wrong for a fleet where each service is deployed on its own host with its own release cadence. The third is a deployment without a VPN and no willingness to think about it, since the README marks the no-vpn path as highly discouraged on the grounds that it may expose your IP address when torrenting, which is the correct warning to take seriously. The fourth is anyone expecting the recommendation service to be a curated editorial catalog. Recommendarr generates recommendations from your own libraries and watchlists using AI, so it reflects what you already have, which is a different thing from a human-curated list.

Editorial conclusion

media-stack suits a self-hoster who wants the arr applications and Jellyfin wired together with one network and one compose file, and who will keep the whole stack on one host. It does not suit a fleet deploying each service separately, or anyone running without a VPN who has not thought about it. Before deploying, create the Docker network with a subnet you have free, pick a profile since no profile starts nothing, and run the volume ownership fix before Seerr if you are coming from Jellyseerr.

Frequently asked questions

What is in the media-stack bundle?

Eight services: a VPN container, Radarr for movies, Sonarr for shows, Prowlarr as the torrent indexer manager, qBittorrent as the client, Seerr for requests, Jellyfin as the streamer, and Recommendarr for AI-generated recommendations.

Why does docker compose up deploy nothing?

Because a profile is required. You must run docker compose --profile vpn up -d or --profile no-vpn up -d, and the requirement is deliberate so the stack cannot be deployed without a VPN by accident.

Which VPN providers can media-stack use?

NordVPN by default in docker-compose.yml, with ExpressVPN, SurfShark, ProtonVPN, a custom OpenVPN configuration and WireGuard listed as alternatives. The README states that all providers use the OpenVPN protocol, which does not match a WireGuard entry.

Why do Radarr and Sonarr need static container IPs?

Because Prowlarr can only communicate with them using their container IP addresses when it is behind a VPN, and those addresses would change on restart. They are set with RADARR_STATIC_CONTAINER_IP and SONARR_STATIC_CONTAINER_IP.

How do I migrate from Jellyseerr to Seerr?

Use the same volume name, jellyseerr-config, to keep your configuration, and before starting the Seerr container change ownership of that volume to UID 1000 with a throwaway Alpine container, because Seerr runs as a non-root user where Jellyseerr ran as root.

Official sources

  1. Issues
  2. License: GPL-3.0
  3. navilg/media-stack on GitHub
  4. README