18 KiB
Self-Hosting on the Fediverse with Akkoma
Akkoma is a Fediverse/ActivityPub server forked from roam:Pleroma written in Elixir, supporting the Mastodon Server API. This is a light-weight thing and I intend to self-publish to the Fediverse with an instance running on The Wobserver.
akkoma: init at 3.4.0 by illdefined · Pull Request #192285 · NixOS/nixpkgs
[2022-12-02 Fri 12:22]
Akkoma on NixOS
CLOCK: [2022-12-02 Fri 12:22]–[2022-12-02 Fri 16:24] => 4:02
The configuration interface in NixOS is nicer but also quite complicated. I have this broken up a bit and it consists of most of the configuration but not things like emoji and some other static assets like my favicon and whatnot.
It's not super complicated but we'll break it up in to multiple imports so that I can explain what is going on a bit:
myAkkoma
carries an unreleased patch to skip over the Pleroma observability rules – i need to fix the Wobservability section below to use this anyways, but I couldn't turn off the observability rules because they were in the DB configuration. Oughtta make sure I move that stuff out in to config.exs
fairly often.
{ config, pkgs, lib, ... }:
let
myAkkoma = pkgs.akkoma.overrideAttrs (old: old // {
# patches = [
# (<arroyo/files/akkoma-fix-pleroma-migration.patch>)
# ];
});
in{
imports = [
./akkoma-users.nix
./akkoma-statics.nix
./akkoma-frontends.nix
./akkoma-virtualhost.nix
./akkoma-wobservability.nix
./akkoma-pwa.nix
./akkoma-config.nix
./akkoma-mrf.nix
./akkoma-search.nix
];
services.postgresql.ensureDatabases = ["akkoma"];
# have to run psql for migrations to pass:
# ALTER DATABASE akkoma OWNER TO akkoma;
services.postgresql.ensureUsers = [
{
name = "akkoma";
ensurePermissions = {
"DATABASE akkoma" = "ALL PRIVILEGES";
};
}
];
services.akkoma = {
enable = true;
package = myAkkoma;
group = "akkoma";
user = "akkoma";
};
}
Akkoma Service Configuration
ref Configuration Cheat Sheet, none of this is particularly exciting.
{ config, pkgs, ... }:
{
services.akkoma.extraPackages = with pkgs; [exiftool ffmpeg-headless imagemagick];
# don't feel like setting password._secret here...
services.akkoma.initDb.enable = false;
services.akkoma.config = {
":pleroma".":instance" = {
name = "Computers :(";
description = "rrix's fediverse home";
email = "fedi@whatthefuck.computer";
registrations_open = false;
limit = 5000;
local_bubble = [
"botsin.space"
"hackers.town"
"tenforward.social"
"regenerate.social"
];
export_prometheus_metrics = true;
static_dir = "/srv/akkoma/static";
upload_dir = "/srv/akkoma/uploads";
};
":pleroma".":static_fe".enabled = true;
":pleroma"."Pleroma.Repo" = {
adapter = (pkgs.formats.elixirConf { }).lib.mkRaw "Ecto.Adapters.Postgres";
username = config.services.akkoma.user;
database = "akkoma";
hostname = "localhost";
timeout = 30000;
prepare = (pkgs.formats.elixirConf { }).lib.mkRaw ":named";
parameters = {
plan_cache_mode = "force_custom_plan";
};
};
":pleroma".":configurable_from_database" = true;
":pleroma".":media_proxy" = {
enabled = true;
proxy_opts.redirect_on_failure = true;
};
":pleroma"."Pleroma.Upload".filters =
map (pkgs.formats.elixirConf { }).lib.mkRaw [
"Pleroma.Upload.Filter.Exiftool"
"Pleroma.Upload.Filter.Dedupe"
"Pleroma.Upload.Filter.AnonymizeFilename"
];
":pleroma"."Pleroma.Web.Endpoint" = {
http = {
ip = "127.0.0.1";
port = 4000;
};
url = {
host = "notes.whatthefuck.computer";
scheme = "https";
port = 443;
};
};
# secrets
":pleroma"."Pleroma.Repo".password._secret = "/srv/akkoma/dbpass";
":joken".":default_signer"._secret = "/srv/akkoma/jwt-signer";
":pleroma"."Pleroma.Web.Endpoint" = {
live_view.signing_salt._secret = "/srv/akkoma/liveview-salt";
secret_key_base._secret = "/srv/akkoma/secret-key-base";
signing_salt._secret = "/srv/akkoma/signing-salt";
};
":web_push_encryption".":vapid_details" = {
private_key._secret = "/srv/akkoma/vapid-private";
public_key._secret = "/srv/akkoma/vapid-public";
subject = "mailto:fedi@whatthefuck.computer";
};
};
}
System Users
I really would like to manage my uids and gids better, but alas.
{ config, pkgs, ... }:
{
ids.uids.akkoma = 901;
ids.gids.akkoma = 901;
users.groups.akkoma = {
gid = config.ids.gids.akkoma;
};
users.users.akkoma = {
group = "akkoma";
uid = config.ids.uids.akkoma;
shell = "${pkgs.bash}/bin/bash";
isSystemUser = true;
# ugh... services.pleroma.stateDir is readonly
home = "/var/lib/pleroma";
createHome = true;
};
}
Frontend Management
so if you set a static_dir
to a mutable directory on the server, the NixOS module won't install the frontends you ask for. This is good and fine, and not a surprise, certainly not undocumented. So what if you just:
pleroma_ctl frontend install admin-fe --ref stable
pleroma_ctl frontend install fedibird-fe --ref akkoma
pleroma_ctl frontend install pleroma-fe --ref stable
That fails because BindReadOnlyPaths
is set in the NixOS module as part of the hardening of the service. I shouldn't mind, but ** (File.Error) could not make directory (with -p) "/srv/akkoma/static/frontends/tmp": read-only file system
so, okay, make sure BindReadOnlyPaths
is not set, and set ReadWritePaths
just to be sure.
In theory it'd be possible to use a system.activationScripts
entry to take frontends.$foo.package
and slam a symlink in like I do for the terms of service above, but i'm tired.
{ config, pkgs, lib, ... }:
{
systemd.services.akkoma.serviceConfig.ReadWritePaths = [ config.services.akkoma.config.":pleroma".":instance".static_dir ];
systemd.services.akkoma.serviceConfig.BindReadOnlyPaths = lib.mkForce [ ];
services.akkoma.frontends = {
primary = {
name = "pleroma-fe";
ref = "stable";
};
mastodon= {
name = "fedibird-fe";
ref = "akkoma";
};
admin = {
name = "admin-fe";
ref = "stable";
};
};
# shove this thing in a system.activationScript to symlink to static_dir/frontends
# frontends.primary.package = pkgs.runCommand "akkoma-fe" {
# config = builtins.toJSON {
# expertLevel = 1;
# collapseMessageWithSubject = false;
# replyVisibility = "following";
# hideScopeNotice = true;
# renderMisskeyMarkdown = false;
# hideSiteFavicon = true;
# postContentType = "text/markdown";
# showNavShortcuts = false;
# };
# nativeBuildInputs = with pkgs; [ jq xorg.lndir ];
# passAsFile = [ "config" ];
# } ''
# mkdir $out
# lndir ${pkgs.akkoma-frontends.akkoma-fe} $out
#
# rm $out/static/config.json
# jq -s add ${pkgs.akkoma-frontends.akkoma-fe}/static/config.json ${config} \
# >$out/static/config.json
# '';
}
Nginx Frontend for Akkoma
Nothing special here – I have it split in to two blocks here because one of my old iterations of The Arcology Project used this domain to host short-form IndieWeb/microformat notes. The old files still exist and can be resolved in the try_files
block, and any failures will proxy through to the app backend. I also adjust the max body size for image uploads, etc. I might replace that with S3 in the future but for now the images can just go on to the file system.
{ ... }:
{
services.nginx.virtualHosts."notes.whatthefuck.computer" = {
root = "/srv/static-sites/notes.whatthefuck.computer/_site";
extraConfig = ''
client_max_body_size 100M;
'';
locations."/".extraConfig = ''
try_files $uri @proxy;
'';
locations."@proxy" = {
proxyPass = "http://127.0.0.1:4000";
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
'';
};
locations."/phoenix/live_dashboard".extraConfig = ''
auth_basic "closed site";
auth_basic_user_file /etc/nginx-htpasswd;
'';
};
}
Static Files
I could just splat this on to the filesystem but no harm in having it in the Nix store:
<p>
Now look; this is a single-user
instance. <a href="https://notes.whatthefuck.computer/rrix">rrix</a>
posts inane bullshit here. Look at their profile if you care about
what is going on here.
</p>
<p>
<ul>
<li>I'm not a fascist.</li>
<li>I'm not a cop.</li>
<li>I'm not a narc.</li>
<li>I'm not a racist.</li>
<li>I'm not a transphobe.</li>
<li>I'm not gonna put up with bullshit.</li>
<li>I'm just a little computer goblin who wants to self-host.</li>
</ul>
</p>
<p>
If you care about the privacy policy of this instance, don't
federate with it. rrix is a consummate privacy professional, but
they're also just one person. I have no intention to do anything
untoward with posts federated to my instance, nor engage in
non-standard behavior on the fediverse, the NixOS code which deploys
all the software on this server
is <a href="https://cce.whatthefuck.computer/akkoma">available
online</a>.
</p>
<p>
At the same time, I'm likely not going to be able to go
up against government requests for data stored on this instace. As
of [2023-02-16] this instance has not been compelled to give data to
any government or law enforcement agency and has not done so
voluntarily. I'm just one homie hanging out making posts with my
friends and trying to make new ones, and you're here reading
this. What's up?
</p>
{ config, pkgs, ... }:
let
tos = pkgs.writeTextFile {
name="pleroma-terms-of-service";
text = ''
<<tos>
'';
};
in
{
# services.akkoma.extraStatic."static/terms-of-service.html" = tos;
system.activationScripts.install-pleroma-tos.text = ''
echo "Installing Pleroma Terms of Service to static directory"
export DEST_DIR=/srv/akkoma/static/
mkdir -p $DEST_DIR
ln -sf ${tos} $DEST_DIR/static/terms-of-service.html
'';
}
Wobservability
I would like to have some usage metrics emitted, this is just service-level stuff:
Enable Pleroma.Web.Endpoint.MetricsExporter
in settings.
{ ... }:
{
services.prometheus.scrapeConfigs = [
{
job_name = "akkoma";
metrics_path = "/api/pleroma/app_metrics";
static_configs = [{ targets = [ "127.0.0.1:4000" ]; }];
}
];
}
PWA manifest for Pleroma-FE
Pleroma's mute filters, etc aren't 1-1 compatible with Mastodon API so they don't work in Mastodon-FE or Subway Tooter… This is …unfortunate. Let's see how using Pleroma-FE as a Progressive Web App goes..
{
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
"name": "Fedi Notes",
"short_name": "Fedi Notes",
"start_url": "https://notes.whatthefuck.computer/",
"display": "standalone",
"background_color": "#004daa",
"theme_color": "#31363b",
"description": "Pleroma-FE on notes.whatthefuck.computer",
"icons": [{
"src": "https://notes.whatthefuck.computer/media/e8ecd309a5e3d99736beff056fb461ba157dc4ff9317f3cc6063c4ce280bd604.blob",
"sizes": "312x312",
"type": "image/png"
}]
}
This needs to be shoved in to <head>
and there's no way to do that directly in Pleroma-FE so I make a silly little HTML page and shove it on to a virtualhost:
<html>
<head>
<link rel="manifest" href="notes-pwa.json" />
</head>
<body>
<h1>Pleroma-FE as PWA</h1>
<p>
On browsers which support "add to home screen" or similar
things, you can create a Progressive Web App which will
load the <code>Computer :(</code> site in a dedicated frame.
</p>
</body>
</html>
And this is installed, like so:
{ config, pkgs, ... }:
{
system.activationScripts = let
json = <arroyo/files/notes-pwa.json>;
html = <arroyo/files/notes-pwa.html>;
in {
install-pleroma-tos.text = ''
echo "Installing Pleroma PWA manifest and static page"
export DEST_DIR=/srv/static-sites/default
mkdir -p $DEST_DIR
ln -sf ${html} $DEST_DIR/pwa.html
ln -sf ${json} $DEST_DIR/notes-pwa.json
'';
};
services.nginx.virtualHosts."fontkeming.fail".extraConfig = ''
disable_symlinks off;
'';
}
## why 404?
Enable meilisearch
for Full Text Search of Toots
The Postgres FTS in Pleroma seemed like it worked better than the one in Akkoma? Very strange, to me. meilisearch
is an elasticsearch-alike that is hopefully less shitty than elasticsearch.
This is configured based on the docs, and Akkoma's secret handling…
{ config, pkgs, lib, ... }:
{
services.meilisearch = {
enable = true;
noAnalytics = true;
masterKeyEnvironmentFile = "/srv/meilisearch/key.txt";
environment = "production";
};
services.akkoma.config = {
":pleroma"."Pleroma.Search" = {
module = (pkgs.formats.elixirConf { }).lib.mkRaw "Pleroma.Search.Meilisearch";
};
":pleroma"."Pleroma.Search.Meilisearch" = {
url = "http://127.0.0.1:7700";
private_key._secret = "/srv/akkoma/meilisearch_key";
};
};
}
Okay so I updated my NixOS from 23.05 to 23.11 today and Meilisearch failed to start because the DB is incompatible between versions. You need a running instance to curl
the dump endpoint, update, and then reimport. I decided to blow away the DB instead and reindex:
rm /var/lib/meilisearch/*
systemctl start meilisearch
, look in the journal to see what the new master key ispleroma_ctl search.meilisearch show-keys $THE_ADMIN_KEY
and take the "use for all operations" key and stick it in/srv/akkoma/meilisearch_key
or whateversystemctl restart akkoma-config && systemctl restart akkoma
to bring the secret in to the configurationpleroma_ctl search.meilisearch index
NEXT Akkoma Moderation Rules
the new Message Rewrite Facility x fediblock dropped. I don't see a lot of this stuff and part of me thinks that having it boosted in to my TWKN or even home timeline is a great signal that I should be unfollowing whoever is bringing the filth in to my instance, but also I want to respect my own sanity.
{ pkgs, ... }:
let ecf = pkgs.formats.elixirConf {};
in
{
services.akkoma.config = {
":pleroma".":instance" = {
quarantined_instances = [
<<quarantined_instances()>>
];
};
":pleroma".":mrf".policies = ecf.lib.mkRaw "Pleroma.Web.ActivityPub.MRF.SimplePolicy";
":pleroma".":mrf_simple".reject = [
<<reject_instances()>>
];
};
}
These two functions generate those from a table I don't export.
(->> tbl
(-map #'first)
(--map (format "\"%s\"" it))
(s-join "\n"))
This elixirConf stuff, and the declarative config model, is pretty slick. <nixpkgs/pkgs/pkgs-lib/formats.nix>
(->> tbl
(--map (format "(ecf.lib.mkTuple [\"%s\" \"%s\"])" (first it) (second it)))
(s-join "\n"))