diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 878b77969af..402f222f4e9 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -814,6 +814,7 @@
./services/web-apps/dokuwiki.nix
./services/web-apps/frab.nix
./services/web-apps/gotify-server.nix
+ ./services/web-apps/grocy.nix
./services/web-apps/icingaweb2/icingaweb2.nix
./services/web-apps/icingaweb2/module-monitoring.nix
./services/web-apps/ihatemoney
diff --git a/nixos/modules/services/web-apps/grocy.nix b/nixos/modules/services/web-apps/grocy.nix
new file mode 100644
index 00000000000..568bdfd0c42
--- /dev/null
+++ b/nixos/modules/services/web-apps/grocy.nix
@@ -0,0 +1,172 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.grocy;
+in {
+ options.services.grocy = {
+ enable = mkEnableOption "grocy";
+
+ hostName = mkOption {
+ type = types.str;
+ description = ''
+ FQDN for the grocy instance.
+ '';
+ };
+
+ nginx.enableSSL = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether or not to enable SSL (with ACME and let's encrypt)
+ for the grocy vhost.
+ '';
+ };
+
+ phpfpm.settings = mkOption {
+ type = with types; attrsOf (oneOf [ int str bool ]);
+ default = {
+ "pm" = "dynamic";
+ "php_admin_value[error_log]" = "stderr";
+ "php_admin_flag[log_errors]" = true;
+ "listen.owner" = "nginx";
+ "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";
+ };
+
+ description = ''
+ Options for grocy's PHPFPM pool.
+ '';
+ };
+
+ dataDir = mkOption {
+ type = types.str;
+ default = "/var/lib/grocy";
+ description = ''
+ Home directory of the grocy user which contains
+ the application's state.
+ '';
+ };
+
+ settings = {
+ currency = mkOption {
+ type = types.str;
+ default = "USD";
+ example = "EUR";
+ description = ''
+ ISO 4217 code for the currency to display.
+ '';
+ };
+
+ culture = mkOption {
+ type = types.enum [ "de" "en" "da" "en_GB" "es" "fr" "hu" "it" "nl" "no" "pl" "pt_BR" "ru" "sk_SK" "sv_SE" "tr" ];
+ default = "en";
+ description = ''
+ Display language of the frontend.
+ '';
+ };
+
+ calendar = {
+ showWeekNumber = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Show the number of the weeks in the calendar views.
+ '';
+ };
+ firstDayOfWeek = mkOption {
+ default = null;
+ type = types.nullOr (types.enum (range 0 6));
+ description = ''
+ Which day of the week (0=Sunday, 1=Monday etc.) should be the
+ first day.
+ '';
+ };
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ environment.etc."grocy/config.php".text = ''
+
+
+
Grocy
+
+ Grocy is a web-based self-hosted groceries
+ & household management solution for your home.
+
+
+
+ Basic usage
+
+ A very basic configuration may look like this:
+{ pkgs, ... }:
+{
+ services.grocy = {
+ enable = true;
+ hostName = "grocy.tld";
+ };
+}
+ This configures a simple vhost using nginx
+ which listens to grocy.tld with fully configured ACME/LE (this can be
+ disabled by setting services.grocy.nginx.enableSSL
+ to false). After the initial setup the credentials admin:admin
+ can be used to login.
+
+
+ The application's state is persisted at /var/lib/grocy/grocy.db in a
+ sqlite3 database. The migration is applied when requesting the /-route
+ of the application.
+
+
+
+
+ Settings
+
+ The configuration for grocy is located at /etc/grocy/config.php.
+ By default, the following settings can be defined in the NixOS-configuration:
+{ pkgs, ... }:
+{
+ services.grocy.settings = {
+ # The default currency in the system for invoices etc.
+ # Please note that exchange rates aren't taken into account, this
+ # is just the setting for what's shown in the frontend.
+ currency = "EUR";
+
+ # The display language (and locale configuration) for grocy.
+ culture = "de";
+
+ calendar = {
+ # Whether or not to show the week-numbers
+ # in the calendar.
+ showWeekNumber = true;
+
+ # Index of the first day to be shown in the calendar (0=Sunday, 1=Monday,
+ # 2=Tuesday and so on).
+ firstDayOfWeek = 2;
+ };
+ };
+}
+
+
+ If you want to alter the configuration file on your own, you can do this manually with
+ an expression like this:
+{ lib, ... }:
+{
+ environment.etc."grocy/config.php".text = lib.mkAfter ''
+ // Arbitrary PHP code in grocy's configuration file
+ '';
+}
+
+
+
+
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index bdac56169fd..5f2d2858163 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -96,6 +96,7 @@ in
freeswitch = handleTest ./freeswitch.nix {};
fsck = handleTest ./fsck.nix {};
gotify-server = handleTest ./gotify-server.nix {};
+ grocy = handleTest ./grocy.nix {};
gitea = handleTest ./gitea.nix {};
gitlab = handleTest ./gitlab.nix {};
gitolite = handleTest ./gitolite.nix {};
diff --git a/nixos/tests/grocy.nix b/nixos/tests/grocy.nix
new file mode 100644
index 00000000000..7fa479ed2c4
--- /dev/null
+++ b/nixos/tests/grocy.nix
@@ -0,0 +1,47 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+ name = "grocy";
+ meta = with pkgs.stdenv.lib.maintainers; {
+ maintainers = [ ma27 ];
+ };
+
+ machine = { pkgs, ... }: {
+ services.grocy = {
+ enable = true;
+ hostName = "localhost";
+ nginx.enableSSL = false;
+ };
+ environment.systemPackages = [ pkgs.jq ];
+ };
+
+ testScript = ''
+ machine.start()
+ machine.wait_for_open_port(80)
+ machine.wait_for_unit("multi-user.target")
+
+ machine.succeed("curl -sSf http://localhost")
+
+ machine.succeed(
+ "curl -c cookies -sSf -X POST http://localhost/login -d 'username=admin&password=admin'"
+ )
+
+ cookie = machine.succeed(
+ "grep -v '^#' cookies | awk '{ print $7 }' | sed -e '/^$/d' | perl -pe 'chomp'"
+ )
+
+ machine.succeed(
+ f"curl -sSf -X POST http://localhost/api/objects/tasks -b 'grocy_session={cookie}' "
+ + '-d \'{"assigned_to_user_id":1,"name":"Test Task","due_date":"1970-01-01"}\'''
+ + " --header 'Content-Type: application/json'"
+ )
+
+ task_name = machine.succeed(
+ f"curl -sSf http://localhost/api/tasks -b 'grocy_session={cookie}' --header 'Accept: application/json' | jq '.[].name' | xargs echo | perl -pe 'chomp'"
+ )
+
+ assert task_name == "Test Task"
+
+ machine.succeed("curl -sSfI http://localhost/api/tasks 2>&1 | grep '401 Unauthorized'")
+
+ machine.shutdown()
+ '';
+})
diff --git a/pkgs/servers/grocy/config-locations.patch b/pkgs/servers/grocy/config-locations.patch
new file mode 100644
index 00000000000..475be78ec20
--- /dev/null
+++ b/pkgs/servers/grocy/config-locations.patch
@@ -0,0 +1,61 @@
+diff --git a/app.php b/app.php
+index 5f91e4d..09c6010 100644
+--- a/app.php
++++ b/app.php
+@@ -23,7 +23,7 @@ else
+ require_once __DIR__ . '/vendor/autoload.php';
+
+ // Load config files
+-require_once GROCY_DATAPATH . '/config.php';
++require_once getenv('GROCY_CONFIG_FILE');
+ require_once __DIR__ . '/config-dist.php'; // For not in own config defined values we use the default ones
+
+ // Definitions for dev/demo/prerelease mode
+@@ -49,7 +49,7 @@ $appContainer = new \Slim\Container([
+ ],
+ 'view' => function($container)
+ {
+- return new \Slim\Views\Blade(__DIR__ . '/views', GROCY_DATAPATH . '/viewcache');
++ return new \Slim\Views\Blade(__DIR__ . '/views', getenv('GROCY_CACHE_DIR'));
+ },
+ 'LoginControllerInstance' => function($container)
+ {
+diff --git a/services/DatabaseService.php b/services/DatabaseService.php
+index 0bcf9b8..ec45e93 100644
+--- a/services/DatabaseService.php
++++ b/services/DatabaseService.php
+@@ -13,7 +13,7 @@ class DatabaseService
+ return GROCY_DATAPATH . '/grocy_' . GROCY_CULTURE . '.db';
+ }
+
+- return GROCY_DATAPATH . '/grocy.db';
++ return getenv('GROCY_DB_FILE');
+ }
+
+ private $DbConnectionRaw;
+diff --git a/services/FilesService.php b/services/FilesService.php
+index 7933b73..f52657e 100644
+--- a/services/FilesService.php
++++ b/services/FilesService.php
+@@ -12,7 +12,7 @@ class FilesService extends BaseService
+ {
+ parent::__construct();
+
+- $this->StoragePath = GROCY_DATAPATH . '/storage';
++ $this->StoragePath = getenv('GROCY_STORAGE_DIR');
+
+ if (!file_exists($this->StoragePath))
+ {
+diff --git a/services/StockService.php b/services/StockService.php
+index d7482ef..d1399a7 100644
+--- a/services/StockService.php
++++ b/services/StockService.php
+@@ -933,7 +933,7 @@ class StockService extends BaseService
+ throw new \Exception('No barcode lookup plugin defined');
+ }
+
+- $path = GROCY_DATAPATH . "/plugins/$pluginName.php";
++ $path = getenv('GROCY_PLUGIN_DIR');
+ if (file_exists($path))
+ {
+ require_once $path;
diff --git a/pkgs/servers/grocy/default.nix b/pkgs/servers/grocy/default.nix
new file mode 100644
index 00000000000..7af59f6904c
--- /dev/null
+++ b/pkgs/servers/grocy/default.nix
@@ -0,0 +1,32 @@
+{ stdenv, fetchurl, unzip }:
+
+stdenv.mkDerivation rec {
+ pname = "grocy";
+ version = "2.6.0";
+
+ src = fetchurl {
+ url = "https://github.com/grocy/grocy/releases/download/v${version}/grocy_${version}.zip";
+ sha256 = "1d4hy495in7p0i4fnhai1yqhjhmblv1g30siggmqpjrzdiiw3bak";
+ };
+
+ nativeBuildInputs = [ unzip ];
+ unpackPhase = ''
+ unzip ${src} -d .
+ '';
+
+ patches = [ ./config-locations.patch ];
+
+ dontBuild = true;
+
+ installPhase = ''
+ mkdir -p $out/
+ cp -R . $out/
+ '';
+
+ meta = with stdenv.lib; {
+ license = licenses.mit;
+ maintainers = with maintainers; [ ma27 ];
+ description = "ERP beyond your fridge - grocy is a web-based self-hosted groceries & household management solution for your home";
+ homepage = "https://grocy.info/";
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 3140dd17bf7..d68bdccef67 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -5278,6 +5278,8 @@ in
inherit (darwin.apple_sdk.frameworks) Security;
};
+ grocy = callPackage ../servers/grocy { };
+
nextcloud = callPackage ../servers/nextcloud { };
nextcloud-client = libsForQt5.callPackage ../applications/networking/nextcloud-client { };