1{ lib, pkgs, config, ... }:
2
3with lib;
4
5let
6 cfg = config.services.plausible;
7
8in {
9 options.services.plausible = {
10 enable = mkEnableOption "plausible";
11
12 releaseCookiePath = mkOption {
13 default = null;
14 type = with types; nullOr (either str path);
15 description = ''
16 The path to the file with release cookie. (used for remote connection to the running node).
17 '';
18 };
19
20 adminUser = {
21 name = mkOption {
22 default = "admin";
23 type = types.str;
24 description = ''
25 Name of the admin user that plausible will created on initial startup.
26 '';
27 };
28
29 email = mkOption {
30 type = types.str;
31 example = "admin@localhost";
32 description = ''
33 Email-address of the admin-user.
34 '';
35 };
36
37 passwordFile = mkOption {
38 type = types.either types.str types.path;
39 description = ''
40 Path to the file which contains the password of the admin user.
41 '';
42 };
43
44 activate = mkEnableOption "activating the freshly created admin-user";
45 };
46
47 database = {
48 clickhouse = {
49 setup = mkEnableOption "creating a clickhouse instance" // { default = true; };
50 url = mkOption {
51 default = "http://localhost:8123/default";
52 type = types.str;
53 description = ''
54 The URL to be used to connect to <package>clickhouse</package>.
55 '';
56 };
57 };
58 postgres = {
59 setup = mkEnableOption "creating a postgresql instance" // { default = true; };
60 dbname = mkOption {
61 default = "plausible";
62 type = types.str;
63 description = ''
64 Name of the database to use.
65 '';
66 };
67 socket = mkOption {
68 default = "/run/postgresql";
69 type = types.str;
70 description = ''
71 Path to the UNIX domain-socket to communicate with <package>postgres</package>.
72 '';
73 };
74 };
75 };
76
77 server = {
78 disableRegistration = mkOption {
79 default = true;
80 type = types.bool;
81 description = ''
82 Whether to prohibit creating an account in plausible's UI.
83 '';
84 };
85 secretKeybaseFile = mkOption {
86 type = types.either types.path types.str;
87 description = ''
88 Path to the secret used by the <literal>phoenix</literal>-framework. Instructions
89 how to generate one are documented in the
90 <link xlink:href="https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html#content">
91 framework docs</link>.
92 '';
93 };
94 port = mkOption {
95 default = 8000;
96 type = types.port;
97 description = ''
98 Port where the service should be available.
99 '';
100 };
101 baseUrl = mkOption {
102 type = types.str;
103 description = ''
104 Public URL where plausible is available.
105
106 Note that <literal>/path</literal> components are currently ignored:
107 <link xlink:href="https://github.com/plausible/analytics/issues/1182">
108 https://github.com/plausible/analytics/issues/1182
109 </link>.
110 '';
111 };
112 };
113
114 mail = {
115 email = mkOption {
116 default = "hello@plausible.local";
117 type = types.str;
118 description = ''
119 The email id to use for as <emphasis>from</emphasis> address of all communications
120 from Plausible.
121 '';
122 };
123 smtp = {
124 hostAddr = mkOption {
125 default = "localhost";
126 type = types.str;
127 description = ''
128 The host address of your smtp server.
129 '';
130 };
131 hostPort = mkOption {
132 default = 25;
133 type = types.port;
134 description = ''
135 The port of your smtp server.
136 '';
137 };
138 user = mkOption {
139 default = null;
140 type = types.nullOr types.str;
141 description = ''
142 The username/email in case SMTP auth is enabled.
143 '';
144 };
145 passwordFile = mkOption {
146 default = null;
147 type = with types; nullOr (either str path);
148 description = ''
149 The path to the file with the password in case SMTP auth is enabled.
150 '';
151 };
152 enableSSL = mkEnableOption "SSL when connecting to the SMTP server";
153 retries = mkOption {
154 type = types.ints.unsigned;
155 default = 2;
156 description = ''
157 Number of retries to make until mailer gives up.
158 '';
159 };
160 };
161 };
162 };
163
164 config = mkIf cfg.enable {
165 assertions = [
166 { assertion = cfg.adminUser.activate -> cfg.database.postgres.setup;
167 message = ''
168 Unable to automatically activate the admin-user if no locally managed DB for
169 postgres (`services.plausible.database.postgres.setup') is enabled!
170 '';
171 }
172 ];
173
174 services.postgresql = mkIf cfg.database.postgres.setup {
175 enable = true;
176 };
177
178 services.clickhouse = mkIf cfg.database.clickhouse.setup {
179 enable = true;
180 };
181
182 services.epmd.enable = true;
183
184 environment.systemPackages = [ pkgs.plausible ];
185
186 systemd.services = mkMerge [
187 {
188 plausible = {
189 inherit (pkgs.plausible.meta) description;
190 documentation = [ "https://plausible.io/docs/self-hosting" ];
191 wantedBy = [ "multi-user.target" ];
192 after = optionals cfg.database.postgres.setup [ "postgresql.service" "plausible-postgres.service" ];
193 requires = optional cfg.database.clickhouse.setup "clickhouse.service"
194 ++ optionals cfg.database.postgres.setup [
195 "postgresql.service"
196 "plausible-postgres.service"
197 ];
198
199 environment = {
200 # NixOS specific option to avoid that it's trying to write into its store-path.
201 # See also https://github.com/lau/tzdata#data-directory-and-releases
202 STORAGE_DIR = "/var/lib/plausible/elixir_tzdata";
203
204 # Configuration options from
205 # https://plausible.io/docs/self-hosting-configuration
206 PORT = toString cfg.server.port;
207 DISABLE_REGISTRATION = boolToString cfg.server.disableRegistration;
208
209 RELEASE_TMP = "/var/lib/plausible/tmp";
210 # Home is needed to connect to the node with iex
211 HOME = "/var/lib/plausible";
212
213 ADMIN_USER_NAME = cfg.adminUser.name;
214 ADMIN_USER_EMAIL = cfg.adminUser.email;
215
216 DATABASE_SOCKET_DIR = cfg.database.postgres.socket;
217 DATABASE_NAME = cfg.database.postgres.dbname;
218 CLICKHOUSE_DATABASE_URL = cfg.database.clickhouse.url;
219
220 BASE_URL = cfg.server.baseUrl;
221
222 MAILER_EMAIL = cfg.mail.email;
223 SMTP_HOST_ADDR = cfg.mail.smtp.hostAddr;
224 SMTP_HOST_PORT = toString cfg.mail.smtp.hostPort;
225 SMTP_RETRIES = toString cfg.mail.smtp.retries;
226 SMTP_HOST_SSL_ENABLED = boolToString cfg.mail.smtp.enableSSL;
227
228 SELFHOST = "true";
229 } // (optionalAttrs (cfg.mail.smtp.user != null) {
230 SMTP_USER_NAME = cfg.mail.smtp.user;
231 });
232
233 path = [ pkgs.plausible ]
234 ++ optional cfg.database.postgres.setup config.services.postgresql.package;
235 script = ''
236 export CONFIG_DIR=$CREDENTIALS_DIRECTORY
237
238 # setup
239 ${pkgs.plausible}/createdb.sh
240 ${pkgs.plausible}/migrate.sh
241 ${optionalString cfg.adminUser.activate ''
242 if ! ${pkgs.plausible}/init-admin.sh | grep 'already exists'; then
243 psql -d plausible <<< "UPDATE users SET email_verified=true;"
244 fi
245 ''}
246 ${optionalString (cfg.releaseCookiePath != null) ''
247 export RELEASE_COOKIE="$(< $CREDENTIALS_DIRECTORY/RELEASE_COOKIE )"
248 ''}
249 plausible start
250 '';
251
252 serviceConfig = {
253 DynamicUser = true;
254 PrivateTmp = true;
255 WorkingDirectory = "/var/lib/plausible";
256 StateDirectory = "plausible";
257 LoadCredential = [
258 "ADMIN_USER_PWD:${cfg.adminUser.passwordFile}"
259 "SECRET_KEY_BASE:${cfg.server.secretKeybaseFile}"
260 ] ++ lib.optionals (cfg.mail.smtp.passwordFile != null) [ "SMTP_USER_PWD:${cfg.mail.smtp.passwordFile}"]
261 ++ lib.optionals (cfg.releaseCookiePath != null) [ "RELEASE_COOKIE:${cfg.releaseCookiePath}"];
262 };
263 };
264 }
265 (mkIf cfg.database.postgres.setup {
266 # `plausible' requires the `citext'-extension.
267 plausible-postgres = {
268 after = [ "postgresql.service" ];
269 partOf = [ "plausible.service" ];
270 serviceConfig = {
271 Type = "oneshot";
272 User = config.services.postgresql.superUser;
273 RemainAfterExit = true;
274 };
275 script = with cfg.database.postgres; ''
276 PSQL() {
277 ${config.services.postgresql.package}/bin/psql --port=5432 "$@"
278 }
279 # check if the database already exists
280 if ! PSQL -lqt | ${pkgs.coreutils}/bin/cut -d \| -f 1 | ${pkgs.gnugrep}/bin/grep -qw ${dbname} ; then
281 PSQL -tAc "CREATE ROLE plausible WITH LOGIN;"
282 PSQL -tAc "CREATE DATABASE ${dbname} WITH OWNER plausible;"
283 PSQL -d ${dbname} -tAc "CREATE EXTENSION IF NOT EXISTS citext;"
284 fi
285 '';
286 };
287 })
288 ];
289 };
290
291 meta.maintainers = with maintainers; [ ma27 ];
292 meta.doc = ./plausible.xml;
293}