grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / plausible.nix
blob188b80ca43a1d414d3fcd375e5af9154534232c7
1 { lib, pkgs, config, ... }:
3 with lib;
5 let
6   cfg = config.services.plausible;
8 in {
9   options.services.plausible = {
10     enable = mkEnableOption "plausible";
12     package = mkPackageOption pkgs "plausible" { };
14     adminUser = {
15       name = mkOption {
16         default = "admin";
17         type = types.str;
18         description = ''
19           Name of the admin user that plausible will created on initial startup.
20         '';
21       };
23       email = mkOption {
24         type = types.str;
25         example = "admin@localhost";
26         description = ''
27           Email-address of the admin-user.
28         '';
29       };
31       passwordFile = mkOption {
32         type = types.either types.str types.path;
33         description = ''
34           Path to the file which contains the password of the admin user.
35         '';
36       };
38       activate = mkEnableOption "activating the freshly created admin-user";
39     };
41     database = {
42       clickhouse = {
43         setup = mkEnableOption "creating a clickhouse instance" // { default = true; };
44         url = mkOption {
45           default = "http://localhost:8123/default";
46           type = types.str;
47           description = ''
48             The URL to be used to connect to `clickhouse`.
49           '';
50         };
51       };
52       postgres = {
53         setup = mkEnableOption "creating a postgresql instance" // { default = true; };
54         dbname = mkOption {
55           default = "plausible";
56           type = types.str;
57           description = ''
58             Name of the database to use.
59           '';
60         };
61         socket = mkOption {
62           default = "/run/postgresql";
63           type = types.str;
64           description = ''
65             Path to the UNIX domain-socket to communicate with `postgres`.
66           '';
67         };
68       };
69     };
71     server = {
72       disableRegistration = mkOption {
73         default = true;
74         type = types.enum [true false "invite_only"];
75         description = ''
76           Whether to prohibit creating an account in plausible's UI or allow on `invite_only`.
77         '';
78       };
79       secretKeybaseFile = mkOption {
80         type = types.either types.path types.str;
81         description = ''
82           Path to the secret used by the `phoenix`-framework. Instructions
83           how to generate one are documented in the
84           [
85           framework docs](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html#content).
86         '';
87       };
88       listenAddress = mkOption {
89         default = "127.0.0.1";
90         type = types.str;
91         description = ''
92           The IP address on which the server is listening.
93         '';
94       };
95       port = mkOption {
96         default = 8000;
97         type = types.port;
98         description = ''
99           Port where the service should be available.
100         '';
101       };
102       baseUrl = mkOption {
103         type = types.str;
104         description = ''
105           Public URL where plausible is available.
107           Note that `/path` components are currently ignored:
108           [
109             https://github.com/plausible/analytics/issues/1182
110           ](https://github.com/plausible/analytics/issues/1182).
111         '';
112       };
113     };
115     mail = {
116       email = mkOption {
117         default = "hello@plausible.local";
118         type = types.str;
119         description = ''
120           The email id to use for as *from* address of all communications
121           from Plausible.
122         '';
123       };
124       smtp = {
125         hostAddr = mkOption {
126           default = "localhost";
127           type = types.str;
128           description = ''
129             The host address of your smtp server.
130           '';
131         };
132         hostPort = mkOption {
133           default = 25;
134           type = types.port;
135           description = ''
136             The port of your smtp server.
137           '';
138         };
139         user = mkOption {
140           default = null;
141           type = types.nullOr types.str;
142           description = ''
143             The username/email in case SMTP auth is enabled.
144           '';
145         };
146         passwordFile = mkOption {
147           default = null;
148           type = with types; nullOr (either str path);
149           description = ''
150             The path to the file with the password in case SMTP auth is enabled.
151           '';
152         };
153         enableSSL = mkEnableOption "SSL when connecting to the SMTP server";
154         retries = mkOption {
155           type = types.ints.unsigned;
156           default = 2;
157           description = ''
158             Number of retries to make until mailer gives up.
159           '';
160         };
161       };
162     };
163   };
165   imports = [
166     (mkRemovedOptionModule [ "services" "plausible" "releaseCookiePath" ] "Plausible uses no distributed Erlang features, so this option is no longer necessary and was removed")
167   ];
169   config = mkIf cfg.enable {
170     assertions = [
171       { assertion = cfg.adminUser.activate -> cfg.database.postgres.setup;
172         message = ''
173           Unable to automatically activate the admin-user if no locally managed DB for
174           postgres (`services.plausible.database.postgres.setup') is enabled!
175         '';
176       }
177     ];
179     services.postgresql = mkIf cfg.database.postgres.setup {
180       enable = true;
181     };
183     services.clickhouse = mkIf cfg.database.clickhouse.setup {
184       enable = true;
185     };
187     environment.systemPackages = [ cfg.package ];
189     systemd.services = mkMerge [
190       {
191         plausible = {
192           inherit (cfg.package.meta) description;
193           documentation = [ "https://plausible.io/docs/self-hosting" ];
194           wantedBy = [ "multi-user.target" ];
195           after = optional cfg.database.clickhouse.setup "clickhouse.service"
196           ++ optionals cfg.database.postgres.setup [
197               "postgresql.service"
198               "plausible-postgres.service"
199             ];
200           requires = optional cfg.database.clickhouse.setup "clickhouse.service"
201             ++ optionals cfg.database.postgres.setup [
202               "postgresql.service"
203               "plausible-postgres.service"
204             ];
206           environment = {
207             # NixOS specific option to avoid that it's trying to write into its store-path.
208             # See also https://github.com/lau/tzdata#data-directory-and-releases
209             STORAGE_DIR = "/var/lib/plausible/elixir_tzdata";
211             # Configuration options from
212             # https://plausible.io/docs/self-hosting-configuration
213             PORT = toString cfg.server.port;
214             LISTEN_IP = cfg.server.listenAddress;
216             # Note [plausible-needs-no-erlang-distributed-features]:
217             # Plausible does not use, and does not plan to use, any of
218             # Erlang's distributed features, see:
219             #     https://github.com/plausible/analytics/pull/1190#issuecomment-1018820934
220             # Thus, disable distribution for improved simplicity and security:
221             #
222             # When distribution is enabled,
223             # Elixir spwans the Erlang VM, which will listen by default on all
224             # interfaces for messages between Erlang nodes (capable of
225             # remote code execution); it can be protected by a cookie; see
226             # https://erlang.org/doc/reference_manual/distributed.html#security).
227             #
228             # It would be possible to restrict the interface to one of our choice
229             # (e.g. localhost or a VPN IP) similar to how we do it with `listenAddress`
230             # for the Plausible web server; if distribution is ever needed in the future,
231             # https://github.com/NixOS/nixpkgs/pull/130297 shows how to do it.
232             #
233             # But since Plausible does not use this feature in any way,
234             # we just disable it.
235             RELEASE_DISTRIBUTION = "none";
236             # Additional safeguard, in case `RELEASE_DISTRIBUTION=none` ever
237             # stops disabling the start of EPMD.
238             ERL_EPMD_ADDRESS = "127.0.0.1";
240             DISABLE_REGISTRATION = if isBool cfg.server.disableRegistration then boolToString cfg.server.disableRegistration else cfg.server.disableRegistration;
242             RELEASE_TMP = "/var/lib/plausible/tmp";
243             # Home is needed to connect to the node with iex
244             HOME = "/var/lib/plausible";
246             ADMIN_USER_NAME = cfg.adminUser.name;
247             ADMIN_USER_EMAIL = cfg.adminUser.email;
249             DATABASE_SOCKET_DIR = cfg.database.postgres.socket;
250             DATABASE_NAME = cfg.database.postgres.dbname;
251             CLICKHOUSE_DATABASE_URL = cfg.database.clickhouse.url;
253             BASE_URL = cfg.server.baseUrl;
255             MAILER_EMAIL = cfg.mail.email;
256             SMTP_HOST_ADDR = cfg.mail.smtp.hostAddr;
257             SMTP_HOST_PORT = toString cfg.mail.smtp.hostPort;
258             SMTP_RETRIES = toString cfg.mail.smtp.retries;
259             SMTP_HOST_SSL_ENABLED = boolToString cfg.mail.smtp.enableSSL;
261             SELFHOST = "true";
262           } // (optionalAttrs (cfg.mail.smtp.user != null) {
263             SMTP_USER_NAME = cfg.mail.smtp.user;
264           });
266           path = [ cfg.package ]
267             ++ optional cfg.database.postgres.setup config.services.postgresql.package;
268           script = ''
269             # Elixir does not start up if `RELEASE_COOKIE` is not set,
270             # even though we set `RELEASE_DISTRIBUTION=none` so the cookie should be unused.
271             # Thus, make a random one, which should then be ignored.
272             export RELEASE_COOKIE=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 20)
273             export ADMIN_USER_PWD="$(< $CREDENTIALS_DIRECTORY/ADMIN_USER_PWD )"
274             export SECRET_KEY_BASE="$(< $CREDENTIALS_DIRECTORY/SECRET_KEY_BASE )"
276             ${lib.optionalString (cfg.mail.smtp.passwordFile != null)
277               ''export SMTP_USER_PWD="$(< $CREDENTIALS_DIRECTORY/SMTP_USER_PWD )"''}
279             ${lib.optionalString cfg.database.postgres.setup ''
280               # setup
281               ${cfg.package}/createdb.sh
282             ''}
284             ${cfg.package}/migrate.sh
285             export IP_GEOLOCATION_DB=${pkgs.dbip-country-lite}/share/dbip/dbip-country-lite.mmdb
286             ${cfg.package}/bin/plausible eval "(Plausible.Release.prepare() ; Plausible.Auth.create_user(\"$ADMIN_USER_NAME\", \"$ADMIN_USER_EMAIL\", \"$ADMIN_USER_PWD\"))"
287             ${optionalString cfg.adminUser.activate ''
288               psql -d plausible <<< "UPDATE users SET email_verified=true where email = '$ADMIN_USER_EMAIL';"
289             ''}
291             exec plausible start
292           '';
294           serviceConfig = {
295             DynamicUser = true;
296             PrivateTmp = true;
297             WorkingDirectory = "/var/lib/plausible";
298             StateDirectory = "plausible";
299             LoadCredential = [
300               "ADMIN_USER_PWD:${cfg.adminUser.passwordFile}"
301               "SECRET_KEY_BASE:${cfg.server.secretKeybaseFile}"
302             ] ++ lib.optionals (cfg.mail.smtp.passwordFile != null) [ "SMTP_USER_PWD:${cfg.mail.smtp.passwordFile}"];
303           };
304         };
305       }
306       (mkIf cfg.database.postgres.setup {
307         # `plausible' requires the `citext'-extension.
308         plausible-postgres = {
309           after = [ "postgresql.service" ];
310           partOf = [ "plausible.service" ];
311           serviceConfig = {
312             Type = "oneshot";
313             User = config.services.postgresql.superUser;
314             RemainAfterExit = true;
315           };
316           script = with cfg.database.postgres; ''
317             PSQL() {
318               ${config.services.postgresql.package}/bin/psql --port=5432 "$@"
319             }
320             # check if the database already exists
321             if ! PSQL -lqt | ${pkgs.coreutils}/bin/cut -d \| -f 1 | ${pkgs.gnugrep}/bin/grep -qw ${dbname} ; then
322               PSQL -tAc "CREATE ROLE plausible WITH LOGIN;"
323               PSQL -tAc "CREATE DATABASE ${dbname} WITH OWNER plausible;"
324               PSQL -d ${dbname} -tAc "CREATE EXTENSION IF NOT EXISTS citext;"
325             fi
326           '';
327         };
328       })
329     ];
330   };
332   meta.maintainers = teams.cyberus.members;
333   meta.doc = ./plausible.md;