diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 878b77969af1..402f222f4e92 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 000000000000..568bdfd0c429
--- /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 e1c299b8413e..8e938e57e67f 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 000000000000..7fa479ed2c42
--- /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()
+ '';
+})