complete-computing-environment/akkoma.org

18 KiB
Raw Permalink Blame History

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

{ 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"))