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_DOMAIN is 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 on mastodon.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.com in such a way it can serve @[email protected], set LOCAL_DOMAIN to example.com and WEB_DOMAIN to mastodon.example.com. This also requires additional configuration on the server hosting example.com to redirect requests from https://example.com/.well-known/webfinger to https://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.