CCE

Self-Hosting on the Fediverse with Akkoma

Contents

Akkoma is a Fediverse/ActivityPub server forked from 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. My sites' content are federated using this Akkoma server and Feediverse.

INPROGRESS why run akkoma?

Why not Mastodon? features, customizing elixir v. ruby, frontend diversity, website boy,

Why not Pleroma? "isn't that the nazi software"

Why not GTS? recently had post edit support added

What else? i just want my site to have activitypub tbqh....

akkoma: init at 3.4.0 by illdefined · Pull Request #192285 · NixOS/nixpkgs

Akkoma on NixOS

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:

nix:tangle ~/nix/nixos/akkoma.nix:noweb yes
{ 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
    # included on its own as ARROYO_NIXOS_MODULE
    # ./akkoma-media-proxy.nix
    ./akkoma-wobservability.nix
    ./akkoma-pwa.nix
    ./akkoma-config.nix
    ./akkoma-mrf.nix
    ./akkoma-search.nix
  ];

  services.postgresql.ensureDatabases = ["akkoma"];
  # maybe have to run psql for migrations to pass:
  # ALTER DATABASE akkoma OWNER TO akkoma;
  services.postgresql.ensureUsers = [
    {
      name = "akkoma";
      ensureDBOwnership = true;
    }
  ];

  services.akkoma = {
    enable = true;
    package = myAkkoma;

    group = "akkoma";
    user = "akkoma";
  };
}

Akkoma Service Configuration

ref Configuration Cheat Sheet, none of this is particularly exciting.

nix:tangle ~/nix/nixos/akkoma-config.nix
{ config, pkgs, ... }:
{
  sops.secrets = {
    akkoma_dbpass.owner = "akkoma";
    akkoma_jwt_signer.owner = "akkoma";
    akkoma_liveview_salt.owner = "akkoma";
    akkoma_secret_key_base.owner = "akkoma";
    akkoma_signing_salt.owner = "akkoma";
    akkoma_vapid_private.owner = "akkoma";
    akkoma_vapid_public.owner = "akkoma";
  };

  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 posting hole";
      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;
      base_url = "https://i.notes.whatthefuck.computer";
    };

    ":pleroma"."Pleroma.Upload".base_url = "https://notes.whatthefuck.computer/media";
    ":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 = config.sops.secrets.akkoma_dbpass.path;
    ":joken".":default_signer"._secret = config.sops.secrets.akkoma_jwt_signer.path;
    ":pleroma"."Pleroma.Web.Endpoint" = {
      live_view.signing_salt._secret = config.sops.secrets.akkoma_liveview_salt.path;
      secret_key_base._secret = config.sops.secrets.akkoma_secret_key_base.path;
      signing_salt._secret = config.sops.secrets.akkoma_signing_salt.path;
    };
    ":web_push_encryption".":vapid_details" = {
      private_key._secret = config.sops.secrets.akkoma_vapid_private.path;
      public_key._secret = config.sops.secrets.akkoma_vapid_public.path;
      subject = "mailto:fedi@whatthefuck.computer";
    };
  };
}

System Users

I really would like to manage my uids and gids better, but alas.

