<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Writing — Arne Bahlo</title><description>Articles written by Arne Bahlo.</description><link>https://2023.arne.me/writing</link><language>en-us</language><item><title>Static OG (Open Graph) Images in Astro</title><link>https://2023.arne.me/writing/static-og-images-in-astro/</link><guid>https://2023.arne.me/writing/static-og-images-in-astro/</guid><description>A guide to set up build-time Open Graph images in Astro using Satori, sharp and Astro endpoints.</description><pubDate>Fri, 07 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Have you every shared a link to a friend and wondered where the image preview
came from?
That&apos;s the &lt;a href=&quot;https://ogp.me/&quot;&gt;Open Graph Protocol&lt;/a&gt;, a set of HTML &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt;
extensions that can enrich a link, originally invented at Facebook.
This blog post describes how you can generate these images on build time in
the &lt;a href=&quot;https://astro.build&quot;&gt;Astro web framework&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Most of the guides out there (including Vercel&apos;s official &lt;a href=&quot;https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation&quot;&gt;OG Image Generation&lt;/a&gt;)
use a function to generate the OG image dynamically.
While there&apos;s nothing wrong with that, I really wanted mine to be statically
generated on build-time; it&apos;s faster, cheaper and cooler.&lt;/p&gt;
&lt;h2&gt;Build your image&lt;/h2&gt;
&lt;p&gt;The first thing to do is figure out what your OG image should look like.
We&apos;re going to use Vercel&apos;s &lt;a href=&quot;https://github.com/vercel/satori&quot;&gt;Satori&lt;/a&gt;, a
JavaScript library which can render a HTML tree as SVG.
Vercel also has a great &lt;a href=&quot;https://og-playground.vercel.app/&quot;&gt;OG Image Playground&lt;/a&gt;,
where you can play around and quickly get results.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Set the size to 1200×630px as that&apos;s what Facebook
recommends in &lt;a href=&quot;https://developers.facebook.com/docs/sharing/webmasters/images/&quot;&gt;their guidelines&lt;/a&gt;.
Use Flexbox liberally and enable debug mode when figuring out the layout.
&lt;a href=&quot;https://css-tricks.com/snippets/css/a-guide-to-flexbox/&quot;&gt;A Complete Guide to Flexbox&lt;/a&gt;
is a great resource to have at hand.&lt;/p&gt;
&lt;h2&gt;Create an Astro endpoint&lt;/h2&gt;
&lt;p&gt;Got a nice image built in the playground? Then let&apos;s get started.
Install Satori to generate the SVG and &lt;a href=&quot;https://github.com/lovell/sharp&quot;&gt;sharp&lt;/a&gt;
to then convert it to PNG:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ npm install satori sharp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create an endpoint, e.g. &lt;code&gt;pages/og-image.png.ts&lt;/code&gt; with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import fs from &quot;fs/promises&quot;;
import satori from &quot;satori&quot;;
import sharp from &quot;sharp&quot;;
import type { APIRoute } from &apos;astro&apos;;

