1 { lib, pkgs, config, ... }:
6 cfg = config.services.plausible;
9 options.services.plausible = {
10 enable = mkEnableOption "plausible";
12 package = mkPackageOption pkgs "plausible" { };
19 Name of the admin user that plausible will created on initial startup.
25 example = "admin@localhost";
27 Email-address of the admin-user.
31 passwordFile = mkOption {
32 type = types.either types.str types.path;
34 Path to the file which contains the password of the admin user.
38 activate = mkEnableOption "activating the freshly created admin-user";
43 setup = mkEnableOption "creating a clickhouse instance" // { default = true; };
45 default = "http://localhost:8123/default";
48 The URL to be used to connect to `clickhouse`.
53 setup = mkEnableOption "creating a postgresql instance" // { default = true; };
55 default = "plausible";
58 Name of the database to use.
62 default = "/run/postgresql";
65 Path to the UNIX domain-socket to communicate with `postgres`.
72 disableRegistration = mkOption {
74 type = types.enum [true false "invite_only"];
76 Whether to prohibit creating an account in plausible's UI or allow on `invite_only`.
79 secretKeybaseFile = mkOption {
80 type = types.either types.path types.str;
82 Path to the secret used by the `phoenix`-framework. Instructions
83 how to generate one are documented in the
85 framework docs](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html#content).
88 listenAddress = mkOption {
89 default = "127.0.0.1";
92 The IP address on which the server is listening.
99 Port where the service should be available.
105 Public URL where plausible is available.
107 Note that `/path` components are currently ignored:
109 https://github.com/plausible/analytics/issues/1182
110 ](https://github.com/plausible/analytics/issues/1182).
117 default = "hello@plausible.local";
120 The email id to use for as *from* address of all communications
125 hostAddr = mkOption {
126 default = "localhost";
129 The host address of your smtp server.
132 hostPort = mkOption {
136 The port of your smtp server.
141 type = types.nullOr types.str;
143 The username/email in case SMTP auth is enabled.
146 passwordFile = mkOption {
148 type = with types; nullOr (either str path);
150 The path to the file with the password in case SMTP auth is enabled.
153 enableSSL = mkEnableOption "SSL when connecting to the SMTP server";
155 type = types.ints.unsigned;
158 Number of retries to make until mailer gives up.
166 (mkRemovedOptionModule [ "services" "plausible" "releaseCookiePath" ] "Plausible uses no distributed Erlang features, so this option is no longer necessary and was removed")
169 config = mkIf cfg.enable {
171 { assertion = cfg.adminUser.activate -> cfg.database.postgres.setup;
173 Unable to automatically activate the admin-user if no locally managed DB for
174 postgres (`services.plausible.database.postgres.setup') is enabled!
179 services.postgresql = mkIf cfg.database.postgres.setup {
183 services.clickhouse = mkIf cfg.database.clickhouse.setup {
187 environment.systemPackages = [ cfg.package ];
189 systemd.services = mkMerge [
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 [
198 "plausible-postgres.service"
200 requires = optional cfg.database.clickhouse.setup "clickhouse.service"
201 ++ optionals cfg.database.postgres.setup [
203 "plausible-postgres.service"
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:
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).
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.
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;
262 } // (optionalAttrs (cfg.mail.smtp.user != null) {
263 SMTP_USER_NAME = cfg.mail.smtp.user;
266 path = [ cfg.package ]
267 ++ optional cfg.database.postgres.setup config.services.postgresql.package;
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 ''
281 ${cfg.package}/createdb.sh
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';"
297 WorkingDirectory = "/var/lib/plausible";
298 StateDirectory = "plausible";
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}"];
306 (mkIf cfg.database.postgres.setup {
307 # `plausible' requires the `citext'-extension.
308 plausible-postgres = {
309 after = [ "postgresql.service" ];
310 partOf = [ "plausible.service" ];
313 User = config.services.postgresql.superUser;
314 RemainAfterExit = true;
316 script = with cfg.database.postgres; ''
318 ${config.services.postgresql.package}/bin/psql --port=5432 "$@"
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;"
332 meta.maintainers = teams.cyberus.members;
333 meta.doc = ./plausible.md;