nix:tangle ~/nix/nixos/akkoma-users.nix
{ 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.

nix:tangle ~/nix/nixos/akkoma-frontends.nix
{ 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

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.

nix:tangle ~/nix/nixos/akkoma-virtualhost.nix
{ ... }:

{
  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 @backend;
    '';
    locations."@backend" = {
      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 = ''
      try_files $uri @backend;
      auth_basic           "closed site";
      auth_basic_user_file /srv/nginx-htpasswd;
    '';
    addSSL = true;
    useACMEHost = "fontkeming.fail";
  };
}

Media Caching for Akkoma

Media is proxied and cached on both the wobserver and the edgeserver.

nix:tangle ~/nix/nixos/akkoma-media-proxy.nix
{ ... }:

{
  services.nginx.virtualHosts."i.notes.whatthefuck.computer" = {
    addSSL = true;
    useACMEHost = "fontkeming.fail";

    locations."/" = {
      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;

        proxy_cache akkoma_media_cache;

        # Decouple proxy and upstream responses
        proxy_buffering on;
        proxy_cache_lock on;
        proxy_ignore_client_abort on;

        # Default cache times for various responses
        proxy_cache_valid 200 1y;
        proxy_cache_valid 206 301 304 1h;

        # Allow serving of stale items
        proxy_cache_use_stale error timeout invalid_header updating;
      '';
    };
  };
  services.nginx.commonHttpConfig = ''
    proxy_cache_path /var/cache/nginx/cache/akkoma-media-cache
      levels= keys_zone=akkoma_media_cache:16m max_size=16g
      inactive=1y use_temp_path=off;
  '';
  systemd.tmpfiles.rules = [
    "d '/var/cache/nginx/cache/' 0775 nginx nginx 1y"
  ];
}

Static Files

I could just splat this on to the filesystem but no harm in having it in the Nix store:

html#+name: tos:noweb-ref tos
<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 [2024-05-07] 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>
nix:tangle ~/nix/nixos/akkoma-statics.nix:noweb yes
{ 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.

nix:tangle ~/nix/nixos/akkoma-wobservability.nix
{ pkgs, ... }:

{
  services.akkoma.config.":pleroma".":instance" = {
    export_prometheus_metrics = true;
  };

  services.prometheus.scrapeConfigs = [
    {
      job_name = "akkoma";
      metrics_path = "/api/v1/akkoma/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..

json:tangle ~/nix/files/notes-pwa.json
{
    "$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:

web:tangle ~/nix/files/notes-pwa.html
<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:

nix:tangle ~/nix/nixos/akkoma-pwa.nix
{ config, pkgs, ... }:

{
  system.activationScripts = let
    json = ../files/notes-pwa.json;
    html = ../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...

nix:tangle ~/nix/nixos/akkoma-search.nix
{ config, pkgs, lib, ... }:

{
  # disable meilisearch
  services.meilisearch = {
    enable = false;
    # noAnalytics = true;
    # masterKeyEnvironmentFile = "/srv/meilisearch/key.txt";
    # environment = "production";
  };

  # enable RUM index type
  # https://docs.akkoma.dev/stable/configuration/cheatsheet/#rum-indexing-for-full-text-search
  services.postgresql.extraPlugins = ps: with ps; [
    rum
  ];



  services.akkoma.config = {
    ":pleroma".":database" = {
      rum_enabled = true;
    };
    # config :pleroma, :database, rum_enabled: true

    ":pleroma"."Pleroma.Search" = {
      module = (pkgs.formats.elixirConf { }).lib.mkRaw  "Pleroma.Search.DatabaseSearch";
    };
  };
}

don't enable meilisearch unless you need it...

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 is

  • pleroma_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 whatever

  • systemctl restart akkoma-config && systemctl restart akkoma to bring the secret in to the configuration

  • pleroma_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.

nix:tangle ~/nix/nixos/akkoma-mrf.nix:noweb yes
{ 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.

lua#+name: quarantined_instances:eval arroyo:noweb-ref quarantined_instances:var tbl=blocked-domains
local lines = {}
for _, row in ipairs(tbl) do
  table.insert(lines, string.format('"%s"', row[1]))
end
return table.concat(lines, "\n")

This elixirConf stuff, and the declarative config model, is pretty slick. <nixpkgs/pkgs/pkgs-lib/formats.nix>

lua#+name: reject_instances:eval arroyo:noweb-ref reject_instances:var tbl=blocked-domains
local lines = {}
for _, row in ipairs(tbl) do
  table.insert(lines, string.format('(ecf.lib.mkTuple ["%s" "%s"])', row[1], row[2]))
end
return table.concat(lines, "\n")