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_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 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.com
in such a way it can serve@[email protected]
, setLOCAL_DOMAIN
toexample.com
andWEB_DOMAIN
tomastodon.example.com
. This also requires additional configuration on the server hostingexample.com
to redirect requests fromhttps://example.com/.well-known/webfinger
tohttps://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.