Before this week, it had been a long time since I visited the Plex subreddit.

I shared my last article there, which was a technical write-up of moving my Plex instance from a Hetzner auction server to a virtual machine running on hardware in my home network, and the considerations that influenced the migration.

It didn’t take long for me to realize that a culture of hostility towards even the mention of Hetzner or other cloud hosting providers has strongly taken root since Plex announced it’s blanket network ban on IP ranges associated with Hetzner data centers.

I saw many posts and comments of users asking about issues with their Plex instances that had for years been working without issue on Hetzner servers until this past October when Plex enacted their very poorly communicated network ban, which hit a significant number of customers like myself who had paid for a lifetime Plex Pass.

Although I myself am not pursuing this option for reasons outlined in my last article, I wanted to share a clear and detailed example of how to circumvent Plex’s ban on IPs originating from Hetzner data centers (because gatekeeping is for losers).

WireGuard VPN Connection Details

This can work with any WireGuard VPN provider (even with your own WireGuard server on another machine!) but for the sake of simplicity I have chosen to use Mullvad as the reference in this tutorial.

  • Go to mullvad.net and open an account
    • This is actually a very cool process; no email, no details, they just provide you a secret account ID
  • Once you have an account, navigate to “Add time to your account”
    • You can just add 1 month of time for 5 EUR if you want to try out the quality of their service
  • Head over to the WireGuard Configuration page
    • Select “Linux” and then hit “Generate key”
    • Select a Country, City and Server for your exit location (bottom of the page)
    • Scroll down a little more to hit “Download file” and get your authentication details
      • You’ll only be able to do this once! Make sure you do it before you navigate away

At this point you’ll have a .conf file containing the fields Interface.PrivateKey and Interface.Address which you’ll need later.

Server Configuration

Below is a fully annotated NixOS server configuration which sets some sane server defaults, configures SSH access, firewall rules, and brings up a Plex container which sends all outgoing requests through a WireGuard VPN using your new connection details.

This server configuration does not include hardware configuration, which is naturally prone to variation, especially on auction servers, however it should not be too difficult to adapt my nixos-hetzner-robot-starter template (video walkthrough) to work with your server’s hardware.

{
  config,
}: let
  # These are helper functions to look up uids and gids
  # which take a single string argument
  #
  # eg. uid "samira" -> returns the uid for the "samira" user
  uid = username:
    if config.users.users.${username}.uid == null
    then "1000"
    else toString config.users.users.${username}.uid;

  # eg. gid "users" -> returns the gid for the "users" group
  gid = group:
    if config.users.groups.${group}.gid == null
    then "100"
    else toString config.users.groups.${group}.gid;

  # FIXME: Set your username for this server
  username = "<YOUR PREFERRED USERNAME>";

  # FIXME: Set this or you won't be able to SSH
  publicKeys = [
    "<YOUR PUBLIC KEY>"
    "<OPTIONALLY ANY OTHER PUBLIC KEYS OF YOURS>"
  ];

