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