Skip to content

Declarative installation on NixOS

Jan Tojnar edited this page May 12, 2024 · 6 revisions

Basic installation

NixOS contains a module for installing selfoss as a PHP-FPM service.

By default the module assumes a nginx server so we will need to set it up too.

{ config, lib, pkgs,  ... }:
{
  networking.firewall.allowedTCPPorts = [
    80
    443
  ];

  services = {
    nginx = {
      enable = true;

      virtualHosts = {
        "reader.example.org" = {
          root = "/var/lib/selfoss"; # hardcoded in the module
          extraConfig = ''
            include /var/lib/selfoss/.nginx.conf;
            location ~ \.php$ {
              fastcgi_pass unix:${config.services.phpfpm.pools.${config.services.selfoss.pool}.socket};
              include ${config.services.nginx.package}/conf/fastcgi.conf;
              fastcgi_param PATH_INFO $fastcgi_path_info;
              fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
            }
          '';
        };
      };
    };

    selfoss = {
      enable = true;
    };
  };
}

You can also choose a different database engine using services.selfoss.database option or specify any other settings in services.selfoss.extraConfig.

The module however installs selfoss into mutable /var/lib/selfoss which can potentially be exploited. Additionally, it runs a stable version of selfoss, but we might want to install development build to get the latest and greatest features.

Using nightly releases of selfoss with immutable root

We will leave downloading and installing selfoss to Nix. selfoss will be loaded from a read-only directory in Nix store so data will need to be written to a different location. We will use systemd for that.

{ stdenv
, fetchurl
, lib
, unzip
}:

stdenv.mkDerivation (finalAttrs: {
  pname = "selfoss";
  version = "2.20-2429274";

  src = fetchurl {
    url = "https://dl.cloudsmith.io/public/fossar/selfoss-git/raw/names/selfoss.zip/versions/${finalAttrs.version}/selfoss-${finalAttrs.version}.zip";
    hash = "sha256-4/qxXL5RGW+h9swcxP1fwMQqkwu3lhPqT40MVgjJz5M=";
  };

  nativeBuildInputs = [
    unzip
  ];

  installPhase = ''
    runHook preInstall

    cp -r . $out

    runHook postInstall
  '';

  meta = {
    description = "Multipurpose RSS reader and aggregation web app";
    homepage = "https://selfoss.aditu.de";
    license = lib.licenses.gpl3Only;
    maintainers = with lib.maintainers; [ jtojnar ];
    platforms = lib.platforms.all;
  };
})
{ config, lib, pkgs,  ... }:
let
  settings = {
    username = "myname";
    password = "$2y$10$V5mUC2qhO4hGaiIi/d.IhOAuTu6PYAwHvIDI5FjrhakOPo1XA4hz6";
    auto_mark_as_read = true;
    share = "p";
    homepage = "unread";
    datadir = "/var/lib/selfoss";
    logger_destination = "file:php://stderr";
    logger_level = "DEBUG";
    debug = "1"; # extra logging info
    base_url = "https://reader.example.org/";
    items_lifetime = "9999";
  };

  settingsEnv = lib.mapAttrs' (name: value: lib.nameValuePair "selfoss_${name}" value) settings;

  # Modify the upstream nginx config to point to our mutable datadir.
  nginxConf =
    pkgs.runCommand
      "selfoss.nginx.conf"
      {
        src = "${pkgs.selfoss}/.nginx.conf";
      }
      ''
        substitute "$src" "$out" \
          --replace 'try_files $uri /data/$uri;' 'root ${settings.datadir};'
      '';
in {
  networking.firewall.allowedTCPPorts = [
    80
    443
  ];

  services = {
    nginx = {
      enable = true;

      virtualHosts = {
        "reader.example.org" = {
          root = pkgs.selfoss;
          extraConfig = ''
            include ${nginxConf};

            location ~ \.php$ {
              fastcgi_pass unix:${config.services.phpfpm.pools.reader.socket};
              include ${config.services.nginx.package}/conf/fastcgi.conf;
              fastcgi_param PATH_INFO $fastcgi_path_info;
              fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
            }
          '';
        };
      };
    };

    phpfpm = rec {
      pools = {
        reader = {
          user = "reader";

          settings = {
            "listen.owner" = "nginx";
            "listen.group" = "root";
            "pm" = "dynamic";
            "pm.max_children" = 5;
            "pm.start_servers" = 2;
            "pm.min_spare_servers" = 1;
            "pm.max_spare_servers" = 3;
            # push stderr output to journal
            "catch_workers_output" = true;
            # Accept settings from the systemd service.
            "clear_env" = false;
          };

          phpOptions = ''
            ; Set up $_ENV superglobal.
            ; https://php.net/request-order
            variables_order = "EGPCS"
          '';
        };
      };
    };
  };

  users = {
    users = {
      nginx.extraGroups = [
        "reader"
      ];

      reader = { uid = 501; group = "reader"; isSystemUser = true; };
    };

    groups = {
      reader = { gid = 501; };
    };
  };

  # I was not able to pass the variables through services.phpfpm.pools.reader.phpEnv:
  # https://github.com/NixOS/nixpkgs/issues/79469#issuecomment-631461513
  systemd.services.phpfpm-reader.environment = settingsEnv;
  systemd.services.phpfpm-reader.serviceConfig.StateDirectory = "selfoss"; # TODO: make it create cache, sqlite, thumbnails and favicons subdirectories.

  systemd.services.selfoss-update = {
    serviceConfig = {
      ExecStart = "${pkgs.php}/bin/php ${pkgs.selfoss}/cliupdate.php";
      User = "reader";
      StateDirectory = "selfoss";
    };
    environment = settingsEnv;
    startAt = "hourly";
    wantedBy = [ "multi-user.target" ];
  };

  nixpkgs.overlays = [
    (final: prev: {
      # Let’s use latest development build selfoss instead of the stable version from Nixpkgs.
      selfoss = prev.callPackage ./selfoss.nix { };
    })
  ];
}