about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorRyan Lahfa <masterancpp@gmail.com>2023-05-14 17:09:19 +0200
committerGitHub <noreply@github.com>2023-05-14 17:09:19 +0200
commit8ef486b60e994db9e21c58e7e15b8e90d6df5936 (patch)
tree361f54a3b979affc0737e63d3492ba297cff4111 /nixos
parentef5c1750c9bc0200663e3a978af244ceeac92b80 (diff)
parentf341151cfa872ff8d80d3c00e090380920cda8c5 (diff)
Merge pull request #207194 from RaitoBezarius/pixelfed-module
pixelfed: init at 0.11.5, module, tests
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-2305.section.md2
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/web-apps/pixelfed.nix478
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/web-apps/pixelfed/default.nix8
-rw-r--r--nixos/tests/web-apps/pixelfed/standard.nix38
6 files changed, 528 insertions, 0 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index c45fae5b1cd4a..c6a264a1b037f 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -38,6 +38,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [Akkoma](https://akkoma.social), an ActivityPub microblogging server. Available as [services.akkoma](options.html#opt-services.akkoma.enable).
 
+- [Pixelfed](https://pixelfed.org/), an Instagram-like ActivityPub server. Available as [services.pixelfed](options.html#opt-services.pixelfed.enable).
+
 - [blesh](https://github.com/akinomyoga/ble.sh), a line editor written in pure bash. Available as [programs.bash.blesh](#opt-programs.bash.blesh.enable).
 
 - [webhook](https://github.com/adnanh/webhook), a lightweight webhook server. Available as [services.webhook](#opt-services.webhook.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 9ca58fdc08757..1ebab063a464c 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1178,6 +1178,7 @@
   ./services/web-apps/gerrit.nix
   ./services/web-apps/gotify-server.nix
   ./services/web-apps/grocy.nix
+  ./services/web-apps/pixelfed.nix
   ./services/web-apps/healthchecks.nix
   ./services/web-apps/hedgedoc.nix
   ./services/web-apps/hledger-web.nix
diff --git a/nixos/modules/services/web-apps/pixelfed.nix b/nixos/modules/services/web-apps/pixelfed.nix
new file mode 100644
index 0000000000000..817d0f9b60f50
--- /dev/null
+++ b/nixos/modules/services/web-apps/pixelfed.nix
@@ -0,0 +1,478 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pixelfed;
+  user = cfg.user;
+  group = cfg.group;
+  pixelfed = cfg.package.override { inherit (cfg) dataDir runtimeDir; };
+  # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L185-L190
+  extraPrograms = with pkgs; [ jpegoptim optipng pngquant gifsicle ffmpeg ];
+  # Ensure PHP extensions: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L135-L147
+  phpPackage = cfg.phpPackage.buildEnv {
+    extensions = { enabled, all }:
+      enabled
+      ++ (with all; [ bcmath ctype curl mbstring gd intl zip redis imagick ]);
+  };
+  configFile =
+    pkgs.writeText "pixelfed-env" (lib.generators.toKeyValue { } cfg.settings);
+  # Management script
+  pixelfed-manage = pkgs.writeShellScriptBin "pixelfed-manage" ''
+    cd ${pixelfed}
+    sudo=exec
+    if [[ "$USER" != ${user} ]]; then
+      sudo='exec /run/wrappers/bin/sudo -u ${user}'
+    fi
+    $sudo ${cfg.phpPackage}/bin/php artisan "$@"
+  '';
+  dbSocket = {
+    "pgsql" = "/run/postgresql";
+    "mysql" = "/run/mysqld/mysqld.sock";
+  }.${cfg.database.type};
+  dbService = {
+    "pgsql" = "postgresql.service";
+    "mysql" = "mysql.service";
+  }.${cfg.database.type};
+  redisService = "redis-pixelfed.service";
+in {
+  options.services = {
+    pixelfed = {
+      enable = mkEnableOption (lib.mdDoc "a Pixelfed instance");
+      package = mkPackageOptionMD pkgs "pixelfed" { };
+      phpPackage = mkPackageOptionMD pkgs "php81" { };
+
+      user = mkOption {
+        type = types.str;
+        default = "pixelfed";
+        description = lib.mdDoc ''
+          User account under which pixelfed runs.
+
+          ::: {.note}
+          If left as the default value this user will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the pixelfed application starts.
+          :::
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "pixelfed";
+        description = lib.mdDoc ''
+          Group account under which pixelfed runs.
+
+          ::: {.note}
+          If left as the default value this group will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the group exists before the pixelfed application starts.
+          :::
+        '';
+      };
+
+      domain = mkOption {
+        type = types.str;
+        description = lib.mdDoc ''
+          FQDN for the Pixelfed instance.
+        '';
+      };
+
+      secretFile = mkOption {
+        type = types.path;
+        description = lib.mdDoc ''
+          A secret file to be sourced for the .env settings.
+          Place `APP_KEY` and other settings that should not end up in the Nix store here.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; (attrsOf (oneOf [ bool int str ]));
+        description = lib.mdDoc ''
+          .env settings for Pixelfed.
+          Secrets should use `secretFile` option instead.
+        '';
+      };
+
+      nginx = mkOption {
+        type = types.nullOr (types.submodule
+          (import ../web-servers/nginx/vhost-options.nix {
+            inherit config lib;
+          }));
+        default = null;
+        example = lib.literalExpression ''
+          {
+            serverAliases = [
+              "pics.''${config.networking.domain}"
+            ];
+            enableACME = true;
+            forceHttps = true;
+          }
+        '';
+        description = lib.mdDoc ''
+          With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr.
+          Set to {} if you do not need any customization to the virtual host.
+          If enabled, then by default, the {option}`serverName` is
+          `''${domain}`,
+          If this is set to null (the default), no nginx virtualHost will be configured.
+        '';
+      };
+
+      redis.createLocally = mkEnableOption
+        (lib.mdDoc "a local Redis database using UNIX socket authentication")
+        // {
+          default = true;
+        };
+
+      database = {
+        createLocally = mkEnableOption
+          (lib.mdDoc "a local database using UNIX socket authentication") // {
+            default = true;
+          };
+        automaticMigrations = mkEnableOption
+          (lib.mdDoc "automatic migrations for database schema and data") // {
+            default = true;
+          };
+
+        type = mkOption {
+          type = types.enum [ "mysql" "pgsql" ];
+          example = "pgsql";
+          default = "mysql";
+          description = lib.mdDoc ''
+            Database engine to use.
+            Note that PGSQL is not well supported: https://github.com/pixelfed/pixelfed/issues/2727
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "pixelfed";
+          description = lib.mdDoc "Database name.";
+        };
+      };
+
+      maxUploadSize = mkOption {
+        type = types.str;
+        default = "8M";
+        description = lib.mdDoc ''
+          Max upload size with units.
+        '';
+      };
+
+      poolConfig = mkOption {
+        type = with types; attrsOf (oneOf [ int str bool ]);
+        default = { };
+
+        description = lib.mdDoc ''
+          Options for Pixelfed's PHP-FPM pool.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/pixelfed";
+        description = lib.mdDoc ''
+          State directory of the `pixelfed` user which holds
+          the application's state and data.
+        '';
+      };
+
+      runtimeDir = mkOption {
+        type = types.str;
+        default = "/run/pixelfed";
+        description = lib.mdDoc ''
+          Ruutime directory of the `pixelfed` user which holds
+          the application's caches and temporary files.
+        '';
+      };
+
+      schedulerInterval = mkOption {
+        type = types.str;
+        default = "1d";
+        description = lib.mdDoc "How often the Pixelfed cron task should run";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.pixelfed = mkIf (cfg.user == "pixelfed") {
+      isSystemUser = true;
+      group = cfg.group;
+      extraGroups = lib.optional cfg.redis.createLocally "redis-pixelfed";
+    };
+    users.groups.pixelfed = mkIf (cfg.group == "pixelfed") { };
+
+    services.redis.servers.pixelfed.enable = lib.mkIf cfg.redis.createLocally true;
+    services.pixelfed.settings = mkMerge [
+      ({
+        APP_ENV = mkDefault "production";
+        APP_DEBUG = mkDefault false;
+        # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L312-L316
+        APP_URL = mkDefault "https://${cfg.domain}";
+        ADMIN_DOMAIN = mkDefault cfg.domain;
+        APP_DOMAIN = mkDefault cfg.domain;
+        SESSION_DOMAIN = mkDefault cfg.domain;
+        SESSION_SECURE_COOKIE = mkDefault true;
+        OPEN_REGISTRATION = mkDefault false;
+        # ActivityPub: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L360-L364
+        ACTIVITY_PUB = mkDefault true;
+        AP_REMOTE_FOLLOW = mkDefault true;
+        AP_INBOX = mkDefault true;
+        AP_OUTBOX = mkDefault true;
+        AP_SHAREDINBOX = mkDefault true;
+        # Image optimization: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L367-L404
+        PF_OPTIMIZE_IMAGES = mkDefault true;
+        IMAGE_DRIVER = mkDefault "imagick";
+        # Mobile APIs
+        OAUTH_ENABLED = mkDefault true;
+        # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L351
+        EXP_EMC = mkDefault true;
+        # Defer to systemd
+        LOG_CHANNEL = mkDefault "stderr";
+        # TODO: find out the correct syntax?
+        # TRUST_PROXIES = mkDefault "127.0.0.1/8, ::1/128";
+      })
+      (mkIf (cfg.redis.createLocally) {
+        BROADCAST_DRIVER = mkDefault "redis";
+        CACHE_DRIVER = mkDefault "redis";
+        QUEUE_DRIVER = mkDefault "redis";
+        SESSION_DRIVER = mkDefault "redis";
+        WEBSOCKET_REPLICATION_MODE = mkDefault "redis";
+        # Suppport phpredis and predis configuration-style.
+        REDIS_SCHEME = "unix";
+        REDIS_HOST = config.services.redis.servers.pixelfed.unixSocket;
+        REDIS_PATH = config.services.redis.servers.pixelfed.unixSocket;
+      })
+      (mkIf (cfg.database.createLocally) {
+        DB_CONNECTION = cfg.database.type;
+        DB_SOCKET = dbSocket;
+        DB_DATABASE = cfg.database.name;
+        DB_USERNAME = user;
+        # No TCP/IP connection.
+        DB_PORT = 0;
+      })
+    ];
+
+    environment.systemPackages = [ pixelfed-manage ];
+
+    services.mysql =
+      mkIf (cfg.database.createLocally && cfg.database.type == "mysql") {
+        enable = mkDefault true;
+        package = mkDefault pkgs.mariadb;
+        ensureDatabases = [ cfg.database.name ];
+        ensureUsers = [{
+          name = user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }];
+      };
+
+    services.postgresql =
+      mkIf (cfg.database.createLocally && cfg.database.type == "pgsql") {
+        enable = mkDefault true;
+        ensureDatabases = [ cfg.database.name ];
+        ensureUsers = [{
+          name = user;
+          ensurePermissions = { };
+        }];
+      };
+
+    # Make each individual option overridable with lib.mkDefault.
+    services.pixelfed.poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) {
+      "pm" = "dynamic";
+      "php_admin_value[error_log]" = "stderr";
+      "php_admin_flag[log_errors]" = true;
+      "catch_workers_output" = true;
+      "pm.max_children" = "32";
+      "pm.start_servers" = "2";
+      "pm.min_spare_servers" = "2";
+      "pm.max_spare_servers" = "4";
+      "pm.max_requests" = "500";
+    };
+
+    services.phpfpm.pools.pixelfed = {
+      inherit user group;
+      inherit phpPackage;
+
+      phpOptions = ''
+        post_max_size = ${toString cfg.maxUploadSize}
+        upload_max_filesize = ${toString cfg.maxUploadSize}
+        max_execution_time = 600;
+      '';
+
+      settings = {
+        "listen.owner" = user;
+        "listen.group" = group;
+        "listen.mode" = "0660";
+        "catch_workers_output" = "yes";
+      } // cfg.poolConfig;
+    };
+
+    systemd.services.phpfpm-pixelfed.after = [ "pixelfed-data-setup.service" ];
+    systemd.services.phpfpm-pixelfed.requires =
+      [ "pixelfed-horizon.service" "pixelfed-data-setup.service" ]
+      ++ lib.optional cfg.database.createLocally dbService
+      ++ lib.optional cfg.redis.createLocally redisService;
+    # Ensure image optimizations programs are available.
+    systemd.services.phpfpm-pixelfed.path = extraPrograms;
+
+    systemd.services.pixelfed-horizon = {
+      description = "Pixelfed task queueing via Laravel Horizon framework";
+      after = [ "network.target" "pixelfed-data-setup.service" ];
+      requires = [ "pixelfed-data-setup.service" ]
+        ++ (lib.optional cfg.database.createLocally dbService)
+        ++ (lib.optional cfg.redis.createLocally redisService);
+      wantedBy = [ "multi-user.target" ];
+      # Ensure image optimizations programs are available.
+      path = extraPrograms;
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pixelfed-manage}/bin/pixelfed-manage horizon";
+        StateDirectory =
+          lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
+        User = user;
+        Group = group;
+        Restart = "on-failure";
+      };
+    };
+
+    systemd.timers.pixelfed-cron = {
+      description = "Pixelfed periodic tasks timer";
+      after = [ "pixelfed-data-setup.service" ];
+      requires = [ "phpfpm-pixelfed.service" ];
+      wantedBy = [ "timers.target" ];
+
+      timerConfig = {
+        OnBootSec = cfg.schedulerInterval;
+        OnUnitActiveSec = cfg.schedulerInterval;
+      };
+    };
+
+    systemd.services.pixelfed-cron = {
+      description = "Pixelfed periodic tasks";
+      # Ensure image optimizations programs are available.
+      path = extraPrograms;
+
+      serviceConfig = {
+        ExecStart = "${pixelfed-manage}/bin/pixelfed-manage schedule:run";
+        User = user;
+        Group = group;
+        StateDirectory = cfg.dataDir;
+      };
+    };
+
+    systemd.services.pixelfed-data-setup = {
+      description =
+        "Pixelfed setup: migrations, environment file update, cache reload, data changes";
+      wantedBy = [ "multi-user.target" ];
+      after = lib.optional cfg.database.createLocally dbService;
+      requires = lib.optional cfg.database.createLocally dbService;
+      path = with pkgs; [ bash pixelfed-manage rsync ] ++ extraPrograms;
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = user;
+        Group = group;
+        StateDirectory =
+          lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
+        LoadCredential = "env-secrets:${cfg.secretFile}";
+        UMask = "077";
+      };
+
+      script = ''
+        # Concatenate non-secret .env and secret .env
+        rm -f ${cfg.dataDir}/.env
+        cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env
+        echo -e '\n' >> ${cfg.dataDir}/.env
+        cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env
+
+        # Link the static storage (package provided) to the runtime storage
+        # Necessary for cities.json and static images.
+        mkdir -p ${cfg.dataDir}/storage
+        rsync -av --no-perms ${pixelfed}/storage-static/ ${cfg.dataDir}/storage
+        chmod -R +w ${cfg.dataDir}/storage
+
+        # Link the app.php in the runtime folder.
+        # We cannot link the cache folder only because bootstrap folder needs to be writeable.
+        ln -sf ${pixelfed}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php
+
+        # https://laravel.com/docs/10.x/filesystem#the-public-disk
+        # Creating the public/storage → storage/app/public link
+        # is unnecessary as it's part of the installPhase of pixelfed.
+
+        # Install Horizon
+        # FIXME: require write access to public/ — should be done as part of install — pixelfed-manage horizon:publish
+
+        # Before running any PHP program, cleanup the bootstrap.
+        # It's necessary if you upgrade the application otherwise you might
+        # try to import non-existent modules.
+        rm -rf ${cfg.runtimeDir}/bootstrap/*
+
+        # Perform the first migration.
+        [[ ! -f ${cfg.dataDir}/.initial-migration ]] && pixelfed-manage migrate --force && touch ${cfg.dataDir}/.initial-migration
+
+        ${lib.optionalString cfg.database.automaticMigrations ''
+          # Force migrate the database.
+          pixelfed-manage migrate --force
+        ''}
+
+        # Import location data
+        pixelfed-manage import:cities
+
+        ${lib.optionalString cfg.settings.ACTIVITY_PUB ''
+          # ActivityPub federation bookkeeping
+          [[ ! -f ${cfg.dataDir}/.instance-actor-created ]] && pixelfed-manage instance:actor && touch ${cfg.dataDir}/.instance-actor-created
+        ''}
+
+        ${lib.optionalString cfg.settings.OAUTH_ENABLED ''
+          # Generate Passport encryption keys
+          [[ ! -f ${cfg.dataDir}/.passport-keys-generated ]] && pixelfed-manage passport:keys && touch ${cfg.dataDir}/.passport-keys-generated
+        ''}
+
+        pixelfed-manage route:cache
+        pixelfed-manage view:cache
+        pixelfed-manage config:cache
+      '';
+    };
+
+    systemd.tmpfiles.rules = [
+      # Cache must live across multiple systemd units runtimes.
+      "d ${cfg.runtimeDir}/                         0700 ${user} ${group} - -"
+      "d ${cfg.runtimeDir}/cache                    0700 ${user} ${group} - -"
+    ];
+
+    # Enable NGINX to access our phpfpm-socket.
+    users.users."${config.services.nginx.group}".extraGroups = [ cfg.group ];
+    services.nginx = mkIf (cfg.nginx != null) {
+      enable = true;
+      virtualHosts."${cfg.domain}" = mkMerge [
+        cfg.nginx
+        {
+          root = lib.mkForce "${pixelfed}/public/";
+          locations."/".tryFiles = "$uri $uri/ /index.php?query_string";
+          locations."/favicon.ico".extraConfig = ''
+            access_log off; log_not_found off;
+          '';
+          locations."/robots.txt".extraConfig = ''
+            access_log off; log_not_found off;
+          '';
+          locations."~ \\.php$".extraConfig = ''
+            fastcgi_split_path_info ^(.+\.php)(/.+)$;
+            fastcgi_pass unix:${config.services.phpfpm.pools.pixelfed.socket};
+            fastcgi_index index.php;
+          '';
+          locations."~ /\\.(?!well-known).*".extraConfig = ''
+            deny all;
+          '';
+          extraConfig = ''
+            add_header X-Frame-Options "SAMEORIGIN";
+            add_header X-XSS-Protection "1; mode=block";
+            add_header X-Content-Type-Options "nosniff";
+            index index.html index.htm index.php;
+            error_page 404 /index.php;
+            client_max_body_size ${toString cfg.maxUploadSize};
+          '';
+        }
+      ];
+    };
+  };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 9f35dca5cc4f1..2d9f5318a36d0 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -432,6 +432,7 @@ in {
   man = handleTest ./man.nix {};
   mariadb-galera = handleTest ./mysql/mariadb-galera.nix {};
   mastodon = discoverTests (import ./web-apps/mastodon { inherit handleTestOn; });
+  pixelfed = discoverTests (import ./web-apps/pixelfed { inherit handleTestOn; });
   mate = handleTest ./mate.nix {};
   matomo = handleTest ./matomo.nix {};
   matrix-appservice-irc = handleTest ./matrix/appservice-irc.nix {};
diff --git a/nixos/tests/web-apps/pixelfed/default.nix b/nixos/tests/web-apps/pixelfed/default.nix
new file mode 100644
index 0000000000000..4464ebe434865
--- /dev/null
+++ b/nixos/tests/web-apps/pixelfed/default.nix
@@ -0,0 +1,8 @@
+{ system ? builtins.currentSystem, handleTestOn }:
+let
+  supportedSystems = [ "x86_64-linux" "i686-linux" ];
+
+in
+{
+  standard = handleTestOn supportedSystems ./standard.nix { inherit system; };
+}
diff --git a/nixos/tests/web-apps/pixelfed/standard.nix b/nixos/tests/web-apps/pixelfed/standard.nix
new file mode 100644
index 0000000000000..9260e27af960d
--- /dev/null
+++ b/nixos/tests/web-apps/pixelfed/standard.nix
@@ -0,0 +1,38 @@
+import ../../make-test-python.nix ({pkgs, ...}:
+{
+  name = "pixelfed-standard";
+  meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+
+  nodes = {
+    server = { pkgs, ... }: {
+      services.pixelfed = {
+        enable = true;
+        domain = "pixelfed.local";
+        # Configure NGINX.
+        nginx = {};
+        secretFile = (pkgs.writeText "secrets.env" ''
+          # Snakeoil secret, can be any random 32-chars secret via CSPRNG.
+          APP_KEY=adKK9EcY8Hcj3PLU7rzG9rJ6KKTOtYfA
+        '');
+        settings."FORCE_HTTPS_URLS" = false;
+      };
+    };
+  };
+
+  testScript = ''
+    # Wait for Pixelfed PHP pool
+    server.wait_for_unit("phpfpm-pixelfed.service")
+    # Wait for NGINX
+    server.wait_for_unit("nginx.service")
+    # Wait for HTTP port
+    server.wait_for_open_port(80)
+    # Access the homepage.
+    server.succeed("curl -H 'Host: pixelfed.local' http://localhost")
+    # Create an account
+    server.succeed("pixelfed-manage user:create --name=test --username=test --email=test@test.com --password=test")
+    # Create a OAuth token.
+    # TODO: figure out how to use it to send a image/toot
+    # server.succeed("pixelfed-manage passport:client --personal")
+    # server.succeed("curl -H 'Host: pixefed.local' -H 'Accept: application/json' -H 'Authorization: Bearer secret' -F'status'='test' http://localhost/api/v1/statuses")
+  '';
+})