diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index ac9ce95fe6f3..a1e7cf01882e 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1102,6 +1102,7 @@ ./services/video/rtsp-simple-server.nix ./services/video/unifi-video.nix ./services/wayland/cage.nix + ./services/web-apps/akkoma.nix ./services/web-apps/alps.nix ./services/web-apps/atlassian/confluence.nix ./services/web-apps/atlassian/crowd.nix diff --git a/nixos/modules/services/web-apps/akkoma.md b/nixos/modules/services/web-apps/akkoma.md new file mode 100644 index 000000000000..fc849be0c872 --- /dev/null +++ b/nixos/modules/services/web-apps/akkoma.md @@ -0,0 +1,332 @@ +# Akkoma {#module-services-akkoma} + +[Akkoma](https://akkoma.dev/) is a lightweight ActivityPub microblogging server forked from Pleroma. + +## Service configuration {#modules-services-akkoma-service-configuration} + +The Elixir configuration file required by Akkoma is generated automatically from +[{option}`services.akkoma.config`](options.html#opt-services.akkoma.config). Secrets must be +included from external files outside of the Nix store by setting the configuration option to +an attribute set containing the attribute {option}`_secret` – a string pointing to the file +containing the actual value of the option. + +For the mandatory configuration settings these secrets will be generated automatically if the +referenced file does not exist during startup, unless disabled through +[{option}`services.akkoma.initSecrets`](options.html#opt-services.akkoma.initSecrets). + +The following configuration binds Akkoma to the Unix socket `/run/akkoma/socket`, expecting to +be run behind a HTTP proxy on `fediverse.example.com`. + + +```nix +services.akkoma.enable = true; +services.akkoma.config = { + ":pleroma" = { + ":instance" = { + name = "My Akkoma instance"; + description = "More detailed description"; + email = "admin@example.com"; + registration_open = false; + }; + + "Pleroma.Web.Endpoint" = { + url.host = "fediverse.example.com"; + }; + }; +}; +``` + +Please refer to the [configuration cheat sheet](https://docs.akkoma.dev/stable/configuration/cheatsheet/) +for additional configuration options. + +## User management {#modules-services-akkoma-user-management} + +After the Akkoma service is running, the administration utility can be used to +[manage users](https://docs.akkoma.dev/stable/administration/CLI_tasks/user/). In particular an +administrative user can be created with + +```ShellSession +$ pleroma_ctl user new --admin --moderator --password +``` + +## Proxy configuration {#modules-services-akkoma-proxy-configuration} + +Although it is possible to expose Akkoma directly, it is common practice to operate it behind an +HTTP reverse proxy such as nginx. + +```nix +services.akkoma.nginx = { + enableACME = true; + forceSSL = true; +}; + +services.nginx = { + enable = true; + + clientMaxBodySize = "16m"; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; +}; +``` + +Please refer to [](#module-security-acme) for details on how to provision an SSL/TLS certificate. + +### Media proxy {#modules-services-akkoma-media-proxy} + +Without the media proxy function, Akkoma does not store any remote media like pictures or video +locally, and clients have to fetch them directly from the source server. + +```nix +# Enable nginx slice module distributed with Tengine +services.nginx.package = pkgs.tengine; + +# Enable media proxy +services.akkoma.config.":pleroma".":media_proxy" = { + enabled = true; + proxy_opts.redirect_on_failure = true; +}; + +# Adjust the persistent cache size as needed: +# Assuming an average object size of 128 KiB, around 1 MiB +# of memory is required for the key zone per GiB of cache. +# Ensure that the cache directory exists and is writable by nginx. +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; +''; + +services.akkoma.nginx = { + locations."/proxy" = { + proxyPass = "http://unix:/run/akkoma/socket"; + + extraConfig = '' + proxy_cache akkoma_media_cache; + + # Cache objects in slices of 1 MiB + slice 1m; + proxy_cache_key $host$uri$is_args$args$slice_range; + proxy_set_header Range $slice_range; + + # 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; + ''; + }; +}; +``` + +#### Prefetch remote media {#modules-services-akkoma-prefetch-remote-media} + +The following example enables the `MediaProxyWarmingPolicy` MRF policy which automatically +fetches all media associated with a post through the media proxy, as soon as the post is +received by the instance. + +```nix +services.akkoma.config.":pleroma".":mrf".policies = + map (pkgs.formats.elixirConf { }).lib.mkRaw [ + "Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy" +]; +``` + +#### Media previews {#modules-services-akkoma-media-previews} + +Akkoma can generate previews for media. + +```nix +services.akkoma.config.":pleroma".":media_preview_proxy" = { + enabled = true; + thumbnail_max_width = 1920; + thumbnail_max_height = 1080; +}; +``` + +## Frontend management {#modules-services-akkoma-frontend-management} + +Akkoma will be deployed with the `pleroma-fe` and `admin-fe` frontends by default. These can be +modified by setting +[{option}`services.akkoma.frontends`](options.html#opt-services.akkoma.frontends). + +The following example overrides the primary frontend’s default configuration using a custom +derivation. + +```nix +services.akkoma.frontends.primary.package = pkgs.runCommand "pleroma-fe" { + config = builtins.toJSON { + expertLevel = 1; + collapseMessageWithSubject = false; + stopGifs = false; + replyVisibility = "following"; + webPushHideIfCW = true; + 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.pleroma-fe} $out + + rm $out/static/config.json + jq -s add ${pkgs.akkoma-frontends.pleroma-fe}/static/config.json ${config} \ + >$out/static/config.json +''; +``` + +## Federation policies {#modules-services-akkoma-federation-policies} + +Akkoma comes with a number of modules to police federation with other ActivityPub instances. +The most valuable for typical users is the +[`:mrf_simple`](https://docs.akkoma.dev/stable/configuration/cheatsheet/#mrf_simple) module +which allows limiting federation based on instance hostnames. + +This configuration snippet provides an example on how these can be used. Choosing an adequate +federation policy is not trivial and entails finding a balance between connectivity to the rest +of the fediverse and providing a pleasant experience to the users of an instance. + + +```nix +services.akkoma.config.":pleroma" = with (pkgs.formats.elixirConf { }).lib; { + ":mrf".policies = map mkRaw [ + "Pleroma.Web.ActivityPub.MRF.SimplePolicy" + ]; + + ":mrf_simple" = { + # Tag all media as sensitive + media_nsfw = mkMap { + "nsfw.weird.kinky" = "Untagged NSFW content"; + }; + + # Reject all activities except deletes + reject = mkMap { + "kiwifarms.cc" = "Persistent harassment of users, no moderation"; + }; + + # Force posts to be visible by followers only + followers_only = mkMap { + "beta.birdsite.live" = "Avoid polluting timelines with Twitter posts"; + }; + }; +}; +``` + +## Upload filters {#modules-services-akkoma-upload-filters} + +This example strips GPS and location metadata from uploads, deduplicates them and anonymises the +the file name. + +```nix +services.akkoma.config.":pleroma"."Pleroma.Upload".filters = + map (pkgs.formats.elixirConf { }).lib.mkRaw [ + "Pleroma.Upload.Filter.Exiftool" + "Pleroma.Upload.Filter.Dedupe" + "Pleroma.Upload.Filter.AnonymizeFilename" + ]; +``` + +## Migration from Pleroma {#modules-services-akkoma-migration-pleroma} + +Pleroma instances can be migrated to Akkoma either by copying the database and upload data or by +pointing Akkoma to the existing data. The necessary database migrations are run automatically +during startup of the service. + +The configuration has to be copy‐edited manually. + +Depending on the size of the database, the initial migration may take a long time and exceed the +startup timeout of the system manager. To work around this issue one may adjust the startup timeout +{option}`systemd.services.akkoma.serviceConfig.TimeoutStartSec` or simply run the migrations +manually: + +```ShellSession +pleroma_ctl migrate +``` + +### Copying data {#modules-services-akkoma-migration-pleroma-copy} + +Copying the Pleroma data instead of re‐using it in place may permit easier reversion to Pleroma, +but allows the two data sets to diverge. + +First disable Pleroma and then copy its database and upload data: + +```ShellSession +# Create a copy of the database +nix-shell -p postgresql --run 'createdb -T pleroma akkoma' + +# Copy upload data +mkdir /var/lib/akkoma +cp -R --reflink=auto /var/lib/pleroma/uploads /var/lib/akkoma/ +``` + +After the data has been copied, enable the Akkoma service and verify that the migration has been +successful. If no longer required, the original data may then be deleted: + +```ShellSession +# Delete original database +nix-shell -p postgresql --run 'dropdb pleroma' + +# Delete original Pleroma state +rm -r /var/lib/pleroma +``` + +### Re‐using data {#modules-services-akkoma-migration-pleroma-reuse} + +To re‐use the Pleroma data in place, disable Pleroma and enable Akkoma, pointing it to the +Pleroma database and upload directory. + +```nix +# Adjust these settings according to the database name and upload directory path used by Pleroma +services.akkoma.config.":pleroma"."Pleroma.Repo".database = "pleroma"; +services.akkoma.config.":pleroma".":instance".upload_dir = "/var/lib/pleroma/uploads"; +``` + +Please keep in mind that after the Akkoma service has been started, any migrations applied by +Akkoma have to be rolled back before the database can be used again with Pleroma. This can be +achieved through `pleroma_ctl ecto.rollback`. Refer to the +[Ecto SQL documentation](https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Rollback.html) for +details. + +## Advanced deployment options {#modules-services-akkoma-advanced-deployment} + +### Confinement {#modules-services-akkoma-confinement} + +The Akkoma systemd service may be confined to a chroot with + +```nix +services.systemd.akkoma.confinement.enable = true; +``` + +Confinement of services is not generally supported in NixOS and therefore disabled by default. +Depending on the Akkoma configuration, the default confinement settings may be insufficient and +lead to subtle errors at run time, requiring adjustment: + +Use +[{option}`services.systemd.akkoma.confinement.packages`](options.html#opt-systemd.services._name_.confinement.packages) +to make packages available in the chroot. + +{option}`services.systemd.akkoma.serviceConfig.BindPaths` and +{option}`services.systemd.akkoma.serviceConfig.BindReadOnlyPaths` permit access to outside paths +through bind mounts. Refer to +[{manpage}`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=) +for details. + +### Distributed deployment {#modules-services-akkoma-distributed-deployment} + +Being an Elixir application, Akkoma can be deployed in a distributed fashion. + +This requires setting +[{option}`services.akkoma.dist.address`](options.html#opt-services.akkoma.dist.address) and +[{option}`services.akkoma.dist.cookie`](options.html#opt-services.akkoma.dist.cookie). The +specifics depend strongly on the deployment environment. For more information please check the +relevant [Erlang documentation](https://www.erlang.org/doc/reference_manual/distributed.html). diff --git a/nixos/modules/services/web-apps/akkoma.nix b/nixos/modules/services/web-apps/akkoma.nix new file mode 100644 index 000000000000..47ba53e42221 --- /dev/null +++ b/nixos/modules/services/web-apps/akkoma.nix @@ -0,0 +1,1086 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.akkoma; + ex = cfg.config; + db = ex.":pleroma"."Pleroma.Repo"; + web = ex.":pleroma"."Pleroma.Web.Endpoint"; + + isConfined = config.systemd.services.akkoma.confinement.enable; + hasSmtp = (attrByPath [ ":pleroma" "Pleroma.Emails.Mailer" "adapter" "value" ] null ex) == "Swoosh.Adapters.SMTP"; + + isAbsolutePath = v: isString v && substring 0 1 v == "/"; + isSecret = v: isAttrs v && v ? _secret && isAbsolutePath v._secret; + + absolutePath = with types; mkOptionType { + name = "absolutePath"; + description = "absolute path"; + descriptionClass = "noun"; + check = isAbsolutePath; + inherit (str) merge; + }; + + secret = mkOptionType { + name = "secret"; + description = "secret value"; + descriptionClass = "noun"; + check = isSecret; + nestedTypes = { + _secret = absolutePath; + }; + }; + + ipAddress = with types; mkOptionType { + name = "ipAddress"; + description = "IPv4 or IPv6 address"; + descriptionClass = "conjunction"; + check = x: str.check x && builtins.match "[.0-9:A-Fa-f]+" x != null; + inherit (str) merge; + }; + + elixirValue = let + elixirValue' = with types; + nullOr (oneOf [ bool int float str (attrsOf elixirValue') (listOf elixirValue') ]) // { + description = "Elixir value"; + }; + in elixirValue'; + + frontend = { + options = { + package = mkOption { + type = types.package; + description = mdDoc "Akkoma frontend package."; + example = literalExpression "pkgs.akkoma-frontends.pleroma-fe"; + }; + + name = mkOption { + type = types.nonEmptyStr; + description = mdDoc "Akkoma frontend name."; + example = "pleroma-fe"; + }; + + ref = mkOption { + type = types.nonEmptyStr; + description = mdDoc "Akkoma frontend reference."; + example = "stable"; + }; + }; + }; + + sha256 = builtins.hashString "sha256"; + + replaceSec = let + replaceSec' = { }@args: v: + if isAttrs v + then if v ? _secret + then if isAbsolutePath v._secret + then sha256 v._secret + else abort "Invalid secret path (_secret = ${v._secret})" + else mapAttrs (_: val: replaceSec' args val) v + else if isList v + then map (replaceSec' args) v + else v; + in replaceSec' { }; + + # Erlang/Elixir uses a somewhat special format for IP addresses + erlAddr = addr: fileContents + (pkgs.runCommand addr { + nativeBuildInputs = with pkgs; [ elixir ]; + code = '' + case :inet.parse_address('${addr}') do + {:ok, addr} -> IO.inspect addr + {:error, _} -> System.halt(65) + end + ''; + passAsFile = [ "code" ]; + } ''elixir "$codePath" >"$out"''); + + format = pkgs.formats.elixirConf { }; + configFile = format.generate "config.exs" + (replaceSec + (attrsets.updateManyAttrsByPath [{ + path = [ ":pleroma" "Pleroma.Web.Endpoint" "http" "ip" ]; + update = addr: + if isAbsolutePath addr + then format.lib.mkTuple + [ (format.lib.mkAtom ":local") addr ] + else format.lib.mkRaw (erlAddr addr); + }] cfg.config)); + + writeShell = { name, text, runtimeInputs ? [ ] }: + pkgs.writeShellApplication { inherit name text runtimeInputs; } + "/bin/${name}"; + + genScript = writeShell { + name = "akkoma-gen-cookie"; + runtimeInputs = with pkgs; [ coreutils util-linux ]; + text = '' + install -m 0400 \ + -o ${escapeShellArg cfg.user } \ + -g ${escapeShellArg cfg.group} \ + <(hexdump -n 16 -e '"%02x"' /dev/urandom) \ + "$RUNTIME_DIRECTORY/cookie" + ''; + }; + + copyScript = writeShell { + name = "akkoma-copy-cookie"; + runtimeInputs = with pkgs; [ coreutils ]; + text = '' + install -m 0400 \ + -o ${escapeShellArg cfg.user} \ + -g ${escapeShellArg cfg.group} \ + ${escapeShellArg cfg.dist.cookie._secret} \ + "$RUNTIME_DIRECTORY/cookie" + ''; + }; + + secretPaths = catAttrs "_secret" (collect isSecret cfg.config); + + vapidKeygen = pkgs.writeText "vapidKeygen.exs" '' + [public_path, private_path] = System.argv() + {public_key, private_key} = :crypto.generate_key :ecdh, :prime256v1 + File.write! public_path, Base.url_encode64(public_key, padding: false) + File.write! private_path, Base.url_encode64(private_key, padding: false) + ''; + + initSecretsScript = writeShell { + name = "akkoma-init-secrets"; + runtimeInputs = with pkgs; [ coreutils elixir ]; + text = let + key-base = web.secret_key_base; + jwt-signer = ex.":joken".":default_signer"; + signing-salt = web.signing_salt; + liveview-salt = web.live_view.signing_salt; + vapid-private = ex.":web_push_encryption".":vapid_details".private_key; + vapid-public = ex.":web_push_encryption".":vapid_details".public_key; + in '' + secret() { + # Generate default secret if non‐existent + test -e "$2" || install -D -m 0600 <(tr -dc 'A-Za-z-._~' &2 + exit 65 + fi + } + + secret 64 ${escapeShellArg key-base._secret} + secret 64 ${escapeShellArg jwt-signer._secret} + secret 8 ${escapeShellArg signing-salt._secret} + secret 8 ${escapeShellArg liveview-salt._secret} + + ${optionalString (isSecret vapid-public) '' + { test -e ${escapeShellArg vapid-private._secret} && \ + test -e ${escapeShellArg vapid-public._secret}; } || \ + elixir ${escapeShellArgs [ vapidKeygen vapid-public._secret vapid-private._secret ]} + ''} + ''; + }; + + configScript = writeShell { + name = "akkoma-config"; + runtimeInputs = with pkgs; [ coreutils replace-secret ]; + text = '' + cd "$RUNTIME_DIRECTORY" + tmp="$(mktemp config.exs.XXXXXXXXXX)" + trap 'rm -f "$tmp"' EXIT TERM + + cat ${escapeShellArg configFile} >"$tmp" + ${concatMapStrings (file: '' + replace-secret ${escapeShellArgs [ (sha256 file) file ]} "$tmp" + '') secretPaths} + + chown ${escapeShellArg cfg.user}:${escapeShellArg cfg.group} "$tmp" + chmod 0400 "$tmp" + mv -f "$tmp" config.exs + ''; + }; + + pgpass = let + esc = escape [ ":" ''\'' ]; + in if (cfg.initDb.password != null) + then pkgs.writeText "pgpass.conf" '' + *:*:*${esc cfg.initDb.username}:${esc (sha256 cfg.initDb.password._secret)} + '' + else null; + + escapeSqlId = x: ''"${replaceStrings [ ''"'' ] [ ''""'' ] x}"''; + escapeSqlStr = x: "'${replaceStrings [ "'" ] [ "''" ] x}'"; + + setupSql = pkgs.writeText "setup.psql" '' + \set ON_ERROR_STOP on + + ALTER ROLE ${escapeSqlId db.username} + LOGIN PASSWORD ${if db ? password + then "${escapeSqlStr (sha256 db.password._secret)}" + else "NULL"}; + + ALTER DATABASE ${escapeSqlId db.database} + OWNER TO ${escapeSqlId db.username}; + + \connect ${escapeSqlId db.database} + CREATE EXTENSION IF NOT EXISTS citext; + CREATE EXTENSION IF NOT EXISTS pg_trgm; + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + ''; + + dbHost = if db ? socket_dir then db.socket_dir + else if db ? socket then db.socket + else if db ? hostname then db.hostname + else null; + + initDbScript = writeShell { + name = "akkoma-initdb"; + runtimeInputs = with pkgs; [ coreutils replace-secret config.services.postgresql.package ]; + text = '' + pgpass="$(mktemp -t pgpass-XXXXXXXXXX.conf)" + setupSql="$(mktemp -t setup-XXXXXXXXXX.psql)" + trap 'rm -f "$pgpass $setupSql"' EXIT TERM + + ${optionalString (dbHost != null) '' + export PGHOST=${escapeShellArg dbHost} + ''} + export PGUSER=${escapeShellArg cfg.initDb.username} + ${optionalString (pgpass != null) '' + cat ${escapeShellArg pgpass} >"$pgpass" + replace-secret ${escapeShellArgs [ + (sha256 cfg.initDb.password._secret) cfg.initDb.password._secret ]} "$pgpass" + export PGPASSFILE="$pgpass" + ''} + + cat ${escapeShellArg setupSql} >"$setupSql" + ${optionalString (db ? password) '' + replace-secret ${escapeShellArgs [ + (sha256 db.password._secret) db.password._secret ]} "$setupSql" + ''} + + # Create role if non‐existent + psql -tAc "SELECT 1 FROM pg_roles + WHERE rolname = "${escapeShellArg (escapeSqlStr db.username)} | grep -F -q 1 || \ + psql -tAc "CREATE ROLE "${escapeShellArg (escapeSqlId db.username)} + + # Create database if non‐existent + psql -tAc "SELECT 1 FROM pg_database + WHERE datname = "${escapeShellArg (escapeSqlStr db.database)} | grep -F -q 1 || \ + psql -tAc "CREATE DATABASE "${escapeShellArg (escapeSqlId db.database)}" + OWNER "${escapeShellArg (escapeSqlId db.username)}" + TEMPLATE template0 + ENCODING 'utf8' + LOCALE 'C'" + + psql -f "$setupSql" + ''; + }; + + envWrapper = let + script = writeShell { + name = "akkoma-env"; + text = '' + cd "${cfg.package}" + + RUNTIME_DIRECTORY="''${RUNTIME_DIRECTORY:-/run/akkoma}" + AKKOMA_CONFIG_PATH="$RUNTIME_DIRECTORY/config.exs" \ + ERL_EPMD_ADDRESS="${cfg.dist.address}" \ + ERL_EPMD_PORT="${toString cfg.dist.epmdPort}" \ + ERL_FLAGS="${concatStringsSep " " [ + "-kernel inet_dist_use_interface '${erlAddr cfg.dist.address}'" + "-kernel inet_dist_listen_min ${toString cfg.dist.portMin}" + "-kernel inet_dist_listen_max ${toString cfg.dist.portMax}" + ]}" \ + RELEASE_COOKIE="$(<"$RUNTIME_DIRECTORY/cookie")" \ + RELEASE_NAME="akkoma" \ + exec "${cfg.package}/bin/$(basename "$0")" "$@" + ''; + }; + in pkgs.runCommandLocal "akkoma-env" { } '' + mkdir -p "$out/bin" + + ln -r -s ${escapeShellArg script} "$out/bin/pleroma" + ln -r -s ${escapeShellArg script} "$out/bin/pleroma_ctl" + ''; + + userWrapper = pkgs.writeShellApplication { + name = "pleroma_ctl"; + text = '' + if [ "''${1-}" == "update" ]; then + echo "OTP releases are not supported on NixOS." >&2 + exit 64 + fi + + exec sudo -u ${escapeShellArg cfg.user} \ + "${envWrapper}/bin/pleroma_ctl" "$@" + ''; + }; + + socketScript = if isAbsolutePath web.http.ip + then writeShell { + name = "akkoma-socket"; + runtimeInputs = with pkgs; [ coreutils inotify-tools ]; + text = '' + coproc { + inotifywait -q -m -e create ${escapeShellArg (dirOf web.http.ip)} + } + + trap 'kill "$COPROC_PID"' EXIT TERM + + until test -S ${escapeShellArg web.http.ip} + do read -r -u "''${COPROC[0]}" + done + + chmod 0666 ${escapeShellArg web.http.ip} + ''; + } + else null; + + staticDir = ex.":pleroma".":instance".static_dir; + uploadDir = ex.":pleroma".":instance".upload_dir; + + staticFiles = pkgs.runCommandLocal "akkoma-static" { } '' + ${concatStringsSep "\n" (mapAttrsToList (key: val: '' + mkdir -p $out/frontends/${escapeShellArg val.name}/ + ln -s ${escapeShellArg val.package} $out/frontends/${escapeShellArg val.name}/${escapeShellArg val.ref} + '') cfg.frontends)} + + ${optionalString (cfg.extraStatic != null) + (concatStringsSep "\n" (mapAttrsToList (key: val: '' + mkdir -p "$out/$(dirname ${escapeShellArg key})" + ln -s ${escapeShellArg val} $out/${escapeShellArg key} + '') cfg.extraStatic))} + ''; +in { + options = { + services.akkoma = { + enable = mkEnableOption (mdDoc "Akkoma"); + + package = mkOption { + type = types.package; + default = pkgs.akkoma; + defaultText = literalExpression "pkgs.akkoma"; + description = mdDoc "Akkoma package to use."; + }; + + user = mkOption { + type = types.nonEmptyStr; + default = "akkoma"; + description = mdDoc "User account under which Akkoma runs."; + }; + + group = mkOption { + type = types.nonEmptyStr; + default = "akkoma"; + description = mdDoc "Group account under which Akkoma runs."; + }; + + initDb = { + enable = mkOption { + type = types.bool; + default = true; + description = mdDoc '' + Whether to automatically initialise the database on startup. This will create a + database role and database if they do not already exist, and (re)set the role password + and the ownership of the database. + + This setting can be used safely even if the database already exists and contains data. + + The database settings are configured through + [{option}`config.services.akkoma.config.":pleroma"."Pleroma.Repo"`](#opt-services.akkoma.config.__pleroma_._Pleroma.Repo_). + + If disabled, the database has to be set up manually: + + ```SQL + CREATE ROLE akkoma LOGIN; + + CREATE DATABASE akkoma + OWNER akkoma + TEMPLATE template0 + ENCODING 'utf8' + LOCALE 'C'; + + \connect akkoma + CREATE EXTENSION IF NOT EXISTS citext; + CREATE EXTENSION IF NOT EXISTS pg_trgm; + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + ``` + ''; + }; + + username = mkOption { + type = types.nonEmptyStr; + default = config.services.postgresql.superUser; + defaultText = literalExpression "config.services.postgresql.superUser"; + description = mdDoc '' + Name of the database user to initialise the database with. + + This user is required to have the `CREATEROLE` and `CREATEDB` capabilities. + ''; + }; + + password = mkOption { + type = types.nullOr secret; + default = null; + description = mdDoc '' + Password of the database user to initialise the database with. + + If set to `null`, no password will be used. + + The attribute `_secret` should point to a file containing the secret. + ''; + }; + }; + + initSecrets = mkOption { + type = types.bool; + default = true; + description = mdDoc '' + Whether to initialise non‐existent secrets with random values. + + If enabled, appropriate secrets for the following options will be created automatically + if the files referenced in the `_secrets` attribute do not exist during startup. + + - {option}`config.":pleroma"."Pleroma.Web.Endpoint".secret_key_base` + - {option}`config.":pleroma"."Pleroma.Web.Endpoint".signing_salt` + - {option}`config.":pleroma"."Pleroma.Web.Endpoint".live_view.signing_salt` + - {option}`config.":web_push_encryption".":vapid_details".private_key` + - {option}`config.":web_push_encryption".":vapid_details".public_key` + - {option}`config.":joken".":default_signer"` + ''; + }; + + installWrapper = mkOption { + type = types.bool; + default = true; + description = mdDoc '' + Whether to install a wrapper around `pleroma_ctl` to simplify administration of the + Akkoma instance. + ''; + }; + + extraPackages = mkOption { + type = with types; listOf package; + default = with pkgs; [ exiftool ffmpeg_5-headless graphicsmagick-imagemagick-compat ]; + defaultText = literalExpression "with pkgs; [ exiftool graphicsmagick-imagemagick-compat ffmpeg_5-headless ]"; + example = literalExpression "with pkgs; [ exiftool imagemagick ffmpeg_5-full ]"; + description = mdDoc '' + List of extra packages to include in the executable search path of the service unit. + These are needed by various configurable components such as: + + - ExifTool for the `Pleroma.Upload.Filter.Exiftool` upload filter, + - ImageMagick for still image previews in the media proxy as well as for the + `Pleroma.Upload.Filters.Mogrify` upload filter, and + - ffmpeg for video previews in the media proxy. + ''; + }; + + frontends = mkOption { + description = mdDoc "Akkoma frontends."; + type = with types; attrsOf (submodule frontend); + default = { + primary = { + package = pkgs.akkoma-frontends.pleroma-fe; + name = "pleroma-fe"; + ref = "stable"; + }; + admin = { + package = pkgs.akkoma-frontends.admin-fe; + name = "admin-fe"; + ref = "stable"; + }; + }; + defaultText = literalExpression '' + { + primary = { + package = pkgs.akkoma-frontends.pleroma-fe; + name = "pleroma-fe"; + ref = "stable"; + }; + admin = { + package = pkgs.akkoma-frontends.admin-fe; + name = "admin-fe"; + ref = "stable"; + }; + } + ''; + }; + + extraStatic = mkOption { + type = with types; nullOr (attrsOf package); + description = mdDoc '' + Attribute set of extra packages to add to the static files directory. + + Do not add frontends here. These should be configured through + [{option}`services.akkoma.frontends`](#opt-services.akkoma.frontends). + ''; + default = null; + example = literalExpression '' + { + "emoji/blobs.gg" = pkgs.akkoma-emoji.blobs_gg; + "static/terms-of-service.html" = pkgs.writeText "terms-of-service.html" ''' + … + '''; + "favicon.png" = let + rev = "697a8211b0f427a921e7935a35d14bb3e32d0a2c"; + in pkgs.stdenvNoCC.mkDerivation { + name = "favicon.png"; + + src = pkgs.fetchurl { + url = "https://raw.githubusercontent.com/TilCreator/NixOwO/''${rev}/NixOwO_plain.svg"; + hash = "sha256-tWhHMfJ3Od58N9H5yOKPMfM56hYWSOnr/TGCBi8bo9E="; + }; + + nativeBuildInputs = with pkgs; [ librsvg ]; + + dontUnpack = true; + installPhase = ''' + rsvg-convert -o $out -w 96 -h 96 $src + '''; + }; + } + ''; + }; + + dist = { + address = mkOption { + type = ipAddress; + default = "127.0.0.1"; + description = mdDoc '' + Listen address for Erlang distribution protocol and Port Mapper Daemon (epmd). + ''; + }; + + epmdPort = mkOption { + type = types.port; + default = 4369; + description = mdDoc "TCP port to bind Erlang Port Mapper Daemon to."; + }; + + portMin = mkOption { + type = types.port; + default = 49152; + description = mdDoc "Lower bound for Erlang distribution protocol TCP port."; + }; + + portMax = mkOption { + type = types.port; + default = 65535; + description = mdDoc "Upper bound for Erlang distribution protocol TCP port."; + }; + + cookie = mkOption { + type = types.nullOr secret; + default = null; + example = { _secret = "/var/lib/secrets/akkoma/releaseCookie"; }; + description = mdDoc '' + Erlang release cookie. + + If set to `null`, a temporary random cookie will be generated. + ''; + }; + }; + + config = mkOption { + description = mdDoc '' + Configuration for Akkoma. The attributes are serialised to Elixir DSL. + + Refer to for + configuration options. + + Settings containing secret data should be set to an attribute set containing the + attribute `_secret` - a string pointing to a file containing the value the option + should be set to. + ''; + type = types.submodule { + freeformType = format.type; + options = { + ":pleroma" = { + ":instance" = { + name = mkOption { + type = types.nonEmptyStr; + description = mdDoc "Instance name."; + }; + + email = mkOption { + type = types.nonEmptyStr; + description = mdDoc "Instance administrator email."; + }; + + description = mkOption { + type = types.nonEmptyStr; + description = mdDoc "Instance description."; + }; + + static_dir = mkOption { + type = types.path; + default = toString staticFiles; + defaultText = literalMD '' + Derivation gathering the following paths into a directory: + + - [{option}`services.akkoma.frontends`](#opt-services.akkoma.frontends) + - [{option}`services.akkoma.extraStatic`](#opt-services.akkoma.extraStatic) + ''; + description = mdDoc '' + Directory of static files. + + This directory can be built using a derivation, or it can be managed as mutable + state by setting the option to an absolute path. + ''; + }; + + upload_dir = mkOption { + type = absolutePath; + default = "/var/lib/akkoma/uploads"; + description = mdDoc '' + Directory where Akkoma will put uploaded files. + ''; + }; + }; + + "Pleroma.Repo" = mkOption { + type = elixirValue; + default = { + adapter = format.lib.mkRaw "Ecto.Adapters.Postgres"; + socket_dir = "/run/postgresql"; + username = cfg.user; + database = "akkoma"; + }; + defaultText = literalExpression '' + { + adapter = (pkgs.formats.elixirConf { }).lib.mkRaw "Ecto.Adapters.Postgres"; + socket_dir = "/run/postgresql"; + username = config.services.akkoma.user; + database = "akkoma"; + } + ''; + description = mdDoc '' + Database configuration. + + Refer to + + for options. + ''; + }; + + "Pleroma.Web.Endpoint" = { + url = { + host = mkOption { + type = types.nonEmptyStr; + default = config.networking.fqdn; + defaultText = literalExpression "config.networking.fqdn"; + description = mdDoc "Domain name of the instance."; + }; + + scheme = mkOption { + type = types.nonEmptyStr; + default = "https"; + description = mdDoc "URL scheme."; + }; + + port = mkOption { + type = types.port; + default = 443; + description = mdDoc "External port number."; + }; + }; + + http = { + ip = mkOption { + type = types.either absolutePath ipAddress; + default = "/run/akkoma/socket"; + example = "::1"; + description = mdDoc '' + Listener IP address or Unix socket path. + + The value is automatically converted to Elixir’s internal address + representation during serialisation. + ''; + }; + + port = mkOption { + type = types.port; + default = if isAbsolutePath web.http.ip then 0 else 4000; + defaultText = literalExpression '' + if isAbsolutePath config.services.akkoma.config.:pleroma"."Pleroma.Web.Endpoint".http.ip + then 0 + else 4000; + ''; + description = mdDoc '' + Listener port number. + + Must be 0 if using a Unix socket. + ''; + }; + }; + + secret_key_base = mkOption { + type = secret; + default = { _secret = "/var/lib/secrets/akkoma/key-base"; }; + description = mdDoc '' + Secret key used as a base to generate further secrets for encrypting and + signing data. + + The attribute `_secret` should point to a file containing the secret. + + This key can generated can be generated as follows: + + ```ShellSession + $ tr -dc 'A-Za-z-._~' + for options. + ''; + }; + }; + }; + + ":tzdata" = { + ":data_dir" = mkOption { + type = elixirValue; + internal = true; + default = format.lib.mkRaw '' + Path.join(System.fetch_env!("CACHE_DIRECTORY"), "tzdata") + ''; + }; + }; + }; + }; + }; + + nginx = mkOption { + type = with types; nullOr (submodule + (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })); + default = null; + description = mdDoc '' + Extra configuration for the nginx virtual host of Akkoma. + + If set to `null`, no virtual host will be added to the nginx configuration. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + warnings = optionals (!config.security.sudo.enable) ['' + The pleroma_ctl wrapper enabled by the installWrapper option relies on + sudo, which appears to have been disabled through security.sudo.enable. + '']; + + users = { + users."${cfg.user}" = { + description = "Akkoma user"; + group = cfg.group; + isSystemUser = true; + }; + groups."${cfg.group}" = { }; + }; + + # Confinement of the main service unit requires separation of the + # configuration generation into a separate unit to permit access to secrets + # residing outside of the chroot. + systemd.services.akkoma-config = { + description = "Akkoma social network configuration"; + reloadTriggers = [ configFile ] ++ secretPaths; + + unitConfig.PropagatesReloadTo = [ "akkoma.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + UMask = "0077"; + + RuntimeDirectory = "akkoma"; + + ExecStart = mkMerge [ + (mkIf (cfg.dist.cookie == null) [ genScript ]) + (mkIf (cfg.dist.cookie != null) [ copyScript ]) + (mkIf cfg.initSecrets [ initSecretsScript ]) + [ configScript ] + ]; + + ExecReload = mkMerge [ + (mkIf cfg.initSecrets [ initSecretsScript ]) + [ configScript ] + ]; + }; + }; + + systemd.services.akkoma-initdb = mkIf cfg.initDb.enable { + description = "Akkoma social network database setup"; + requires = [ "akkoma-config.service" ]; + requiredBy = [ "akkoma.service" ]; + after = [ "akkoma-config.service" "postgresql.service" ]; + before = [ "akkoma.service" ]; + + serviceConfig = { + Type = "oneshot"; + User = mkIf (db ? socket_dir || db ? socket) + cfg.initDb.username; + RemainAfterExit = true; + UMask = "0077"; + ExecStart = initDbScript; + PrivateTmp = true; + }; + }; + + systemd.services.akkoma = let + runtimeInputs = with pkgs; [ coreutils gawk gnused ] ++ cfg.extraPackages; + in { + description = "Akkoma social network"; + documentation = [ "https://docs.akkoma.dev/stable/" ]; + + # This service depends on network-online.target and is sequenced after + # it because it requires access to the Internet to function properly. + bindsTo = [ "akkoma-config.service" ]; + wants = [ "network-online.service" ]; + wantedBy = [ "multi-user.target" ]; + after = [ + "akkoma-config.target" + "network.target" + "network-online.target" + "postgresql.service" + ]; + + confinement.packages = mkIf isConfined runtimeInputs; + path = runtimeInputs; + + serviceConfig = { + Type = "exec"; + User = cfg.user; + Group = cfg.group; + UMask = "0077"; + + # The run‐time directory is preserved as it is managed by the akkoma-config.service unit. + RuntimeDirectory = "akkoma"; + RuntimeDirectoryPreserve = true; + + CacheDirectory = "akkoma"; + + BindPaths = [ "${uploadDir}:${uploadDir}:norbind" ]; + BindReadOnlyPaths = mkMerge [ + (mkIf (!isStorePath staticDir) [ "${staticDir}:${staticDir}:norbind" ]) + (mkIf isConfined (mkMerge [ + [ "/etc/hosts" "/etc/resolv.conf" ] + (mkIf (isStorePath staticDir) (map (dir: "${dir}:${dir}:norbind") + (splitString "\n" (readFile ((pkgs.closureInfo { rootPaths = staticDir; }) + "/store-paths"))))) + (mkIf (db ? socket_dir) [ "${db.socket_dir}:${db.socket_dir}:norbind" ]) + (mkIf (db ? socket) [ "${db.socket}:${db.socket}:norbind" ]) + ])) + ]; + + ExecStartPre = "${envWrapper}/bin/pleroma_ctl migrate"; + ExecStart = "${envWrapper}/bin/pleroma start"; + ExecStartPost = socketScript; + ExecStop = "${envWrapper}/bin/pleroma stop"; + ExecStopPost = mkIf (isAbsolutePath web.http.ip) + "${pkgs.coreutils}/bin/rm -f '${web.http.ip}'"; + + ProtectProc = "noaccess"; + ProcSubset = "pid"; + ProtectSystem = mkIf (!isConfined) "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + PrivateIPC = true; + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + LockPersonality = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + + CapabilityBoundingSet = mkIf + (any (port: port > 0 && port < 1024) + [ web.http.port cfg.dist.epmdPort cfg.dist.portMin ]) + [ "CAP_NET_BIND_SERVICE" ]; + + NoNewPrivileges = true; + SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ]; + SystemCallArchitectures = "native"; + + DeviceAllow = null; + DevicePolicy = "closed"; + + # SMTP adapter uses dynamic port 0 binding, which is incompatible with bind address filtering + SocketBindAllow = mkIf (!hasSmtp) (mkMerge [ + [ "tcp:${toString cfg.dist.epmdPort}" "tcp:${toString cfg.dist.portMin}-${toString cfg.dist.portMax}" ] + (mkIf (web.http.port != 0) [ "tcp:${toString web.http.port}" ]) + ]); + SocketBindDeny = mkIf (!hasSmtp) "any"; + }; + }; + + systemd.tmpfiles.rules = [ + "d ${uploadDir} 0700 ${cfg.user} ${cfg.group} - -" + "Z ${uploadDir} ~0700 ${cfg.user} ${cfg.group} - -" + ]; + + environment.systemPackages = mkIf (cfg.installWrapper) [ userWrapper ]; + + services.nginx.virtualHosts = mkIf (cfg.nginx != null) { + ${web.url.host} = mkMerge [ cfg.nginx { + locations."/" = { + proxyPass = + if isAbsolutePath web.http.ip + then "http://unix:${web.http.ip}" + else if hasInfix ":" web.http.ip + then "http://[${web.http.ip}]:${toString web.http.port}" + else "http://${web.http.ip}:${toString web.http.port}"; + + proxyWebsockets = true; + recommendedProxySettings = true; + }; + }]; + }; + }; + + meta.maintainers = with maintainers; [ mvs ]; + meta.doc = ./akkoma.xml; +} diff --git a/nixos/modules/services/web-apps/akkoma.xml b/nixos/modules/services/web-apps/akkoma.xml new file mode 100644 index 000000000000..76e6b806f30f --- /dev/null +++ b/nixos/modules/services/web-apps/akkoma.xml @@ -0,0 +1,396 @@ + + Akkoma + + Akkoma is a + lightweight ActivityPub microblogging server forked from Pleroma. + +
+ Service configuration + + The Elixir configuration file required by Akkoma is generated + automatically from + . + Secrets must be included from external files outside of the Nix + store by setting the configuration option to an attribute set + containing the attribute – a string + pointing to the file containing the actual value of the option. + + + For the mandatory configuration settings these secrets will be + generated automatically if the referenced file does not exist + during startup, unless disabled through + . + + + The following configuration binds Akkoma to the Unix socket + /run/akkoma/socket, expecting to be run behind + a HTTP proxy on fediverse.example.com. + + +services.akkoma.enable = true; +services.akkoma.config = { + ":pleroma" = { + ":instance" = { + name = "My Akkoma instance"; + description = "More detailed description"; + email = "admin@example.com"; + registration_open = false; + }; + + "Pleroma.Web.Endpoint" = { + url.host = "fediverse.example.com"; + }; + }; +}; + + + Please refer to the + configuration + cheat sheet for additional configuration options. + +
+
+ User management + + After the Akkoma service is running, the administration utility + can be used to + manage + users. In particular an administrative user can be created + with + + +$ pleroma_ctl user new <nickname> <email> --admin --moderator --password <password> + +
+
+ Proxy configuration + + Although it is possible to expose Akkoma directly, it is common + practice to operate it behind an HTTP reverse proxy such as nginx. + + +services.akkoma.nginx = { + enableACME = true; + forceSSL = true; +}; + +services.nginx = { + enable = true; + + clientMaxBodySize = "16m"; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; +}; + + + Please refer to for + details on how to provision an SSL/TLS certificate. + +
+ Media proxy + + Without the media proxy function, Akkoma does not store any + remote media like pictures or video locally, and clients have to + fetch them directly from the source server. + + +# Enable nginx slice module distributed with Tengine +services.nginx.package = pkgs.tengine; + +# Enable media proxy +services.akkoma.config.":pleroma".":media_proxy" = { + enabled = true; + proxy_opts.redirect_on_failure = true; +}; + +# Adjust the persistent cache size as needed: +# Assuming an average object size of 128 KiB, around 1 MiB +# of memory is required for the key zone per GiB of cache. +# Ensure that the cache directory exists and is writable by nginx. +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; +''; + +services.akkoma.nginx = { + locations."/proxy" = { + proxyPass = "http://unix:/run/akkoma/socket"; + + extraConfig = '' + proxy_cache akkoma_media_cache; + + # Cache objects in slices of 1 MiB + slice 1m; + proxy_cache_key $host$uri$is_args$args$slice_range; + proxy_set_header Range $slice_range; + + # 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; + ''; + }; +}; + +
+ Prefetch remote media + + The following example enables the + MediaProxyWarmingPolicy MRF policy which + automatically fetches all media associated with a post through + the media proxy, as soon as the post is received by the + instance. + + +services.akkoma.config.":pleroma".":mrf".policies = + map (pkgs.formats.elixirConf { }).lib.mkRaw [ + "Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy" +]; + +
+
+ Media previews + + Akkoma can generate previews for media. + + +services.akkoma.config.":pleroma".":media_preview_proxy" = { + enabled = true; + thumbnail_max_width = 1920; + thumbnail_max_height = 1080; +}; + +
+
+
+
+ Frontend management + + Akkoma will be deployed with the pleroma-fe and + admin-fe frontends by default. These can be + modified by setting + . + + + The following example overrides the primary frontend’s default + configuration using a custom derivation. + + +services.akkoma.frontends.primary.package = pkgs.runCommand "pleroma-fe" { + config = builtins.toJSON { + expertLevel = 1; + collapseMessageWithSubject = false; + stopGifs = false; + replyVisibility = "following"; + webPushHideIfCW = true; + 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.pleroma-fe} $out + + rm $out/static/config.json + jq -s add ${pkgs.akkoma-frontends.pleroma-fe}/static/config.json ${config} \ + >$out/static/config.json +''; + +
+
+ Federation policies + + Akkoma comes with a number of modules to police federation with + other ActivityPub instances. The most valuable for typical users + is the + :mrf_simple + module which allows limiting federation based on instance + hostnames. + + + This configuration snippet provides an example on how these can be + used. Choosing an adequate federation policy is not trivial and + entails finding a balance between connectivity to the rest of the + fediverse and providing a pleasant experience to the users of an + instance. + + +services.akkoma.config.":pleroma" = with (pkgs.formats.elixirConf { }).lib; { + ":mrf".policies = map mkRaw [ + "Pleroma.Web.ActivityPub.MRF.SimplePolicy" + ]; + + ":mrf_simple" = { + # Tag all media as sensitive + media_nsfw = mkMap { + "nsfw.weird.kinky" = "Untagged NSFW content"; + }; + + # Reject all activities except deletes + reject = mkMap { + "kiwifarms.cc" = "Persistent harassment of users, no moderation"; + }; + + # Force posts to be visible by followers only + followers_only = mkMap { + "beta.birdsite.live" = "Avoid polluting timelines with Twitter posts"; + }; + }; +}; + +
+
+ Upload filters + + This example strips GPS and location metadata from uploads, + deduplicates them and anonymises the the file name. + + +services.akkoma.config.":pleroma"."Pleroma.Upload".filters = + map (pkgs.formats.elixirConf { }).lib.mkRaw [ + "Pleroma.Upload.Filter.Exiftool" + "Pleroma.Upload.Filter.Dedupe" + "Pleroma.Upload.Filter.AnonymizeFilename" + ]; + +
+
+ Migration from Pleroma + + Pleroma instances can be migrated to Akkoma either by copying the + database and upload data or by pointing Akkoma to the existing + data. The necessary database migrations are run automatically + during startup of the service. + + + The configuration has to be copy‐edited manually. + + + Depending on the size of the database, the initial migration may + take a long time and exceed the startup timeout of the system + manager. To work around this issue one may adjust the startup + timeout + + or simply run the migrations manually: + + +pleroma_ctl migrate + +
+ Copying data + + Copying the Pleroma data instead of re‐using it in place may + permit easier reversion to Pleroma, but allows the two data sets + to diverge. + + + First disable Pleroma and then copy its database and upload + data: + + +# Create a copy of the database +nix-shell -p postgresql --run 'createdb -T pleroma akkoma' + +# Copy upload data +mkdir /var/lib/akkoma +cp -R --reflink=auto /var/lib/pleroma/uploads /var/lib/akkoma/ + + + After the data has been copied, enable the Akkoma service and + verify that the migration has been successful. If no longer + required, the original data may then be deleted: + + +# Delete original database +nix-shell -p postgresql --run 'dropdb pleroma' + +# Delete original Pleroma state +rm -r /var/lib/pleroma + +
+
+ Re‐using data + + To re‐use the Pleroma data in place, disable Pleroma and enable + Akkoma, pointing it to the Pleroma database and upload + directory. + + +# Adjust these settings according to the database name and upload directory path used by Pleroma +services.akkoma.config.":pleroma"."Pleroma.Repo".database = "pleroma"; +services.akkoma.config.":pleroma".":instance".upload_dir = "/var/lib/pleroma/uploads"; + + + Please keep in mind that after the Akkoma service has been + started, any migrations applied by Akkoma have to be rolled back + before the database can be used again with Pleroma. This can be + achieved through pleroma_ctl ecto.rollback. + Refer to the + Ecto + SQL documentation for details. + +
+
+
+ Advanced deployment options +
+ Confinement + + The Akkoma systemd service may be confined to a chroot with + + +services.systemd.akkoma.confinement.enable = true; + + + Confinement of services is not generally supported in NixOS and + therefore disabled by default. Depending on the Akkoma + configuration, the default confinement settings may be + insufficient and lead to subtle errors at run time, requiring + adjustment: + + + Use + + to make packages available in the chroot. + + + + and + + permit access to outside paths through bind mounts. Refer to + systemd.exec5 + for details. + +
+
+ Distributed deployment + + Being an Elixir application, Akkoma can be deployed in a + distributed fashion. + + + This requires setting + + and + . + The specifics depend strongly on the deployment environment. For + more information please check the relevant + Erlang + documentation. + +
+
+