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