Rethinking Plex Hosting After the Hetzner Ban
Last October, Plex started blocking access to instances running on servers hosted by Hetzner.
I have a Hetzner Auction server that I renew every year or so to make use of newer hardware, which I use to run various workloads, from web services, to scheduled jobs and self-hosted instances of privacy-friendly alternative web frontends like Nitter.
Another one of those workloads, until recently, was Plex.
I didn’t have the time to put too much effort into getting around the Hetzner network ban when it was first implemented, so I just started running Jellyfin instead. I even made a video demonstrating how easy it was to get Jellyfin up and running on a VPS.
The Initial View from Europe⌗
Since getting laid off I finally had some time to try and think about how to get Plex working again.
My circumstances have changed since I first started hosting Plex back in the early 2010s:
- I no longer travel for work (previously, I was an ICRC field delegate and later, for a time, a software development consultant)
- I now have a fixed address and a desktop computer(!)
- I no longer live/work/travel in Europe
- I now have a lot of spare computer parts lying around the house
When I still lived in Europe, hosting Plex on Hetzner made a lot of sense because it was incredibly fast (both in terms of raw bandwidth and transcoding speed) when serving content in Europe and SWANA, and because I only had a laptop that I carried with me when I traveled for work.
The flow of network requests at that time were pretty simple:
Me (Anywhere)🔻
Plex (Hetzner Data Center)
🔻
Google Workspace (Rclone)
🔻
Plex (Hetzner Data Center)
🔻
Me (Anywhere)
The Current View from Washington⌗
Upon moving to the US, I was shocked at the poor quality of service provided by ISPs here. After a lot of reading, I came to realize that my own ISP, Xfinity, in particular was/is known for incredibly poor peering with Hetzner data centers.
To get around the poor peering, I proxied my requests to Plex through a VPS running in the US, and this worked quite well, despite the complexity creep.
The flow of network requests at that time became:
Me (Home)🔻
VPS (US Cloud Provider)
🔻
Plex (Hetzner Data Center)
🔻
Google Workspace (Rclone)
🔻
Plex (Hetzner Data Center)
🔻
VPS (US Cloud Provider)
🔻
Me (Home)
Storage Considerations⌗
Last summer, Google Workspace, which I was using to host the media served by my Plex instance, had a pricing restructure and effectively did away with their “unlimited” storage option.
This wasn’t too bad for me as I had less than 15TB of data in total on my Google Workspace account which was still possible to host on a Google Workspace with three active user accounts.
First Steps Forward⌗
Once I have started working again (and after I have restored my emergency fund) I would like to start buying some hard drives and setting up a local storage solution at home, making use of the old computer parts I have collected over the years.
Until then, with my media storage still all on Google Workspace, I think it makes sense to reduce the complexity of my current setup by removing the VPS that is being used to proxy my media streaming requests from the Hetzner server.
And since we are removing the VPS, why not just run the media server locally on
a virtual machine for now too, with the storage mounted via
rclone
from Google Workspace?
The flow of network requests can now be simplified again:
Me (Home)🔻
Plex (Home)
🔻
Google Workspace (Rclone)
🔻
Plex (Home)
🔻
Me (Home)
This works nicely because US ISPs generally don’t have peering issues with Google Workspace, so we can avoid the extra hop, while also benefiting from the traffic being served from the Plex server to the Plex client at home over the LAN.
Virtual Machine Setup⌗
My main desktop computer at home runs Windows 11 and a bunch of VMs on WSL2.
I figure that for now, until I am ready to assemble a dedicated server at home (I still need a motherboard and a case in addition to the hard drives), I can create another WSL2 VM running NixOS, which will make the eventual migration to bare metal a largely lift-and-shift operation.
This is how my Plex VM on WSL2 is configured, based on my various NixOS starter templates:
{
config,
pkgs,
username,
...
}: let
uid = username:
if config.users.users.${username}.uid == null
then "1000"
else toString config.users.users.${username}.uid;
gid = group:
if config.users.groups.${group}.gid == null
then "100"
else toString config.users.groups.${group}.gid;
inherit (config.users.users.${username}) home;
media = "${home}/media";
plexData = "${home}/plex";
remote = "${media}/remote";
mount = "gsuite-encrypted";
in {
imports = [
../modules/nix/base.nix
../modules/nix/linux.nix
];
system.stateVersion = "22.05";
time.timeZone = "America/Los_Angeles";
wsl = {
enable = true;
wslConf.automount.root = "/mnt";
wslConf.interop.appendWindowsPath = false;
wslConf.network.generateHosts = true;
defaultUser = username;
startMenuLaunchers = true;
docker-desktop.enable = false;
interop.register = true;
};
environment.systemPackages = [
(import ../pkgs/win32yank.nix {inherit pkgs;})
];
systemd.tmpfiles.rules = [
"d '${media}' 0755 ${username} users - -"
"d '${plexData}' 0755 ${username} users - -"
];
services.plex = let
plexPass = pkgs.plex.override {
plexRaw = pkgs.plexRaw.overrideAttrs (_: rec {
version = "1.32.8.7639-fb6452ebf";
src = pkgs.fetchurl {
url = "https://downloads.plex.tv/plex-media-server-new/${version}/debian/plexmediaserver_${version}_amd64.deb";
sha256 = "sha256-jdGVAdvm7kjxTP3CQ5w6dKZbfCRwSy9TrtxRHaV0/cs=";
};
});
};
in {
enable = true;
dataDir = "${plexData}";
user = username;
group = "users";
openFirewall = true;
package = plexPass;
};
systemd.services.remote_tv = {
wantedBy = ["multi-user.target"];
serviceConfig = {
ExecStartPre = "/run/wrappers/bin/sudo -u ${username} /run/current-system/sw/bin/mkdir -p ${remote}/tv";
ExecStart = ''
${pkgs.rclone}/bin/rclone mount '${mount}:media/TV Shows' ${remote}/tv \
--config=${config.sops.secrets."rclone/rclone.conf".path} \
--read-only \
--allow-other \
--allow-non-empty \
--uid ${uid username} \
--gid ${gid "users"} \
--log-level=INFO \
--buffer-size=50M \
--drive-acknowledge-abuse=true \
--vfs-cache-mode full \
--vfs-cache-max-size 100G \
--vfs-read-chunk-size=32M \
--vfs-read-chunk-size-limit=256M
'';
ExecStop = "/run/wrappers/bin/fusermount3 -u ${remote}/tv";
Type = "notify";
Restart = "always";
RestartSec = "10s";
EnvironmentFile = [config.sops.secrets."rclone/environment".path];
Environment = ["PATH=${pkgs.fuse}/bin:/run/wrappers/bin:$PATH"];
};
};
systemd.services.remote_movies = {
wantedBy = ["multi-user.target"];
serviceConfig = {
ExecStartPre = "/run/wrappers/bin/sudo -u ${username} /run/current-system/sw/bin/mkdir -p ${remote}/movies";
ExecStart = ''
${pkgs.rclone}/bin/rclone mount '${mount}:media/Movies' ${remote}/movies \
--config=${config.sops.secrets."rclone/rclone.conf".path} \
--read-only \
--allow-other \
--allow-non-empty \
--uid ${uid username} \
--gid ${gid "users"} \
--log-level=INFO \
--buffer-size=50M \
--drive-acknowledge-abuse=true \
--vfs-cache-mode full \
--vfs-cache-max-size 100G \
--vfs-read-chunk-size=32M \
--vfs-read-chunk-size-limit=256M
'';
ExecStop = "/run/wrappers/bin/fusermount3 -u ${remote}/movies";
Type = "notify";
Restart = "always";
RestartSec = "10s";
EnvironmentFile = [config.sops.secrets."rclone/environment".path];
Environment = ["PATH=${pkgs.fuse}/bin:/run/wrappers/bin:$PATH"];
};
};
}
Plex Connectivity⌗
With Plex running in a WSL2 VM, it will have an IP address matching
172.*.*.*
, which differs from my LAN’s 192.168.111.1/24
address range, so
there are a few values that we need to add in the settings.
Settings -> Remote Access -> Enable Remote Access
since192.168.111.1/24
addresses appear as “remote” to Plex- For
Settings -> Network -> LAN Networks
I set192.168.111.1/24
- For
Settings -> Network -> Custom server access URLs
I sethttp://192.168.111.221:32400
, where192.168.111.221
is the static address of the machine the Plex VM is running on - I grab the address of the VM from
/etc/resolv.conf
and set anetsh
rule so that requests to192.168.111.221:32400
are forwarded on to the VMsudo netsh interface portproxy add v4tov4 listenport=32400 listenaddress=0.0.0.0 connectport=32400 connectaddress=172.28.96.1
One thing to note however, is that the connectaddress
changes when the host
is restarted. This is very rare for me so I don’t mind updating this by hand
every now and then.
Keeping the Data Fresh⌗
Due to the poor quality of internet service in my apartment and across the US in general, it makes sense to keep network-heavy workloads running on the Hetzner server.
For a long time, I have used mergerfs
to create a consolidated view on the server of remote
files on Google
Workspace and local
files that “appear” on the server from time to time.
directory | description |
---|---|
media/remote/tv | Google Workspace files mounted via rclone |
media/local/tv | Local hard drive files |
media/merged/tv | All the files! 🎉 |
The media files under merged
are what Plex and Jellyfin running on the server
look at, providing a consolidated view of newer files on the hard drive and
older files mounted from Google Workspace, and every day a systemd
timer runs
to move any files older than 5 days to Google Workspace, freeing up space on
the local hard drive.
{
systemd.services.move_tv = {
enable = enableCron;
startAt = "daily";
serviceConfig = {
Type = "oneshot";
User = username;
Group = "users";
EnvironmentFile = [config.sops.secrets."rclone/environment".path];
ExecStart = ''
${pkgs.rclone}/bin/rclone \
--config=${config.sops.secrets."rclone/rclone.conf".path} \
--drive-upload-cutoff 1000T \
--tpslimit 5 \
move \
"${local}/tv" \
"${mount}:media/TV Shows" \
--log-file ${home}/logs/upload_tv.log \
--delete-empty-src-dirs \
--fast-list \
--stats-one-line \
-v \
--min-age 5d
'';
};
};
}
These days it’s exceedingly rare that new files “appear” on my server given that there are so many family subscriptions of Netflix, Hulu etc. going around in our extended family.
Nevertheless, given that Plex is now running in a VM on my home network, I have
removed the --min-age 5d
restriction from my systemd
timers so that
whatever new files may “appear” on the server on any given day are always moved
to Google Workspace within 24 hours.
Thinking Ahead⌗
When I am able to buy the remaining parts to put together a dedicated home server, the migration path seems pretty clear:
- Zip up the
plex
folder containing my Plex server configuration - Provision the new server with a NixOS template based on the existing VM configuration
- Unzip the Plex server configuration on the dedicated server before enabling
the
plex
service - Remove the VM-specific network configuration from the Plex server settings
- Slowly sync my media from Google Workspace to the dedicated server
- Once the sync is complete, add a new SFTP remote
remote to my
rclone
configuration for new files to also be pushed directly to the dedicated server at home from Hetzner
If you have any questions or comments you can reach out to me on Twitter and Mastodon.
If you’re interested in what I read to come up with solutions like this one, you can subscribe to my Software Development RSS feed.
If you’d like to watch me writing code while explaining what I’m doing, you can also subscribe to my YouTube channel.
If you found this content valuable, or if you are a happy user of
komorebi
or my NixOS starter
templates, please consider sponsoring me on
GitHub or tipping me on
Ko-fi.