grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / matomo.nix
blob722745dbdb5da18f0a38ea5389e93d42c75779f6
1 { config, lib, options, pkgs, ... }:
2 with lib;
3 let
4   cfg = config.services.matomo;
5   fpm = config.services.phpfpm.pools.${pool};
7   user = "matomo";
8   dataDir = "/var/lib/${user}";
9   deprecatedDataDir = "/var/lib/piwik";
11   pool = user;
12   phpExecutionUnit = "phpfpm-${pool}";
13   databaseService = "mysql.service";
15 in {
16   imports = [
17     (mkRenamedOptionModule [ "services" "piwik" "enable" ] [ "services" "matomo" "enable" ])
18     (mkRenamedOptionModule [ "services" "piwik" "webServerUser" ] [ "services" "matomo" "webServerUser" ])
19     (mkRemovedOptionModule [ "services" "piwik" "phpfpmProcessManagerConfig" ] "Use services.phpfpm.pools.<name>.settings")
20     (mkRemovedOptionModule [ "services" "matomo" "phpfpmProcessManagerConfig" ] "Use services.phpfpm.pools.<name>.settings")
21     (mkRenamedOptionModule [ "services" "piwik" "nginx" ] [ "services" "matomo" "nginx" ])
22     (mkRenamedOptionModule [ "services" "matomo" "periodicArchiveProcessingUrl" ] [ "services" "matomo" "hostname" ])
23   ];
25   options = {
26     services.matomo = {
27       # NixOS PR for database setup: https://github.com/NixOS/nixpkgs/pull/6963
28       # Matomo issue for automatic Matomo setup: https://github.com/matomo-org/matomo/issues/10257
29       # TODO: find a nice way to do this when more NixOS MySQL and / or Matomo automatic setup stuff is implemented.
30       enable = mkOption {
31         type = types.bool;
32         default = false;
33         description = ''
34           Enable Matomo web analytics with php-fpm backend.
35           Either the nginx option or the webServerUser option is mandatory.
36         '';
37       };
39       package = mkPackageOption pkgs "matomo" { };
41       webServerUser = mkOption {
42         type = types.nullOr types.str;
43         default = null;
44         example = "lighttpd";
45         description = ''
46           Name of the web server user that forwards requests to {option}`services.phpfpm.pools.<name>.socket` the fastcgi socket for Matomo if the nginx
47           option is not used. Either this option or the nginx option is mandatory.
48           If you want to use another webserver than nginx, you need to set this to that server's user
49           and pass fastcgi requests to `index.php`, `matomo.php` and `piwik.php` (legacy name) to this socket.
50         '';
51       };
53       periodicArchiveProcessing = mkOption {
54         type = types.bool;
55         default = true;
56         description = ''
57           Enable periodic archive processing, which generates aggregated reports from the visits.
59           This means that you can safely disable browser triggers for Matomo archiving,
60           and safely enable to delete old visitor logs.
61           Before deleting visitor logs,
62           make sure though that you run `systemctl start matomo-archive-processing.service`
63           at least once without errors if you have already collected data before.
64         '';
65       };
67       hostname = mkOption {
68         type = types.str;
69         default = "${user}.${config.networking.fqdnOrHostName}";
70         defaultText = literalExpression ''
71           "${user}.''${config.${options.networking.fqdnOrHostName}}"
72         '';
73         example = "matomo.yourdomain.org";
74         description = ''
75           URL of the host, without https prefix. You may want to change it if you
76           run Matomo on a different URL than matomo.yourdomain.
77         '';
78       };
80       nginx = mkOption {
81         type = types.nullOr (types.submodule (
82           recursiveUpdate
83             (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
84             {
85               # enable encryption by default,
86               # as sensitive login and Matomo data should not be transmitted in clear text.
87               options.forceSSL.default = true;
88               options.enableACME.default = true;
89             }
90         )
91         );
92         default = null;
93         example = literalExpression ''
94           {
95             serverAliases = [
96               "matomo.''${config.networking.domain}"
97               "stats.''${config.networking.domain}"
98             ];
99             enableACME = false;
100           }
101         '';
102         description = ''
103             With this option, you can customize an nginx virtualHost which already has sensible defaults for Matomo.
104             Either this option or the webServerUser option is mandatory.
105             Set this to {} to just enable the virtualHost if you don't need any customization.
106             If enabled, then by default, the {option}`serverName` is
107             `''${user}.''${config.networking.hostName}.''${config.networking.domain}`,
108             SSL is active, and certificates are acquired via ACME.
109             If this is set to null (the default), no nginx virtualHost will be configured.
110         '';
111       };
112     };
113   };
115   config = mkIf cfg.enable {
116     warnings = mkIf (cfg.nginx != null && cfg.webServerUser != null) [
117       "If services.matomo.nginx is set, services.matomo.nginx.webServerUser is ignored and should be removed."
118     ];
120     assertions = [ {
121         assertion = cfg.nginx != null || cfg.webServerUser != null;
122         message = "Either services.matomo.nginx or services.matomo.nginx.webServerUser is mandatory";
123     }];
125     users.users.${user} = {
126       isSystemUser = true;
127       createHome = true;
128       home = dataDir;
129       group  = user;
130     };
131     users.groups.${user} = {};
133     systemd.services.matomo-setup-update = {
134       # everything needs to set up and up to date before Matomo php files are executed
135       requiredBy = [ "${phpExecutionUnit}.service" ];
136       before = [ "${phpExecutionUnit}.service" ];
137       # the update part of the script can only work if the database is already up and running
138       requires = [ databaseService ];
139       after = [ databaseService ];
140       path = [ cfg.package ];
141       environment.PIWIK_USER_PATH = dataDir;
142       serviceConfig = {
143         Type = "oneshot";
144         User = user;
145         # hide especially config.ini.php from other
146         UMask = "0007";
147         # TODO: might get renamed to MATOMO_USER_PATH in future versions
148         # chown + chmod in preStart needs root
149         PermissionsStartOnly = true;
150       };
152       # correct ownership and permissions in case they're not correct anymore,
153       # e.g. after restoring from backup or moving from another system.
154       # Note that ${dataDir}/config/config.ini.php might contain the MySQL password.
155       preStart = ''
156         # migrate data from piwik to Matomo folder
157         if [ -d ${deprecatedDataDir} ]; then
158           echo "Migrating from ${deprecatedDataDir} to ${dataDir}"
159           mv -T ${deprecatedDataDir} ${dataDir}
160         fi
161         chown -R ${user}:${user} ${dataDir}
162         chmod -R ug+rwX,o-rwx ${dataDir}
164         if [ -e ${dataDir}/current-package ]; then
165           CURRENT_PACKAGE=$(readlink ${dataDir}/current-package)
166           NEW_PACKAGE=${cfg.package}
167           if [ "$CURRENT_PACKAGE" != "$NEW_PACKAGE" ]; then
168             # keeping tmp around between upgrades seems to bork stuff, so delete it
169             rm -rf ${dataDir}/tmp
170           fi
171         elif [ -e ${dataDir}/tmp ]; then
172           # upgrade from 4.4.1
173           rm -rf ${dataDir}/tmp
174         fi
175         ln -sfT ${cfg.package} ${dataDir}/current-package
176         '';
177       script = ''
178             # Use User-Private Group scheme to protect Matomo data, but allow administration / backup via 'matomo' group
179             # Copy config folder
180             chmod g+s "${dataDir}"
181             cp -r "${cfg.package}/share/config" "${dataDir}/"
182             mkdir -p "${dataDir}/misc"
183             chmod -R u+rwX,g+rwX,o-rwx "${dataDir}"
185             # check whether user setup has already been done
186             if test -f "${dataDir}/config/config.ini.php"; then
187               # then execute possibly pending database upgrade
188               matomo-console core:update --yes
189             fi
190       '';
191     };
193     # If this is run regularly via the timer,
194     # 'Browser trigger archiving' can be disabled in Matomo UI > Settings > General Settings.
195     systemd.services.matomo-archive-processing = {
196       description = "Archive Matomo reports";
197       # the archiving can only work if the database is already up and running
198       requires = [ databaseService ];
199       after = [ databaseService ];
201       # TODO: might get renamed to MATOMO_USER_PATH in future versions
202       environment.PIWIK_USER_PATH = dataDir;
203       serviceConfig = {
204         Type = "oneshot";
205         User = user;
206         UMask = "0007";
207         CPUSchedulingPolicy = "idle";
208         IOSchedulingClass = "idle";
209         ExecStart = "${cfg.package}/bin/matomo-console core:archive --url=https://${cfg.hostname}";
210       };
211     };
213     systemd.timers.matomo-archive-processing = mkIf cfg.periodicArchiveProcessing {
214       description = "Automatically archive Matomo reports every hour";
216       wantedBy = [ "timers.target" ];
217       timerConfig = {
218         OnCalendar = "hourly";
219         Persistent = "yes";
220         AccuracySec = "10m";
221       };
222     };
224     systemd.services.${phpExecutionUnit} = {
225       # stop phpfpm on package upgrade, do database upgrade via matomo-setup-update, and then restart
226       restartTriggers = [ cfg.package ];
227       # stop config.ini.php from getting written with read permission for others
228       serviceConfig.UMask = "0007";
229     };
231     services.phpfpm.pools = let
232       # workaround for when both are null and need to generate a string,
233       # which is illegal, but as assertions apparently are being triggered *after* config generation,
234       # we have to avoid already throwing errors at this previous stage.
235       socketOwner = if (cfg.nginx != null) then config.services.nginx.user
236       else if (cfg.webServerUser != null) then cfg.webServerUser else "";
237     in {
238       ${pool} = {
239         inherit user;
240         phpOptions = ''
241           error_log = 'stderr'
242           log_errors = on
243         '';
244         settings = mapAttrs (name: mkDefault) {
245           "listen.owner" = socketOwner;
246           "listen.group" = "root";
247           "listen.mode" = "0660";
248           "pm" = "dynamic";
249           "pm.max_children" = 75;
250           "pm.start_servers" = 10;
251           "pm.min_spare_servers" = 5;
252           "pm.max_spare_servers" = 20;
253           "pm.max_requests" = 500;
254           "catch_workers_output" = true;
255         };
256         phpEnv.PIWIK_USER_PATH = dataDir;
257       };
258     };
261     services.nginx.virtualHosts = mkIf (cfg.nginx != null) {
262       # References:
263       # https://fralef.me/piwik-hardening-with-nginx-and-php-fpm.html
264       # https://github.com/perusio/piwik-nginx
265       "${cfg.hostname}" = mkMerge [ cfg.nginx {
266         # don't allow to override the root easily, as it will almost certainly break Matomo.
267         # disadvantage: not shown as default in docs.
268         root = mkForce "${cfg.package}/share";
270         # define locations here instead of as the submodule option's default
271         # so that they can easily be extended with additional locations if required
272         # without needing to redefine the Matomo ones.
273         # disadvantage: not shown as default in docs.
274         locations."/" = {
275           index = "index.php";
276         };
277         # allow index.php for webinterface
278         locations."= /index.php".extraConfig = ''
279           fastcgi_pass unix:${fpm.socket};
280         '';
281         # allow matomo.php for tracking
282         locations."= /matomo.php".extraConfig = ''
283           fastcgi_pass unix:${fpm.socket};
284         '';
285         # allow piwik.php for tracking (deprecated name)
286         locations."= /piwik.php".extraConfig = ''
287           fastcgi_pass unix:${fpm.socket};
288         '';
289         # Any other attempt to access any php files is forbidden
290         locations."~* ^.+\\.php$".extraConfig = ''
291           return 403;
292         '';
293         # Disallow access to unneeded directories
294         # config and tmp are already removed
295         locations."~ ^/(?:core|lang|misc)/".extraConfig = ''
296           return 403;
297         '';
298         # Disallow access to several helper files
299         locations."~* \\.(?:bat|git|ini|sh|txt|tpl|xml|md)$".extraConfig = ''
300           return 403;
301         '';
302         # No crawling of this site for bots that obey robots.txt - no useful information here.
303         locations."= /robots.txt".extraConfig = ''
304           return 200 "User-agent: *\nDisallow: /\n";
305         '';
306         # let browsers cache matomo.js
307         locations."= /matomo.js".extraConfig = ''
308           expires 1M;
309         '';
310         # let browsers cache piwik.js (deprecated name)
311         locations."= /piwik.js".extraConfig = ''
312           expires 1M;
313         '';
314       }];
315     };
316   };
318   meta = {
319     doc = ./matomo.md;
320     maintainers = with lib.maintainers; [ florianjacob ];
321   };