Cloudflare and NixOS Tips When Deploying a Personal Mastodon Server
For the most part I feel very much at home on the Hachyderm Mastodon server; it’s probably the best social media experience that I can remember having and I have had the pleasure of interacting with so many cool and impassioned people there.
Hachyderm implements the default 500 character post limit which is hard-coded into the Mastodon codebase and as of writing these, seems unlikely to ever be made configurable.
Every now and then, especially when adding summaries to long (1hr+) live programming videos that I share across the Fediverse, I come up against that limit.
At the end of last year, I had an idea: Why don’t I just self-host my own Mastodon instance that allows for posts that are longer than 500 chars, make longer posts on that account, and then boost them from my main account on Hachyderm?
Sounds easy, right? Well…
Using my domain⌗
I wanted to be able to use my current domain so that I could be looked up as
@[email protected]. This is actually
quite well documented and can be done by setting the
WEB_DOMAIN
environment variable.
WEB_DOMAINis an optional environment variable allowing the installation of Mastodon on one domain, while having the users’ handles on a different domain, e.g. addressing users as@[email protected]but accessing Mastodon onmastodon.example.com. This may be useful if your domain name is already used for a different website but you still want to use it as a Mastodon identifier because it looks better or shorter.To install Mastodon on
mastodon.example.comin such a way it can serve@[email protected], setLOCAL_DOMAINtoexample.comandWEB_DOMAINtomastodon.example.com. This also requires additional configuration on the server hostingexample.comto redirect requests fromhttps://example.com/.well-known/webfingertohttps://mastodon.example.com/.well-known/webfinger.
In my case, I set WEB_DOMAIN to social.lgug2z.com and LOCAL_DOMAIN to
lgug2z.com.
However, lgug2z.com currently hosts a Hugo website that is deployed to
Cloudflare Pages.
“No problem”, I thought to myself, “I’ll just set up a redirect rule.”
This was in fact, a big problem, as after hours of trying to debug federation issues between my new instance and other Mastodon servers, I realized that Cloudflare’s Redirect Rules do nothing on a URI path where a Cloudflare Pages site is deployed!
It would have been nice if this edge case was clearly documented somewhere by Cloudflare.
My solution for this was to just add a .well-known/webfinger file to the
static folder of my Hugo site and populate it with the JSON payload returned
from
https://social.lgug2z.com/.well-known/webfinger?resource=acct:[email protected].
While this is not a particularly elegant solution, it does the job for a
single-user Mastodon instance.
Deploying a custom build of Mastodon on NixOS⌗
As previously mentioned, the 500 character post limit is hard-coded in the Mastodon codebase.
There is a very detailed post about how to change the source code to set a higher character limit that is kept up to date by @[email protected], which helpfully includes git patch files.
I initially tried overriding pkgs.mastodon both directly in the service
definition and via a NixOS overlay to apply this patch, to no avail.
When I tried forking Mastodon and applying the latest patch to the v4.2.3
release, the patch application failed, so I just cut myself a release/v4.2.3
branch on my fork and made the changes
there.
I once again set an override, this time for src, to build Mastodon from this
revision on my fork, and managed to get a little bit further. Now, querying
https://social.lgug2z.com/api/v2/instance showed that
configuration.statuses.max_characters was indeed set to my new value of 5000,
but this was not reflected in the UI.
Digging around in my generated Caddyfile at /etc/caddy/caddy_config, showed
that although I was referencing cfg.package (instead of pkgs.mastodon) from
my NixOS module when configuring Caddy, I was still being routed to the
frontend files of pkgs.mastodon without my src override.
After a lot of trial, error and GitHub code searches prefixed with lang:nix,
I found some
code
by @[email protected] which suggested that in
addition to overriding src, I should also override mastodonModules in order
to ensure that the Mastodon UI would be served not from pkgs.mastodon, but
from my override.
This was indeed the missing piece, and once I also added an override for
mastodonModules.src, I was able to see the updated UI with the new 5000
maximum character post limit!
My mastodon.nix module⌗
If you are reading this article because you’re trying to achieve something similarap:w
,
here is my mastodon.nix module which uses Caddy as a reverse proxy to serve a
custom build of Mastodon on a subdomain (in my case, social.lgug2z.com) while
allowing your server to show up on the Fediverse as the root domain (in my
case, [email protected]).
{
  config,
  pkgs,
  domain,
  ...
}: let
  cfg = config.services.mastodon;
in {
  services.mastodon = {
    enable = true;
    package = pkgs.mastodon.overrideAttrs (_: let
      pname = "mastodon-lgug2z";
      src = pkgs.fetchFromGitHub {
        owner = "LGUG2Z";
        repo = "mastodon";
        # forked from v4.2.3 with max chars set to 5000
        rev = "b24adb69fa41a580fa2781a44661b7b707e3f765";
        hash = "sha256-BvcvUAcIW5lYT1gTrKsIVIbmDQpAE3KxOiLLWoUtYhw=";
      };
    in {
      inherit src pname;
      mastodonModules = pkgs.mastodon.mastodonModules.overrideAttrs (_: {
        inherit src;
        pname = "${pname}-modules";
      });
    });
    localDomain = "${domain}";
    extraConfig = {
      WEB_DOMAIN = "social.${domain}";
      SINGLE_USER_MODE = "true";
    };
    configureNginx = false;
    smtp.fromAddress = "";
    streamingProcesses = 1;
  };
  networking.firewall.allowedTCPPorts = [80 443];
  users.users.caddy.extraGroups = ["mastodon"];
  systemd.services.caddy.serviceConfig.ReadWriteDirectories = pkgs.lib.mkForce ["/var/lib/caddy" "/run/mastodon-web"];
  services.caddy = {
    enable = true;
    virtualHosts."${cfg.extraConfig.WEB_DOMAIN}".extraConfig = ''
      handle_path /system/* {
          file_server * {
              root /var/lib/mastodon/public-system
          }
      }
      handle /api/v1/streaming/* {
          reverse_proxy  unix//run/mastodon-streaming/streaming.socket
      }
      route * {
          file_server * {
          root ${cfg.package}/public
          pass_thru
          }
          reverse_proxy * unix//run/mastodon-web/web.socket
      }
      handle_errors {
          root * ${cfg.package}/public
          rewrite 500.html
          file_server
      }
      encode gzip
      header /* {
          Strict-Transport-Security "max-age=31536000;"
      }
      header /emoji/* Cache-Control "public, max-age=31536000, immutable"
      header /packs/* Cache-Control "public, max-age=31536000, immutable"
      header /system/accounts/avatars/* Cache-Control "public, max-age=31536000, immutable"
      header /system/media_attachments/files/* Cache-Control "public, max-age=31536000, immutable"
    '';
  };
}
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.