1 { config, lib, pkgs, ... }:
5 cfg = config.services.akkoma;
7 db = ex.":pleroma"."Pleroma.Repo";
8 web = ex.":pleroma"."Pleroma.Web.Endpoint";
10 isConfined = config.systemd.services.akkoma.confinement.enable;
11 hasSmtp = (attrByPath [ ":pleroma" "Pleroma.Emails.Mailer" "adapter" "value" ] null ex) == "Swoosh.Adapters.SMTP";
13 isAbsolutePath = v: isString v && substring 0 1 v == "/";
14 isSecret = v: isAttrs v && v ? _secret && isAbsolutePath v._secret;
16 absolutePath = with types; mkOptionType {
17 name = "absolutePath";
18 description = "absolute path";
19 descriptionClass = "noun";
20 check = isAbsolutePath;
24 secret = mkOptionType {
26 description = "secret value";
27 descriptionClass = "noun";
30 _secret = absolutePath;
34 ipAddress = with types; mkOptionType {
36 description = "IPv4 or IPv6 address";
37 descriptionClass = "conjunction";
38 check = x: str.check x && builtins.match "[.0-9:A-Fa-f]+" x != null;
43 elixirValue' = with types;
44 nullOr (oneOf [ bool int float str (attrsOf elixirValue') (listOf elixirValue') ]) // {
45 description = "Elixir value";
53 description = "Akkoma frontend package.";
54 example = literalExpression "pkgs.akkoma-frontends.akkoma-fe";
58 type = types.nonEmptyStr;
59 description = "Akkoma frontend name.";
60 example = "akkoma-fe";
64 type = types.nonEmptyStr;
65 description = "Akkoma frontend reference.";
71 sha256 = builtins.hashString "sha256";
74 replaceSec' = { }@args: v:
77 then if isAbsolutePath v._secret
79 else abort "Invalid secret path (_secret = ${v._secret})"
80 else mapAttrs (_: val: replaceSec' args val) v
82 then map (replaceSec' args) v
86 # Erlang/Elixir uses a somewhat special format for IP addresses
87 erlAddr = addr: fileContents
88 (pkgs.runCommand addr {
89 nativeBuildInputs = [ cfg.package.elixirPackage ];
91 case :inet.parse_address('${addr}') do
92 {:ok, addr} -> IO.inspect addr
93 {:error, _} -> System.halt(65)
96 passAsFile = [ "code" ];
97 } ''elixir "$codePath" >"$out"'');
99 format = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; };
100 configFile = format.generate "config.exs"
102 (attrsets.updateManyAttrsByPath [{
103 path = [ ":pleroma" "Pleroma.Web.Endpoint" "http" "ip" ];
105 if isAbsolutePath addr
106 then format.lib.mkTuple
107 [ (format.lib.mkAtom ":local") addr ]
108 else format.lib.mkRaw (erlAddr addr);
111 writeShell = { name, text, runtimeInputs ? [ ] }:
112 pkgs.writeShellApplication { inherit name text runtimeInputs; } + "/bin/${name}";
114 genScript = writeShell {
115 name = "akkoma-gen-cookie";
116 runtimeInputs = with pkgs; [ coreutils util-linux ];
119 -o ${escapeShellArg cfg.user } \
120 -g ${escapeShellArg cfg.group} \
121 <(hexdump -n 16 -e '"%02x"' /dev/urandom) \
122 "''${RUNTIME_DIRECTORY%%:*}/cookie"
126 copyScript = writeShell {
127 name = "akkoma-copy-cookie";
128 runtimeInputs = with pkgs; [ coreutils ];
131 -o ${escapeShellArg cfg.user} \
132 -g ${escapeShellArg cfg.group} \
133 ${escapeShellArg cfg.dist.cookie._secret} \
134 "''${RUNTIME_DIRECTORY%%:*}/cookie"
138 secretPaths = catAttrs "_secret" (collect isSecret cfg.config);
140 vapidKeygen = pkgs.writeText "vapidKeygen.exs" ''
141 [public_path, private_path] = System.argv()
142 {public_key, private_key} = :crypto.generate_key :ecdh, :prime256v1
143 File.write! public_path, Base.url_encode64(public_key, padding: false)
144 File.write! private_path, Base.url_encode64(private_key, padding: false)
147 initSecretsScript = writeShell {
148 name = "akkoma-init-secrets";
149 runtimeInputs = with pkgs; [ coreutils cfg.package.elixirPackage ];
151 key-base = web.secret_key_base;
152 jwt-signer = ex.":joken".":default_signer";
153 signing-salt = web.signing_salt;
154 liveview-salt = web.live_view.signing_salt;
155 vapid-private = ex.":web_push_encryption".":vapid_details".private_key;
156 vapid-public = ex.":web_push_encryption".":vapid_details".public_key;
159 # Generate default secret if non‐existent
160 test -e "$2" || install -D -m 0600 <(tr -dc 'A-Za-z-._~' </dev/urandom | head -c "$1") "$2"
161 if [ "$(stat --dereference --format='%s' "$2")" -lt "$1" ]; then
162 echo "Secret '$2' is smaller than minimum size of $1 bytes." >&2
167 secret 64 ${escapeShellArg key-base._secret}
168 secret 64 ${escapeShellArg jwt-signer._secret}
169 secret 8 ${escapeShellArg signing-salt._secret}
170 secret 8 ${escapeShellArg liveview-salt._secret}
172 ${optionalString (isSecret vapid-public) ''
173 { test -e ${escapeShellArg vapid-private._secret} && \
174 test -e ${escapeShellArg vapid-public._secret}; } || \
175 elixir ${escapeShellArgs [ vapidKeygen vapid-public._secret vapid-private._secret ]}
180 configScript = writeShell {
181 name = "akkoma-config";
182 runtimeInputs = with pkgs; [ coreutils replace-secret ];
184 cd "''${RUNTIME_DIRECTORY%%:*}"
185 tmp="$(mktemp config.exs.XXXXXXXXXX)"
186 trap 'rm -f "$tmp"' EXIT TERM
188 cat ${escapeShellArg configFile} >"$tmp"
189 ${concatMapStrings (file: ''
190 replace-secret ${escapeShellArgs [ (sha256 file) file ]} "$tmp"
193 chown ${escapeShellArg cfg.user}:${escapeShellArg cfg.group} "$tmp"
195 mv -f "$tmp" config.exs
200 esc = escape [ ":" ''\'' ];
201 in if (cfg.initDb.password != null)
202 then pkgs.writeText "pgpass.conf" ''
203 *:*:*${esc cfg.initDb.username}:${esc (sha256 cfg.initDb.password._secret)}
207 escapeSqlId = x: ''"${replaceStrings [ ''"'' ] [ ''""'' ] x}"'';
208 escapeSqlStr = x: "'${replaceStrings [ "'" ] [ "''" ] x}'";
210 setupSql = pkgs.writeText "setup.psql" ''
211 \set ON_ERROR_STOP on
213 ALTER ROLE ${escapeSqlId db.username}
214 LOGIN PASSWORD ${if db ? password
215 then "${escapeSqlStr (sha256 db.password._secret)}"
218 ALTER DATABASE ${escapeSqlId db.database}
219 OWNER TO ${escapeSqlId db.username};
221 \connect ${escapeSqlId db.database}
222 CREATE EXTENSION IF NOT EXISTS citext;
223 CREATE EXTENSION IF NOT EXISTS pg_trgm;
224 CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
227 dbHost = if db ? socket_dir then db.socket_dir
228 else if db ? socket then db.socket
229 else if db ? hostname then db.hostname
232 initDbScript = writeShell {
233 name = "akkoma-initdb";
234 runtimeInputs = with pkgs; [ coreutils replace-secret config.services.postgresql.package ];
236 pgpass="$(mktemp -t pgpass-XXXXXXXXXX.conf)"
237 setupSql="$(mktemp -t setup-XXXXXXXXXX.psql)"
238 trap 'rm -f "$pgpass $setupSql"' EXIT TERM
240 ${optionalString (dbHost != null) ''
241 export PGHOST=${escapeShellArg dbHost}
243 export PGUSER=${escapeShellArg cfg.initDb.username}
244 ${optionalString (pgpass != null) ''
245 cat ${escapeShellArg pgpass} >"$pgpass"
246 replace-secret ${escapeShellArgs [
247 (sha256 cfg.initDb.password._secret) cfg.initDb.password._secret ]} "$pgpass"
248 export PGPASSFILE="$pgpass"
251 cat ${escapeShellArg setupSql} >"$setupSql"
252 ${optionalString (db ? password) ''
253 replace-secret ${escapeShellArgs [
254 (sha256 db.password._secret) db.password._secret ]} "$setupSql"
257 # Create role if non‐existent
258 psql -tAc "SELECT 1 FROM pg_roles
259 WHERE rolname = "${escapeShellArg (escapeSqlStr db.username)} | grep -F -q 1 || \
260 psql -tAc "CREATE ROLE "${escapeShellArg (escapeSqlId db.username)}
262 # Create database if non‐existent
263 psql -tAc "SELECT 1 FROM pg_database
264 WHERE datname = "${escapeShellArg (escapeSqlStr db.database)} | grep -F -q 1 || \
265 psql -tAc "CREATE DATABASE "${escapeShellArg (escapeSqlId db.database)}"
266 OWNER "${escapeShellArg (escapeSqlId db.username)}"
276 script = writeShell {
281 RUNTIME_DIRECTORY="''${RUNTIME_DIRECTORY:-/run/akkoma}"
282 AKKOMA_CONFIG_PATH="''${RUNTIME_DIRECTORY%%:*}/config.exs" \
283 ERL_EPMD_ADDRESS="${cfg.dist.address}" \
284 ERL_EPMD_PORT="${toString cfg.dist.epmdPort}" \
285 ERL_FLAGS=${lib.escapeShellArg (lib.escapeShellArgs ([
286 "-kernel" "inet_dist_use_interface" (erlAddr cfg.dist.address)
287 "-kernel" "inet_dist_listen_min" (toString cfg.dist.portMin)
288 "-kernel" "inet_dist_listen_max" (toString cfg.dist.portMax)
289 ] ++ cfg.dist.extraFlags))} \
290 RELEASE_COOKIE="$(<"''${RUNTIME_DIRECTORY%%:*}/cookie")" \
291 RELEASE_NAME="akkoma" \
292 exec "${cfg.package}/bin/$(basename "$0")" "$@"
295 in pkgs.runCommandLocal "akkoma-env" { } ''
298 ln -r -s ${escapeShellArg script} "$out/bin/pleroma"
299 ln -r -s ${escapeShellArg script} "$out/bin/pleroma_ctl"
302 userWrapper = pkgs.writeShellApplication {
303 name = "pleroma_ctl";
305 if [ "''${1-}" == "update" ]; then
306 echo "OTP releases are not supported on NixOS." >&2
310 exec sudo -u ${escapeShellArg cfg.user} \
311 "${envWrapper}/bin/pleroma_ctl" "$@"
315 socketScript = if isAbsolutePath web.http.ip
317 name = "akkoma-socket";
318 runtimeInputs = with pkgs; [ coreutils inotify-tools ];
321 inotifywait -q -m -e create ${escapeShellArg (dirOf web.http.ip)}
324 trap 'kill "$COPROC_PID"' EXIT TERM
326 until test -S ${escapeShellArg web.http.ip}
327 do read -r -u "''${COPROC[0]}"
330 chmod 0666 ${escapeShellArg web.http.ip}
335 staticDir = ex.":pleroma".":instance".static_dir;
336 uploadDir = ex.":pleroma".":instance".upload_dir;
338 staticFiles = pkgs.runCommandLocal "akkoma-static" { } ''
339 ${concatStringsSep "\n" (mapAttrsToList (key: val: ''
340 mkdir -p $out/frontends/${escapeShellArg val.name}/
341 ln -s ${escapeShellArg val.package} $out/frontends/${escapeShellArg val.name}/${escapeShellArg val.ref}
344 ${optionalString (cfg.extraStatic != null)
345 (concatStringsSep "\n" (mapAttrsToList (key: val: ''
346 mkdir -p "$out/$(dirname ${escapeShellArg key})"
347 ln -s ${escapeShellArg val} $out/${escapeShellArg key}
348 '') cfg.extraStatic))}
353 enable = mkEnableOption "Akkoma";
355 package = mkPackageOption pkgs "akkoma" { };
358 type = types.nonEmptyStr;
360 description = "User account under which Akkoma runs.";
364 type = types.nonEmptyStr;
366 description = "Group account under which Akkoma runs.";
374 Whether to automatically initialise the database on startup. This will create a
375 database role and database if they do not already exist, and (re)set the role password
376 and the ownership of the database.
378 This setting can be used safely even if the database already exists and contains data.
380 The database settings are configured through
381 [{option}`config.services.akkoma.config.":pleroma"."Pleroma.Repo"`](#opt-services.akkoma.config.__pleroma_._Pleroma.Repo_).
383 If disabled, the database has to be set up manually:
386 CREATE ROLE akkoma LOGIN;
388 CREATE DATABASE akkoma
395 CREATE EXTENSION IF NOT EXISTS citext;
396 CREATE EXTENSION IF NOT EXISTS pg_trgm;
397 CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
402 username = mkOption {
403 type = types.nonEmptyStr;
404 default = config.services.postgresql.superUser;
405 defaultText = literalExpression "config.services.postgresql.superUser";
407 Name of the database user to initialise the database with.
409 This user is required to have the `CREATEROLE` and `CREATEDB` capabilities.
413 password = mkOption {
414 type = types.nullOr secret;
417 Password of the database user to initialise the database with.
419 If set to `null`, no password will be used.
421 The attribute `_secret` should point to a file containing the secret.
426 initSecrets = mkOption {
430 Whether to initialise non‐existent secrets with random values.
432 If enabled, appropriate secrets for the following options will be created automatically
433 if the files referenced in the `_secrets` attribute do not exist during startup.
435 - {option}`config.":pleroma"."Pleroma.Web.Endpoint".secret_key_base`
436 - {option}`config.":pleroma"."Pleroma.Web.Endpoint".signing_salt`
437 - {option}`config.":pleroma"."Pleroma.Web.Endpoint".live_view.signing_salt`
438 - {option}`config.":web_push_encryption".":vapid_details".private_key`
439 - {option}`config.":web_push_encryption".":vapid_details".public_key`
440 - {option}`config.":joken".":default_signer"`
444 installWrapper = mkOption {
448 Whether to install a wrapper around `pleroma_ctl` to simplify administration of the
453 extraPackages = mkOption {
454 type = with types; listOf package;
455 default = with pkgs; [ exiftool ffmpeg-headless graphicsmagick-imagemagick-compat ];
456 defaultText = literalExpression "with pkgs; [ exiftool ffmpeg-headless graphicsmagick-imagemagick-compat ]";
457 example = literalExpression "with pkgs; [ exiftool ffmpeg-full imagemagick ]";
459 List of extra packages to include in the executable search path of the service unit.
460 These are needed by various configurable components such as:
462 - ExifTool for the `Pleroma.Upload.Filter.Exiftool` upload filter,
463 - ImageMagick for still image previews in the media proxy as well as for the
464 `Pleroma.Upload.Filters.Mogrify` upload filter, and
465 - ffmpeg for video previews in the media proxy.
469 frontends = mkOption {
470 description = "Akkoma frontends.";
471 type = with types; attrsOf (submodule frontend);
474 package = pkgs.akkoma-frontends.akkoma-fe;
479 package = pkgs.akkoma-frontends.admin-fe;
484 defaultText = literalExpression ''
487 package = pkgs.akkoma-frontends.akkoma-fe;
492 package = pkgs.akkoma-frontends.admin-fe;
500 extraStatic = mkOption {
501 type = with types; nullOr (attrsOf package);
503 Attribute set of extra packages to add to the static files directory.
505 Do not add frontends here. These should be configured through
506 [{option}`services.akkoma.frontends`](#opt-services.akkoma.frontends).
509 example = literalExpression ''
511 "emoji/blobs.gg" = pkgs.akkoma-emoji.blobs_gg;
512 "static/terms-of-service.html" = pkgs.writeText "terms-of-service.html" '''
516 rev = "697a8211b0f427a921e7935a35d14bb3e32d0a2c";
517 in pkgs.stdenvNoCC.mkDerivation {
518 name = "favicon.png";
520 src = pkgs.fetchurl {
521 url = "https://raw.githubusercontent.com/TilCreator/NixOwO/''${rev}/NixOwO_plain.svg";
522 hash = "sha256-tWhHMfJ3Od58N9H5yOKPMfM56hYWSOnr/TGCBi8bo9E=";
525 nativeBuildInputs = with pkgs; [ librsvg ];
529 rsvg-convert -o $out -w 96 -h 96 $src
539 default = "127.0.0.1";
541 Listen address for Erlang distribution protocol and Port Mapper Daemon (epmd).
545 epmdPort = mkOption {
548 description = "TCP port to bind Erlang Port Mapper Daemon to.";
551 extraFlags = mkOption {
552 type = with types; listOf str;
554 description = "Extra flags to pass to Erlang";
555 example = [ "+sbwt" "none" "+sbwtdcpu" "none" "+sbwtdio" "none" ];
561 description = "Lower bound for Erlang distribution protocol TCP port.";
567 description = "Upper bound for Erlang distribution protocol TCP port.";
571 type = types.nullOr secret;
573 example = { _secret = "/var/lib/secrets/akkoma/releaseCookie"; };
575 Erlang release cookie.
577 If set to `null`, a temporary random cookie will be generated.
584 Configuration for Akkoma. The attributes are serialised to Elixir DSL.
586 Refer to <https://docs.akkoma.dev/stable/configuration/cheatsheet/> for
587 configuration options.
589 Settings containing secret data should be set to an attribute set containing the
590 attribute `_secret` - a string pointing to a file containing the value the option
593 type = types.submodule {
594 freeformType = format.type;
599 type = types.nonEmptyStr;
600 description = "Instance name.";
604 type = types.nonEmptyStr;
605 description = "Instance administrator email.";
608 description = mkOption {
609 type = types.nonEmptyStr;
610 description = "Instance description.";
613 static_dir = mkOption {
615 default = toString staticFiles;
616 defaultText = literalMD ''
617 Derivation gathering the following paths into a directory:
619 - [{option}`services.akkoma.frontends`](#opt-services.akkoma.frontends)
620 - [{option}`services.akkoma.extraStatic`](#opt-services.akkoma.extraStatic)
623 Directory of static files.
625 This directory can be built using a derivation, or it can be managed as mutable
626 state by setting the option to an absolute path.
630 upload_dir = mkOption {
632 default = "/var/lib/akkoma/uploads";
634 Directory where Akkoma will put uploaded files.
639 "Pleroma.Repo" = mkOption {
642 adapter = format.lib.mkRaw "Ecto.Adapters.Postgres";
643 socket_dir = "/run/postgresql";
647 defaultText = literalExpression ''
649 adapter = (pkgs.formats.elixirConf { }).lib.mkRaw "Ecto.Adapters.Postgres";
650 socket_dir = "/run/postgresql";
651 username = config.services.akkoma.user;
656 Database configuration.
659 <https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-connection-options>
664 "Pleroma.Web.Endpoint" = {
667 type = types.nonEmptyStr;
668 default = config.networking.fqdn;
669 defaultText = literalExpression "config.networking.fqdn";
670 description = "Domain name of the instance.";
674 type = types.nonEmptyStr;
676 description = "URL scheme.";
682 description = "External port number.";
688 type = types.either absolutePath ipAddress;
689 default = "/run/akkoma/socket";
692 Listener IP address or Unix socket path.
694 The value is automatically converted to Elixir’s internal address
695 representation during serialisation.
701 default = if isAbsolutePath web.http.ip then 0 else 4000;
702 defaultText = literalExpression ''
703 if isAbsolutePath config.services.akkoma.config.:pleroma"."Pleroma.Web.Endpoint".http.ip
708 Listener port number.
710 Must be 0 if using a Unix socket.
715 secret_key_base = mkOption {
717 default = { _secret = "/var/lib/secrets/akkoma/key-base"; };
719 Secret key used as a base to generate further secrets for encrypting and
722 The attribute `_secret` should point to a file containing the secret.
724 This key can generated can be generated as follows:
727 $ tr -dc 'A-Za-z-._~' </dev/urandom | head -c 64
733 signing_salt = mkOption {
735 default = { _secret = "/var/lib/secrets/akkoma/liveview-salt"; };
737 LiveView signing salt.
739 The attribute `_secret` should point to a file containing the secret.
741 This salt can be generated as follows:
744 $ tr -dc 'A-Za-z0-9-._~' </dev/urandom | head -c 8
750 signing_salt = mkOption {
752 default = { _secret = "/var/lib/secrets/akkoma/signing-salt"; };
756 The attribute `_secret` should point to a file containing the secret.
758 This salt can be generated as follows:
761 $ tr -dc 'A-Za-z0-9-._~' </dev/urandom | head -c 8
767 "Pleroma.Upload" = let
768 httpConf = cfg.config.":pleroma"."Pleroma.Web.Endpoint".url;
770 base_url = mkOption {
771 type = types.nonEmptyStr;
772 default = if lib.versionOlder config.system.stateVersion "24.05"
773 then "${httpConf.scheme}://${httpConf.host}:${builtins.toString httpConf.port}/media/"
775 defaultText = literalExpression ''
776 if lib.versionOlder config.system.stateVersion "24.05"
777 then "$\{httpConf.scheme}://$\{httpConf.host}:$\{builtins.toString httpConf.port}/media/"
781 Base path which uploads will be stored at.
782 Whilst this can just be set to a subdirectory of the main domain, it is now recommended to use a different subdomain.
787 ":frontends" = mkOption {
790 (key: val: format.lib.mkMap { name = val.name; ref = val.ref; })
792 defaultText = literalExpression ''
793 lib.mapAttrs (key: val:
794 (pkgs.formats.elixirConf { }).lib.mkMap { name = val.name; ref = val.ref; })
795 config.services.akkoma.frontends;
798 Frontend configuration.
800 Users should rely on the default value and prefer to configure frontends through
801 [{option}`config.services.akkoma.frontends`](#opt-services.akkoma.frontends).
807 httpConf = cfg.config.":pleroma"."Pleroma.Web.Endpoint".url;
812 defaultText = literalExpression "false";
814 Whether to enable proxying of remote media through the instance's proxy.
817 base_url = mkOption {
818 type = types.nullOr types.nonEmptyStr;
819 default = if lib.versionOlder config.system.stateVersion "24.05"
820 then "${httpConf.scheme}://${httpConf.host}:${builtins.toString httpConf.port}"
822 defaultText = literalExpression ''
823 if lib.versionOlder config.system.stateVersion "24.05"
824 then "$\{httpConf.scheme}://$\{httpConf.host}:$\{builtins.toString httpConf.port}"
828 Base path for the media proxy.
829 Whilst this can just be set to a subdirectory of the main domain, it is now recommended to use a different subdomain.
836 ":web_push_encryption" = mkOption {
839 Web Push Notifications configuration.
841 The necessary key pair can be generated as follows:
844 $ nix-shell -p nodejs --run 'npx web-push generate-vapid-keys'
847 type = types.submodule {
848 freeformType = elixirValue;
852 type = types.nonEmptyStr;
853 default = "mailto:${ex.":pleroma".":instance".email}";
854 defaultText = literalExpression ''
855 "mailto:''${config.services.akkoma.config.":pleroma".":instance".email}"
857 description = "mailto URI for administrative contact.";
860 public_key = mkOption {
861 type = with types; either nonEmptyStr secret;
862 default = { _secret = "/var/lib/secrets/akkoma/vapid-public"; };
863 description = "base64-encoded public ECDH key.";
866 private_key = mkOption {
868 default = { _secret = "/var/lib/secrets/akkoma/vapid-private"; };
870 base64-encoded private ECDH key.
872 The attribute `_secret` should point to a file containing the secret.
881 ":default_signer" = mkOption {
883 default = { _secret = "/var/lib/secrets/akkoma/jwt-signer"; };
887 The attribute `_secret` should point to a file containing the secret.
889 This secret can be generated as follows:
892 $ tr -dc 'A-Za-z0-9-._~' </dev/urandom | head -c 64
899 ":backends" = mkOption {
900 type = types.listOf elixirValue;
902 default = with format.lib; [
903 (mkTuple [ (mkRaw "ExSyslogger") (mkAtom ":ex_syslogger") ])
915 type = types.nonEmptyStr;
916 apply = format.lib.mkAtom;
918 example = ":warning";
923 <https://hexdocs.pm/logger/Logger.html#module-levels>
931 ":data_dir" = mkOption {
934 default = format.lib.mkRaw ''
935 Path.join(System.fetch_env!("CACHE_DIRECTORY"), "tzdata")
944 type = with types; nullOr (submodule
945 (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }));
948 Extra configuration for the nginx virtual host of Akkoma.
950 If set to `null`, no virtual host will be added to the nginx configuration.
956 config = mkIf cfg.enable {
957 assertions = optionals (cfg.config.":pleroma".":media_proxy".enabled && cfg.config.":pleroma".":media_proxy".base_url == null) [''
958 `services.akkoma.config.":pleroma".":media_proxy".base_url` must be set when the media proxy is enabled.
960 warnings = optionals (with config.security; cfg.installWrapper && (!sudo.enable) && (!sudo-rs.enable)) [''
961 The pleroma_ctl wrapper enabled by the installWrapper option relies on
962 sudo, which appears to have been disabled through security.sudo.enable.
966 users."${cfg.user}" = {
967 description = "Akkoma user";
971 groups."${cfg.group}" = { };
974 # Confinement of the main service unit requires separation of the
975 # configuration generation into a separate unit to permit access to secrets
976 # residing outside of the chroot.
977 systemd.services.akkoma-config = {
978 description = "Akkoma social network configuration";
979 reloadTriggers = [ configFile ] ++ secretPaths;
981 unitConfig.PropagatesReloadTo = [ "akkoma.service" ];
984 RemainAfterExit = true;
987 RuntimeDirectory = mkBefore "akkoma";
989 ExecStart = mkMerge [
990 (mkIf (cfg.dist.cookie == null) [ genScript ])
991 (mkIf (cfg.dist.cookie != null) [ copyScript ])
992 (mkIf cfg.initSecrets [ initSecretsScript ])
996 ExecReload = mkMerge [
997 (mkIf cfg.initSecrets [ initSecretsScript ])
1003 systemd.services.akkoma-initdb = mkIf cfg.initDb.enable {
1004 description = "Akkoma social network database setup";
1005 requires = [ "akkoma-config.service" ];
1006 requiredBy = [ "akkoma.service" ];
1007 after = [ "akkoma-config.service" "postgresql.service" ];
1008 before = [ "akkoma.service" ];
1012 User = mkIf (db ? socket_dir || db ? socket)
1013 cfg.initDb.username;
1014 RemainAfterExit = true;
1016 ExecStart = initDbScript;
1021 systemd.services.akkoma = let
1022 runtimeInputs = with pkgs; [ coreutils gawk gnused ] ++ cfg.extraPackages;
1024 description = "Akkoma social network";
1025 documentation = [ "https://docs.akkoma.dev/stable/" ];
1027 # This service depends on network-online.target and is sequenced after
1028 # it because it requires access to the Internet to function properly.
1029 bindsTo = [ "akkoma-config.service" ];
1030 wants = [ "network-online.target" ];
1031 wantedBy = [ "multi-user.target" ];
1033 "akkoma-config.target"
1035 "network-online.target"
1036 "postgresql.service"
1039 confinement.packages = mkIf isConfined runtimeInputs;
1040 path = runtimeInputs;
1048 # The run‐time directory is preserved as it is managed by the akkoma-config.service unit.
1049 RuntimeDirectory = "akkoma";
1050 RuntimeDirectoryPreserve = true;
1052 CacheDirectory = "akkoma";
1054 BindPaths = [ "${uploadDir}:${uploadDir}:norbind" ];
1055 BindReadOnlyPaths = mkMerge [
1056 (mkIf (!isStorePath staticDir) [ "${staticDir}:${staticDir}:norbind" ])
1057 (mkIf isConfined (mkMerge [
1058 [ "/etc/hosts" "/etc/resolv.conf" ]
1059 (mkIf (isStorePath staticDir) (map (dir: "${dir}:${dir}:norbind")
1060 (splitString "\n" (readFile ((pkgs.closureInfo { rootPaths = staticDir; }) + "/store-paths")))))
1061 (mkIf (db ? socket_dir) [ "${db.socket_dir}:${db.socket_dir}:norbind" ])
1062 (mkIf (db ? socket) [ "${db.socket}:${db.socket}:norbind" ])
1066 ExecStartPre = "${envWrapper}/bin/pleroma_ctl migrate";
1067 ExecStart = "${envWrapper}/bin/pleroma start";
1068 ExecStartPost = socketScript;
1069 ExecStop = "${envWrapper}/bin/pleroma stop";
1070 ExecStopPost = mkIf (isAbsolutePath web.http.ip)
1071 "${pkgs.coreutils}/bin/rm -f '${web.http.ip}'";
1073 ProtectProc = "noaccess";
1075 ProtectSystem = "strict";
1078 PrivateDevices = true;
1080 ProtectHostname = true;
1081 ProtectClock = true;
1082 ProtectKernelTunables = true;
1083 ProtectKernelModules = true;
1084 ProtectKernelLogs = true;
1085 ProtectControlGroups = true;
1087 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
1088 RestrictNamespaces = true;
1089 LockPersonality = true;
1090 RestrictRealtime = true;
1091 RestrictSUIDSGID = true;
1094 CapabilityBoundingSet = mkIf
1095 (any (port: port > 0 && port < 1024)
1096 [ web.http.port cfg.dist.epmdPort cfg.dist.portMin ])
1097 [ "CAP_NET_BIND_SERVICE" ];
1099 NoNewPrivileges = true;
1100 SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ];
1101 SystemCallArchitectures = "native";
1104 DevicePolicy = "closed";
1106 # SMTP adapter uses dynamic port 0 binding, which is incompatible with bind address filtering
1107 SocketBindAllow = mkIf (!hasSmtp) (mkMerge [
1108 [ "tcp:${toString cfg.dist.epmdPort}" "tcp:${toString cfg.dist.portMin}-${toString cfg.dist.portMax}" ]
1109 (mkIf (web.http.port != 0) [ "tcp:${toString web.http.port}" ])
1111 SocketBindDeny = mkIf (!hasSmtp) "any";
1115 systemd.tmpfiles.rules = [
1116 "d ${uploadDir} 0700 ${cfg.user} ${cfg.group} - -"
1117 "Z ${uploadDir} ~0700 ${cfg.user} ${cfg.group} - -"
1120 environment.systemPackages = mkIf (cfg.installWrapper) [ userWrapper ];
1122 services.nginx.virtualHosts = mkIf (cfg.nginx != null) {
1123 ${web.url.host} = mkMerge [ cfg.nginx {
1126 if isAbsolutePath web.http.ip
1127 then "http://unix:${web.http.ip}"
1128 else if hasInfix ":" web.http.ip
1129 then "http://[${web.http.ip}]:${toString web.http.port}"
1130 else "http://${web.http.ip}:${toString web.http.port}";
1132 proxyWebsockets = true;
1133 recommendedProxySettings = true;
1139 meta.maintainers = with maintainers; [ mvs ];
1140 meta.doc = ./akkoma.md;