in {
  # This stops Docker interference with dhcp
  networking.dhcpcd.denyInterfaces = ["veth*"];

  # This allows incoming connections on port 32400 for Plex
  networking.firewall.allowedTCPPorts = [32400];

  # This sets the hostname of the server, this can be anything you like
  networking.hostName = "plex-on-hetz";

  # This is so that users in the "wheel" group don't need to
  # enter their password for sudo commands
  security.sudo.wheelNeedsPassword = false;

  # This enables SSH access to the server and only allows
  # root SSH connections with SSH keys, never with passwords
  services.openssh = {
    enable = true;
    settings.PermitRootLogin = "prohibit-password";
  };

  # This sets the SSH public key(s) you can use to connect
  # to the server with the "root" user
  users.users.root.openssh.authorizedKeys.keys = publicKeys;

  # This creates your user on the server
  users.users.${username} = {
    # This specifies that you are a user that gets home directory
    isNormalUser = true;
    # This adds your user to the "wheel" and "docker" groups
    # In the "wheel" group, you don't need to use your password for sudo
    # In the "docker" group, you don't need to use sudo for docker commands
    extraGroups = ["wheel" "docker"];

    # This sets the SSH public key(s) you can use to connect
    # to the server with your user
    openssh.authorizedKeys.keys = publicKeys;
  };

  # This is a service that bans hosts and IPs that produce authentication
  # errors when trying to SSH multiple times in quick succession
  services.fail2ban = {
    enable = true;
    # FIXME: Add this so you don't get locked out by mistake
    ignoreIP = ["<YOUR HOME IP ADDRESS>"];
  };

  # This enables Docker, makes sure it runs when the server starts
  # and automatically prunes dangling resources to keep space free
  virtualisation.docker = {
    enable = true;
    enableOnBoot = true;
    autoPrune.enable = true;
  };

  # This is where you can add Docker containers, you can think of this
  # block as being conceptually similar to docker-compose in some ways
  virtualisation.oci-containers = {
    backend = "docker";

    # This is the gluetun container: https://github.com/qdm12/gluetun
    # gluetun is a thin docker container for multiple VPN providers that
    # supports WireGuard
    containers.gluetun = {
      # This is so that the container starts automatically after server reboots
      autoStart = true;
      image = "qmcgaw/gluetun:v3.37.0";
      # This is so that we can access Plex on http://localhost:32400
      # from the server, ie. outside of the container
      ports = [
        "32400:32400"
      ];
      # This is where you enter authentication information you get from Mullvad
      environment = {
        VPN_SERVICE_PROVIDER = "mullvad";
        VPN_TYPE = "wireguard";
        # FIXME: Don't forget to add your connection details here!
        WIREGUARD_PRIVATE_KEY = "<Interface.PrivateKey in your downloaded conf file>";
        WIREGUARD_ADDRESSES = "<Interface.Address in your downloaded conf file>";
      };
      # This capability is required by gluetun
      extraOptions = [
        "--cap-add=NET_ADMIN"
      ];
    };

    # This is the Plex container
    containers.plex = {
      # This is so that the container starts automatically after server reboots
      autoStart = true;
      image = "plexinc/pms-docker:1.32.8.7639-fb6452ebf";
      # This is so that the `plex` user inside of the container has the same
      # UID and GID as your user to avoid permissions issues with any directories
      # that are mounted
      environment = {
        PLEX_UID = uid username;
        PLEX_GID = gid "users";
      };
      volumes = [
        # This is so that the configuration can be persisted outside of the container
        # in the ${HOME}/plex directory on the server
        #
        # NOTE: If you are migrating an instance started with `services.plex.enable` you
        # will need to set this as
        # "/path/to/your/plex-nixos/config/dir:/config/Library/Application\ Support"
        "/home/${username}/plex:/config"
        # This is where you share your media files that are under your user account
        # on the server with the Plex container so they can be seen, indexed and played
        #
        # NOTE: If you are migrating an instance started with `services.plex.enable` you
        # will need to make sure the paths inside the container (on the right hand side of the :)
        # match the paths that your Plex instance running as a NixOS service reference
        #
        # TODO: Whatever makes sense for you
        "/home/${username}/path/to/tv:/data/tv"
        "/home/${username}/path/to/movies:/data/movies"
        "/home/${username}/path/to/music:/data/music"
      ];
      # This is to make sure that this container won't start until the `gluetun` container
      # has started and is healthy
      dependsOn = ["gluetun"];
      # This is to make sure that network requests from this container go through the Mullvad
      # WireGuard VPN running in the `gluetun` container
      #
      # This is the key part that allows us to circumvent Plex's Hetzner network ban, because
      # it ensures that requests to https://*.plex.tv endpoints look like they are coming
      # from whichever WireGuard server we are connected to in the `gluetun` container
      #
      # This setup also ensures that other processes running on the server can continue
      # sending outgoing HTTP requests normally without going through the VPN
      extraOptions = [
        "--network=container:gluetun"
      ];
    };
  };
}

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.

Mullvad and Hetzner: Please feel free to give me some free VPN time / compute power for all this free positive PR ;)