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