export const get: APIRoute = async function get({ params, request }) {
  const robotoData = await fs.readFile(&quot;./public/fonts/roboto/Roboto-Regular.ttf&quot;);

  const svg = await satori(
    { 
      type: &quot;h1&quot;, 
      props: { 
        children: &quot;Hello world&quot;, 
        style: { 
          fontWeight: &quot;bold&quot; 
        }
      }
    },
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: &quot;Roboto&quot;,
          data: robotoData,
          weight: &quot;normal&quot;,
          style: &quot;normal&quot;,
        },
      ],
    }
  );

  const png = await sharp(Buffer.from(svg)).png().toBuffer();

  return new Response(png, {
    headers: {
      &quot;Content-Type&quot;: &quot;image/png&quot;,
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One drawback that you can already see in the code above is that Astro does not support TSX endpoints, so we&apos;ll need to use React-elements-like objects.&lt;/p&gt;
&lt;p&gt;You&apos;ll also always need to provide a font because it&apos;ll be embedded.
I used &lt;a href=&quot;https://fonts.google.com/specimen/Roboto&quot;&gt;Roboto&lt;/a&gt; in the example above,
&lt;a href=&quot;https://rsms.me/inter/&quot;&gt;Inter&lt;/a&gt; or &lt;a href=&quot;https://fonts.google.com/specimen/Open+Sans&quot;&gt;Open Sans&lt;/a&gt;
are other solid free sans fonts (choose WOFF or TTF/OTF, WOFF2 is not supported).&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;astro dev&lt;/code&gt; and navigate to the endpoint (in our example &lt;a href=&quot;http://localhost:3000/og-image.png&quot;&gt;:3000/og-image.png&lt;/a&gt;)
to see the generated image.&lt;/p&gt;
&lt;h2&gt;Generate for each item in a collection&lt;/h2&gt;
&lt;p&gt;Once you have an image that you like in a function, you can create them in
batch, for whole collections.
Let&apos;s assume you have a &lt;code&gt;blog&lt;/code&gt; collection and your blog posts live at &lt;code&gt;pages/blog/:slug/index.astro&lt;/code&gt;.
Create a &lt;code&gt;pages/blog/:slug/og-image.png.ts&lt;/code&gt; with your API route and the &lt;code&gt;getStaticPaths&lt;/code&gt; function exported:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { getCollection, getEntryBySlug } from &quot;astro:content&quot;;

export async function getStaticPaths() {
  const posts = await getCollection(&quot;blog&quot;);
  return posts.map((post) =&amp;gt; ({
    params: { slug: post.slug },
    props: post,
  }));
}

export const get: APIRoute = async function get({ params, request }) {
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you now run &lt;code&gt;astro build&lt;/code&gt;, you&apos;ll see that it statically generates an OG image for every blog post you have on your site.&lt;/p&gt;
&lt;h2&gt;Handling images&lt;/h2&gt;
&lt;p&gt;Images (like fonts) need to be embedded into the SVG.
The easiest way I found is using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs&quot;&gt;data urls&lt;/a&gt;
by first reading the file to a Base64 string like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const myImageBase64 = (await fs.readFile(&quot;./public/my-image.png&quot;)).toString(&quot;base64&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then setting it as the &lt;code&gt;backgroundImage&lt;/code&gt; or &lt;code&gt;src&lt;/code&gt; property in Satori:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  type: &quot;div&quot;,
  props: {
    style: {
      backgroundImage: `url(&apos;data:image/png;base64,${myImageBase64}&apos;)`,
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be aware that Satori does not support &lt;code&gt;backgroundSize: cover&lt;/code&gt;, so if you have
that use case, you&apos;ll need to build it yourself with &lt;a href=&quot;https://github.com/image-size/image-size&quot;&gt;image-size&lt;/a&gt; and some math.&lt;/p&gt;
&lt;h2&gt;Set OG tags in HTML&lt;/h2&gt;
&lt;p&gt;Now the only thing left to do is link to your OG images in your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.
There are two properties you&apos;ll want to use for images: &lt;code&gt;og:image&lt;/code&gt; and
&lt;code&gt;twitter:image&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta property=&quot;og:image&quot; content=&quot;/blog/og-image.png&quot; /&amp;gt;
&amp;lt;meta property=&quot;twitter:image&quot; content=&quot;/blog/og-image.png&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use dynamic paths on collections to automatically use the correct image.
Check out &lt;a href=&quot;https://ogp.me/&quot;&gt;the Open Graph protocol&lt;/a&gt; for more Open Graph meta
extensions.&lt;/p&gt;
&lt;h2&gt;Further links &amp;amp; conclusion&lt;/h2&gt;
&lt;p&gt;If you want to see real, working code (I know I often do), check out the endpoint
that powers the OG images of my book reviews:
&lt;a href=&quot;https://github.com/bahlo/arne.me/blob/main/src/pages/books/%5B...slug%5D/og-image.png.ts&quot;&gt;&lt;code&gt;pages/books/[...slug]/og-image.png.ts&lt;/code&gt;&lt;/a&gt;.
This is what it looks like: &lt;a href=&quot;/books/the-design-of-everyday-things/og-image.png&quot;&gt;OG image of a book review&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Did I miss anything? Can this workflow be further improved?
&lt;a href=&quot;mailto:hey@arne.me&quot;&gt;Drop me an email&lt;/a&gt; or &lt;a href=&quot;https://spezi.social/@arne&quot;&gt;@ me in the Fediverse&lt;/a&gt;,
I&apos;d love to hear from you.&lt;/p&gt;
</content:encoded></item><item><title>You’re Using Email Wrong</title><link>https://2023.arne.me/writing/youre-using-email-wrong/</link><guid>https://2023.arne.me/writing/youre-using-email-wrong/</guid><description>If you don’t like email, try a different strategy.</description><pubDate>Sun, 06 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You probably don&apos;t like email, not a lot of people do.
That&apos;s because you&apos;re using it wrong.&lt;/p&gt;
&lt;p&gt;Chances are that if you look at your inbox, it&apos;s full of unsolicited marketing
emails, log-in notifications or spam.
Or you&apos;re doing inbox zero and all that trash lives in your archive.&lt;/p&gt;
&lt;p&gt;As everything else, email is subject (hah) to entropy.
If you&apos;re not careful, chaos will take over and your email inbox will look like
the screenshot above.&lt;/p&gt;
&lt;h2&gt;A different concept&lt;/h2&gt;
&lt;p&gt;As controversial as the company behind &lt;a href=&quot;https://www.hey.com&quot;&gt;HEY&lt;/a&gt; is, the
concept that they introduced with their mail app has fundamentally changed how I
think about email.
I adapted the part that resonated with me to my &lt;a href=&quot;https://fastmail.com&quot;&gt;Fastmail&lt;/a&gt;
account like this:&lt;/p&gt;
&lt;h3&gt;Inbox&lt;/h3&gt;
&lt;p&gt;This is where all emails sent by humans end up.
That&apos;s it.&lt;/p&gt;
&lt;h3&gt;Papertrail&lt;/h3&gt;
&lt;p&gt;Notifications, invoices, everything that you don&apos;t want to delete but are not
really interested in.
This folder has about 1.6k unread emails right now.
They&apos;re not meant to be read, but if I need to look something up, I know where
to look.&lt;/p&gt;
&lt;h3&gt;Newsfeed&lt;/h3&gt;
&lt;p&gt;I&apos;m subscribed to over 20 newsletters and all of them end up in here.
If I have time, I&apos;ll read the newsletters and add the articles I want to read to
&lt;a href=&quot;https://instapaper.com&quot;&gt;Instapaper&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By the way, if you&apos;re looking for something to add to your newsfeed, check out
&lt;a href=&quot;/weekly&quot;&gt;Arne&apos;s Weekly&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Setup&lt;/h2&gt;
&lt;p&gt;I&apos;m using Fastmail with a custom domain and have aliases with rules for the
different destinations, for example &lt;code&gt;papertrail@example.org&lt;/code&gt; or
&lt;code&gt;newsfeed@example.org&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This way, if I sign up for a new service or subscribe to a newsletter, instead
of having to adapt existing rules, I can use the proper email address and
everything will go where it should.&lt;/p&gt;
&lt;p&gt;This is not Fastmail specific, you can do the same with most providers.
For example, if you have &lt;code&gt;hikingfan@gmail.com&lt;/code&gt;, you could set a rule for
&lt;code&gt;hikingfan+papertrail@gmail.com&lt;/code&gt; and one for &lt;code&gt;hikingfan+newsfeed@gmail.com&lt;/code&gt; and
use those email addresses when signing up.&lt;/p&gt;
&lt;h2&gt;The human factor&lt;/h2&gt;
&lt;p&gt;If I look at my inbox, it&apos;s a joy.
Feedback to blog posts, articles and personal messages from people I care about.
Sticking with this strategy made email about humans, not about machines.&lt;/p&gt;
&lt;p&gt;I encourage you to try it!&lt;/p&gt;
&lt;p&gt;If you have a different concept, feedback or ideas, please
&lt;a href=&quot;mailto:hey@arne.me&quot;&gt;let me know&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (Mar 08, 2022):&lt;/strong&gt; Multiple people have responded with their systems and
questions (which I love, please keep doing that), some missed a way to track if
a service &quot;lost&quot; their email address.
If you use an alias, e.g. &lt;code&gt;papertrail@example.org&lt;/code&gt;, you can use the &lt;code&gt;+&lt;/code&gt; operator
like this: &lt;code&gt;papertrail+twitter@example.org&lt;/code&gt;.
The sorting rule will be a bit more complicated, but you won&apos;t have to adapt
it every time you sign up for something.&lt;/p&gt;
</content:encoded></item><item><title>Plex on NixOS</title><link>https://2023.arne.me/writing/plex-on-nixos/</link><guid>https://2023.arne.me/writing/plex-on-nixos/</guid><description>In this post I describe how I set up Plex on NixOS, including a virtual file system for Backblaze B2 and Nginx for HTTPS.</description><pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A few weeks ago, the hard drive (yes, I know) in my home lab died.
It was a sad moment, especially because I ran Plex on it and rely on that for my
music and audiobook needs.&lt;/p&gt;
&lt;p&gt;The upside is that it gave me the opportunity to rethink my Plex setup.
Hosting it at home is great for storage costs and control, but it&apos;s hard to
share with friends or access on the go, especially with a NATed IPv4, so I
decided to move to the cloud.&lt;/p&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;h2&gt;Choosing a server and storage method&lt;/h2&gt;
&lt;p&gt;I chose &lt;a href=&quot;https://cloud.hetzner.com&quot;&gt;Hetzner Cloud&lt;/a&gt; because I like their service,
and they use green energy.&lt;/p&gt;
&lt;p&gt;The biggest challenge was storage. Hetzner charges around €50/month for a 1 TB
volume (others have comparable pricing).&lt;/p&gt;
&lt;p&gt;But then my friend Eric told me about &lt;a href=&quot;https://rclone.org&quot;&gt;rclone&lt;/a&gt; and its
ability to mount blob storage (which is cheap) as a virtual disk.
That means Plex sees all files as if they were actually there and if it tries
to read a file, it&apos;s downloaded on demand if it&apos;s not cached already.&lt;/p&gt;
&lt;p&gt;Armed with this knowledge, I started setting up the server.&lt;/p&gt;
&lt;h2&gt;Setting up NixOS&lt;/h2&gt;
&lt;p&gt;NixOS is a declarative and reproducible operating system.
You have a configuration file in &lt;code&gt;/etc/nixos/configuration.nix&lt;/code&gt; that defines
your installed applications, configuration and system setup.
And if you mess up, you can always roll back.&lt;/p&gt;
&lt;p&gt;The first I did was creating a server on Hetzner with any distribution (I went
width a &lt;a href=&quot;https://www.hetzner.com/cloud#pricing&quot;&gt;CPX11&lt;/a&gt; and Ubuntu) and then following the instructions on the
&lt;a href=&quot;https://github.com/nix-community/nixos-install-scripts/tree/master/hosters/hetzner-cloud&quot;&gt;install scripts for Hetzner Cloud&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you follow along, make sure to choose a server with at least 40 GB of disk
space.&lt;/p&gt;
&lt;p&gt;After booting into NixOS, I changed the root password by running &lt;code&gt;passwd&lt;/code&gt; and
upgraded NixOS (see &lt;a href=&quot;https://nixos.org/manual/nixos/stable/index.html#sec-upgrading&quot;&gt;Upgrading NixOS&lt;/a&gt;).
If you want to further secure your NixOS installation, Christine Dodrill has a
great guide called &lt;a href=&quot;https://christine.website/blog/paranoid-nixos-2021-07-18&quot;&gt;Paranoid NixOS Setup&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Setting up storage&lt;/h2&gt;
&lt;p&gt;I decided to go with &lt;a href=&quot;https://www.backblaze.com/b2/cloud-storage.html&quot;&gt;Backblaze B2&lt;/a&gt;
as I have used it before, it&apos;s cheaper than S3, and I don&apos;t support Amazon.
If you want to use something else, rclone supports
&lt;a href=&quot;https://rclone.org/#providers&quot;&gt;a lot of providers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After creating a bucket for the media, I created an Application Key and made
note of the &lt;code&gt;keyID&lt;/code&gt; and &lt;code&gt;applicationKey&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then I added the following lines to my Nix configuration at
&lt;code&gt;/etc/nixos/configuration.nix&lt;/code&gt; to install rclone and create a
&lt;code&gt;/etc/rclone/rclone.conf&lt;/code&gt; for the bucket:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;environment.systemPackages = [ pkgs.rclone ];

environment.etc = {
  &quot;rclone/rclone.conf&quot; = {
    text = &apos;&apos;
      [b2]
      type = b2
      account = &amp;lt;keyID&amp;gt;
      key = &amp;lt;applicationKey&amp;gt;
      hard_delete = true
      versions = false
    &apos;&apos;;
    mode = &quot;0644&quot;;
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you follow along, make sure to replace &lt;code&gt;&amp;lt;keyID&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;applicationKey&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;By the way, NixOS comes with nano preinstalled, so if you want a real editor,
you can get it with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ nix-shell -p vim
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the disk mount, I created a Systemd service that mounts the bucket on start
and automatically starts on boot.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemd.services.plex_media = {
  enable = true;
  description = &quot;Mount media dir&quot;;
  wantedBy = [&quot;multi-user.target&quot;];
  serviceConfig = {
    ExecStartPre = &quot;/run/current-system/sw/bin/mkdir -p /mnt/media&quot;;
    ExecStart = &apos;&apos;
      ${pkgs.rclone}/bin/rclone mount &apos;b2:&amp;lt;bucket name&amp;gt;/&apos; /mnt/media \
        --config=/etc/rclone/rclone.conf \
        --allow-other \
        --allow-non-empty \
        --log-level=INFO \
        --buffer-size=50M \
        --drive-acknowledge-abuse=true \
        --no-modtime \
        --vfs-cache-mode full \
        --vfs-cache-max-size 20G \
        --vfs-read-chunk-size=32M \
        --vfs-read-chunk-size-limit=256M
    &apos;&apos;;
    ExecStop = &quot;/run/wrappers/bin/fusermount -u /mnt/media&quot;;
    Type = &quot;notify&quot;;
    Restart = &quot;always&quot;;
    RestartSec = &quot;10s&quot;;
    Environment = [&quot;PATH=${pkgs.fuse}/bin:$PATH&quot;];
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you follow along, make sure to replace &lt;code&gt;&amp;lt;bucket name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--vfs-*&lt;/code&gt; arguments configure the virtual file system.
I only have 40 GB local disk space, so I set the cache size to 20 GB (using
&lt;code&gt;--vfs-cache-max-size&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;I then ran &lt;code&gt;nixos-rebuild switch&lt;/code&gt; to apply the configuration, uploaded some
data to the bucket and listed &lt;code&gt;/mnt/media&lt;/code&gt; to make sure everything works.&lt;/p&gt;
&lt;h2&gt;Configuring Plex&lt;/h2&gt;
&lt;p&gt;NixOS has a predefined service for Plex, which I used like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nixpkgs.config.allowUnfree = true; # Plex is unfree

services.plex = {
  enable = true;
  dataDir = &quot;/var/lib/plex&quot;;
  openFirewall = true;
  user = &quot;plex&quot;;
  group = &quot;plex&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this configuration, Nix will open the correct ports in the firewall,
create a user called &lt;code&gt;plex&lt;/code&gt; with a group also called &lt;code&gt;plex&lt;/code&gt; and install the Plex
Media Server with the configuration in &lt;code&gt;/var/lib/plex&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Adding an Audiobooks Plugin&lt;/h3&gt;
&lt;p&gt;I wanted to use the &lt;a href=&quot;https://github.com/macr0dev/Audiobooks.bundle&quot;&gt;Audiobooks.bundle&lt;/a&gt;
metadata agent for better matching, so I added this to the &lt;code&gt;let&lt;/code&gt;-section at the
top of &lt;code&gt;plex.nix&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let
  audiobooksPlugin = pkgs.stdenv.mkDerivation {
    name = &quot;Audiobooks.bundle&quot;;
    src = pkgs.fetchurl {
      url = https://github.com/macr0dev/Audiobooks.bundle/archive/9b1de6b66cd8fe11c7d27623d8579f43df9f8b86.zip;
      sha256 = &quot;539492e3b06fca2ceb5f0cb6c5e47462d38019317b242f6f74d55c3b2d5f6e1d&quot;;
    };
    buildInputs = [ pkgs.unzip ];
    installPhase = &quot;mkdir -p $out; cp -R * $out/&quot;;
  };
in
  # ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That fetches the commit &lt;code&gt;9b1de6b&lt;/code&gt; of the audiobooks plugin and makes sure that
the SHA256 is correct.&lt;/p&gt;
&lt;p&gt;Then I told Plex to use this plugin like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.plex.managePlugins = true;
services.plex.extraPlugins = [audiobooksPlugin];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re following along and get an error which says
&lt;code&gt;services.plex.managePlugins&lt;/code&gt; no longer has an effect, remove that line.&lt;/p&gt;
&lt;p&gt;At this point, after running &lt;code&gt;nixos-rebuild switch&lt;/code&gt; again, I was able to access
the Plex interface at &lt;code&gt;https://&amp;lt;domain or ip&amp;gt;:32400&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Plex needs an initial configuration, but only allows it if it&apos;s coming from a
local connection.
One way to do this is an SSH tunnel, which I opened like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ssh -L 32400:localhost:32400 user@domain-or-ip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I opened &lt;a href=&quot;http://localhost:32400/web&quot;&gt;http://localhost:32400/web&lt;/a&gt; in my local browser and set up Plex.&lt;/p&gt;
&lt;h2&gt;Configuring Nginx&lt;/h2&gt;
&lt;p&gt;I wanted a nice domain with HTTPS on 443 (instead of HTTP on port 32400), so I
set up &lt;a href=&quot;https://nginx.com&quot;&gt;Nginx&lt;/a&gt; with &lt;a href=&quot;https://letsencrypt.org&quot;&gt;Let&apos;s Encrypt&lt;/a&gt;
next.&lt;/p&gt;
&lt;p&gt;The first thing I did was setting &lt;code&gt;openFirewall&lt;/code&gt; to false in the Plex
configuration.
Then I allowed port 80 and 443 for HTTP and HTTPS and all the
&lt;a href=&quot;https://github.com/NixOS/nixpkgs/blob/nixos-21.11/nixos/modules/services/misc/plex.nix#L157-L160&quot;&gt;Plex ports&lt;/a&gt;
except for 32400 as we want to proxy the web interface through Nginx.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.plex = {
  openFirewall = false;
  # ...
};

networking.firewall = {
  allowedTCPPorts = [ 3005 8324 32469 80 443 ];
  allowedUDPPorts = [ 1900 5353 32410 32412 32413 32414 ];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I configured ACME:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;security.acme.acceptTerms = true;
security.acme.defaults.email = &quot;&amp;lt;your email&amp;gt;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default provider is Let&apos;s Encrypt, you can find their terms of service here:
&lt;a href=&quot;https://letsencrypt.org/repository/&quot;&gt;Policy and Legal Repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now it was time to add the Nginx service.
I used recommended settings and only PFS-enabled ciphers with AES256.
As this proxies Plex requests, I forwarded some headers as well.
Here&apos;s the code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.nginx = {
  enable = true;

  # Recommended settings
  recommendedGzipSettings = true;
  recommendedOptimisation = true;
  recommendedProxySettings = true;
  recommendedTlsSettings = true;

  # Only allow PFS-enabled ciphers with AES256
  sslCiphers = &quot;AES256+EECDH:AES256+EDH:!aNULL&quot;;

  virtualHosts = {
    &quot;&amp;lt;your domain&amp;gt;&quot; = {
      forceSSL = true;
      enableACME = true;
      extraConfig = &apos;&apos;
        # Some players don&apos;t reopen a socket and playback stops totally instead of resuming after an extended pause
        send_timeout 100m;
        # Plex headers
        proxy_set_header X-Plex-Client-Identifier $http_x_plex_client_identifier;
        proxy_set_header X-Plex-Device $http_x_plex_device;
        proxy_set_header X-Plex-Device-Name $http_x_plex_device_name;
        proxy_set_header X-Plex-Platform $http_x_plex_platform;
        proxy_set_header X-Plex-Platform-Version $http_x_plex_platform_version;
        proxy_set_header X-Plex-Product $http_x_plex_product;
        proxy_set_header X-Plex-Token $http_x_plex_token;
        proxy_set_header X-Plex-Version $http_x_plex_version;
        proxy_set_header X-Plex-Nocache $http_x_plex_nocache;
        proxy_set_header X-Plex-Provides $http_x_plex_provides;
        proxy_set_header X-Plex-Device-Vendor $http_x_plex_device_vendor;
        proxy_set_header X-Plex-Model $http_x_plex_model;
        # Buffering off send to the client as soon as the data is received from Plex.
        proxy_redirect off;
        proxy_buffering off;
      &apos;&apos;;
      locations.&quot;/&quot; = {
        proxyPass = &quot;http://localhost:32400&quot;;
        proxyWebsockets = true;
      };
    };
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re following along, make sure to replace &lt;code&gt;&amp;lt;your domain&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To secure things even further, I set some headers for every request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.nginx.commonHttpConfig = &apos;&apos;
  # Add HSTS header with preloading to HTTPS requests.
  # Adding this header to HTTP requests is discouraged
  map $scheme $hsts_header {
      https   &quot;max-age=31536000; includeSubdomains; preload&quot;;
  }
  add_header Strict-Transport-Security $hsts_header;
  # Enable CSP for your services.
  #add_header Content-Security-Policy &quot;script-src &apos;self&apos;; object-src &apos;none&apos;; base-uri &apos;none&apos;;&quot; always;
  # Minimize information leaked to other domains
  add_header &apos;Referrer-Policy&apos; &apos;origin-when-cross-origin&apos;;
  # Disable embedding as a frame
  add_header X-Frame-Options DENY;
  # Prevent injection of code in other mime types (XSS Attacks)
  add_header X-Content-Type-Options nosniff;
  # Enable XSS protection of the browser.
  # May be unnecessary when CSP is configured properly (see above)
  add_header X-XSS-Protection &quot;1; mode=block&quot;;
&apos;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, I ran &lt;code&gt;nixos-rebuild switch&lt;/code&gt; one last time to apply the configuration.
Then I opened &lt;code&gt;https://my-domain&lt;/code&gt; in a browser and started creating Plex
libraries.&lt;/p&gt;
&lt;h2&gt;How much does it cost?&lt;/h2&gt;
&lt;p&gt;The CPX11 costs €4,75/month with backups enabled, B2 costs $0.005/GB/month
storage + $0.01/GB downloaded.
Storage pricing depends heavily on the amount of media stored and the amount
of media downloaded.
I pay around €10/month for my setup.&lt;/p&gt;
&lt;h2&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;All that&apos;s left to do now is further configure NixOS to set a hostname,
timezone, installed packages like &lt;code&gt;htop&lt;/code&gt; and enabling
&lt;a href=&quot;https://nixos.org/manual/nixos/stable/index.html#sec-upgrading-automatic&quot;&gt;Automatic Upgrades&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In case that&apos;s useful for you, here is the
&lt;a href=&quot;https://github.com/bahlo/arne.me/blob/main/content/writing/plex-on-nixos/configuration.nix&quot;&gt;configuration.nix&lt;/a&gt;
from when I tested this blog post.&lt;/p&gt;
&lt;p&gt;If you discover an issue or have a question, please don&apos;t hesitate to
&lt;a href=&quot;mailto:hey@arne.me&quot;&gt;let me know&lt;/a&gt;, I&apos;m more than happy to help!&lt;/p&gt;
</content:encoded></item></channel></rss>