at 25.11-pre 16 kB view raw
1{ 2 pkgs, 3 lib, 4 config, 5 ... 6}: 7 8let 9 inherit (lib) 10 mkEnableOption 11 mkPackageOption 12 mkOption 13 mkDefault 14 mkIf 15 types 16 literalExpression 17 ; 18 19 cfg = config.services.mobilizon; 20 21 user = "mobilizon"; 22 group = "mobilizon"; 23 24 settingsFormat = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; }; 25 26 configFile = settingsFormat.generate "mobilizon-config.exs" cfg.settings; 27 28 # Make a package containing launchers with the correct envirenment, instead of 29 # setting it with systemd services, so that the user can also use them without 30 # troubles 31 launchers = 32 pkgs.runCommand "${cfg.package.pname}-launchers-${cfg.package.version}" 33 { 34 src = cfg.package; 35 nativeBuildInputs = with pkgs; [ makeWrapper ]; 36 } 37 '' 38 mkdir -p $out/bin 39 40 makeWrapper \ 41 $src/bin/mobilizon \ 42 $out/bin/mobilizon \ 43 --run '. ${secretEnvFile}' \ 44 --set MOBILIZON_CONFIG_PATH "${configFile}" \ 45 --set-default RELEASE_TMP "/tmp" 46 47 makeWrapper \ 48 $src/bin/mobilizon_ctl \ 49 $out/bin/mobilizon_ctl \ 50 --run '. ${secretEnvFile}' \ 51 --set MOBILIZON_CONFIG_PATH "${configFile}" \ 52 --set-default RELEASE_TMP "/tmp" 53 ''; 54 55 repoSettings = cfg.settings.":mobilizon"."Mobilizon.Storage.Repo"; 56 instanceSettings = cfg.settings.":mobilizon".":instance"; 57 58 isLocalPostgres = repoSettings.socket_dir != null; 59 60 dbUser = if repoSettings.username != null then repoSettings.username else "mobilizon"; 61 62 postgresql = config.services.postgresql.package; 63 postgresqlSocketDir = "/run/postgresql"; 64 65 secretEnvFile = "/var/lib/mobilizon/secret-env.sh"; 66in 67{ 68 options = { 69 services.mobilizon = { 70 enable = mkEnableOption "Mobilizon federated organization and mobilization platform"; 71 72 nginx.enable = lib.mkOption { 73 type = lib.types.bool; 74 default = true; 75 description = '' 76 Whether an Nginx virtual host should be 77 set up to serve Mobilizon. 78 ''; 79 }; 80 81 package = mkPackageOption pkgs "mobilizon" { }; 82 83 settings = mkOption { 84 type = 85 let 86 elixirTypes = settingsFormat.lib.types; 87 in 88 types.submodule { 89 freeformType = settingsFormat.type; 90 91 options = { 92 ":mobilizon" = { 93 94 "Mobilizon.Web.Endpoint" = { 95 url.host = mkOption { 96 type = elixirTypes.str; 97 defaultText = lib.literalMD '' 98 ''${settings.":mobilizon".":instance".hostname} 99 ''; 100 description = '' 101 Your instance's hostname for generating URLs throughout the app 102 ''; 103 }; 104 105 http = { 106 port = mkOption { 107 type = elixirTypes.port; 108 default = 4000; 109 description = '' 110 The port to run the server 111 ''; 112 }; 113 ip = mkOption { 114 type = elixirTypes.tuple; 115 default = settingsFormat.lib.mkTuple [ 116 0 117 0 118 0 119 0 120 0 121 0 122 0 123 1 124 ]; 125 description = '' 126 The IP address to listen on. Defaults to [::1] notated as a byte tuple. 127 ''; 128 }; 129 }; 130 131 has_reverse_proxy = mkOption { 132 type = elixirTypes.bool; 133 default = true; 134 description = '' 135 Whether you use a reverse proxy 136 ''; 137 }; 138 }; 139 140 ":instance" = { 141 name = mkOption { 142 type = elixirTypes.str; 143 description = '' 144 The fallback instance name if not configured into the admin UI 145 ''; 146 }; 147 148 hostname = mkOption { 149 type = elixirTypes.str; 150 description = '' 151 Your instance's hostname 152 ''; 153 }; 154 155 email_from = mkOption { 156 type = elixirTypes.str; 157 defaultText = literalExpression '' 158 noreply@''${settings.":mobilizon".":instance".hostname} 159 ''; 160 description = '' 161 The email for the From: header in emails 162 ''; 163 }; 164 165 email_reply_to = mkOption { 166 type = elixirTypes.str; 167 defaultText = literalExpression '' 168 ''${email_from} 169 ''; 170 description = '' 171 The email for the Reply-To: header in emails 172 ''; 173 }; 174 }; 175 176 "Mobilizon.Storage.Repo" = { 177 socket_dir = mkOption { 178 type = types.nullOr elixirTypes.str; 179 default = postgresqlSocketDir; 180 description = '' 181 Path to the postgres socket directory. 182 183 Set this to null if you want to connect to a remote database. 184 185 If non-null, the local PostgreSQL server will be configured with 186 the configured database, permissions, and required extensions. 187 188 If connecting to a remote database, please follow the 189 instructions on how to setup your database: 190 <https://docs.joinmobilizon.org/administration/install/release/#database-setup> 191 ''; 192 }; 193 194 username = mkOption { 195 type = types.nullOr elixirTypes.str; 196 default = user; 197 description = '' 198 User used to connect to the database 199 ''; 200 }; 201 202 database = mkOption { 203 type = types.nullOr elixirTypes.str; 204 default = "mobilizon_prod"; 205 description = '' 206 Name of the database 207 ''; 208 }; 209 }; 210 }; 211 }; 212 }; 213 default = { }; 214 215 description = '' 216 Mobilizon Elixir documentation, see 217 <https://docs.joinmobilizon.org/administration/configure/reference/> 218 for supported values. 219 ''; 220 }; 221 }; 222 }; 223 224 config = mkIf cfg.enable { 225 226 assertions = [ 227 { 228 assertion = 229 cfg.nginx.enable 230 -> ( 231 cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.ip == settingsFormat.lib.mkTuple [ 232 0 233 0 234 0 235 0 236 0 237 0 238 0 239 1 240 ] 241 ); 242 message = "Setting the IP mobilizon listens on is only possible when the nginx config is not used, as it is hardcoded there."; 243 } 244 ]; 245 246 services.mobilizon.settings = { 247 ":mobilizon" = { 248 "Mobilizon.Web.Endpoint" = { 249 server = true; 250 url.host = mkDefault instanceSettings.hostname; 251 secret_key_base = settingsFormat.lib.mkGetEnv { envVariable = "MOBILIZON_INSTANCE_SECRET"; }; 252 }; 253 254 "Mobilizon.Web.Auth.Guardian".secret_key = settingsFormat.lib.mkGetEnv { 255 envVariable = "MOBILIZON_AUTH_SECRET"; 256 }; 257 258 ":instance" = { 259 registrations_open = mkDefault false; 260 demo = mkDefault false; 261 email_from = mkDefault "noreply@${instanceSettings.hostname}"; 262 email_reply_to = mkDefault instanceSettings.email_from; 263 }; 264 265 "Mobilizon.Storage.Repo" = { 266 # Forced by upstream since it uses PostgreSQL-specific extensions 267 adapter = settingsFormat.lib.mkAtom "Ecto.Adapters.Postgres"; 268 pool_size = mkDefault 10; 269 }; 270 }; 271 272 ":tzdata".":data_dir" = "/var/lib/mobilizon/tzdata/"; 273 }; 274 275 # This somewhat follows upstream's systemd service here: 276 # https://framagit.org/framasoft/mobilizon/-/blob/master/support/systemd/mobilizon.service 277 systemd.services.mobilizon = { 278 description = "Mobilizon federated organization and mobilization platform"; 279 280 wantedBy = [ "multi-user.target" ]; 281 282 path = with pkgs; [ 283 gawk 284 imagemagick 285 libwebp 286 file 287 288 # Optional: 289 gifsicle 290 jpegoptim 291 optipng 292 pngquant 293 ]; 294 295 serviceConfig = { 296 ExecStartPre = "${launchers}/bin/mobilizon_ctl migrate"; 297 ExecStart = "${launchers}/bin/mobilizon start"; 298 ExecStop = "${launchers}/bin/mobilizon stop"; 299 300 User = user; 301 Group = group; 302 303 StateDirectory = "mobilizon"; 304 305 Restart = "on-failure"; 306 307 PrivateTmp = true; 308 ProtectSystem = "full"; 309 NoNewPrivileges = true; 310 311 ReadWritePaths = mkIf isLocalPostgres postgresqlSocketDir; 312 }; 313 }; 314 315 # Create the needed secrets before running Mobilizon, so that they are not 316 # in the nix store 317 # 318 # Since some of these tasks are quite common for Elixir projects (COOKIE for 319 # every BEAM project, Phoenix and Guardian are also quite common), this 320 # service could be abstracted in the future, and used by other Elixir 321 # projects. 322 systemd.services.mobilizon-setup-secrets = { 323 description = "Mobilizon setup secrets"; 324 before = [ "mobilizon.service" ]; 325 wantedBy = [ "mobilizon.service" ]; 326 327 script = 328 let 329 # Taken from here: 330 # https://framagit.org/framasoft/mobilizon/-/blob/1.0.7/lib/mix/tasks/mobilizon/instance.ex#L132-133 331 genSecret = 332 "IO.puts(:crypto.strong_rand_bytes(64)" + "|> Base.encode64()" + "|> binary_part(0, 64))"; 333 334 # Taken from here: 335 # https://github.com/elixir-lang/elixir/blob/v1.11.3/lib/mix/lib/mix/release.ex#L499 336 genCookie = "IO.puts(Base.encode32(:crypto.strong_rand_bytes(32)))"; 337 338 evalElixir = str: '' 339 ${cfg.package.elixirPackage}/bin/elixir --eval '${str}' 340 ''; 341 in 342 '' 343 set -euxo pipefail 344 345 if [ ! -f "${secretEnvFile}" ]; then 346 install -m 600 /dev/null "${secretEnvFile}" 347 cat > "${secretEnvFile}" <<EOF 348 # This file was automatically generated by mobilizon-setup-secrets.service 349 export MOBILIZON_AUTH_SECRET='$(${evalElixir genSecret})' 350 export MOBILIZON_INSTANCE_SECRET='$(${evalElixir genSecret})' 351 export RELEASE_COOKIE='$(${evalElixir genCookie})' 352 EOF 353 fi 354 ''; 355 356 serviceConfig = { 357 Type = "oneshot"; 358 User = user; 359 Group = group; 360 StateDirectory = "mobilizon"; 361 }; 362 }; 363 364 # Add the required PostgreSQL extensions to the local PostgreSQL server, 365 # if local PostgreSQL is configured. 366 systemd.services.mobilizon-postgresql = mkIf isLocalPostgres { 367 description = "Mobilizon PostgreSQL setup"; 368 369 after = [ "postgresql.service" ]; 370 before = [ 371 "mobilizon.service" 372 "mobilizon-setup-secrets.service" 373 ]; 374 wantedBy = [ "mobilizon.service" ]; 375 376 path = [ postgresql ]; 377 378 # Taken from here: 379 # https://framagit.org/framasoft/mobilizon/-/blob/1.1.0/priv/templates/setup_db.eex 380 # TODO(to maintainers of mobilizon): the owner database alteration is necessary 381 # as PostgreSQL 15 changed their behaviors w.r.t. to privileges. 382 # See https://github.com/NixOS/nixpkgs/issues/216989 to get rid 383 # of that workaround. 384 script = '' 385 psql "${repoSettings.database}" -c "\ 386 CREATE EXTENSION IF NOT EXISTS postgis; \ 387 CREATE EXTENSION IF NOT EXISTS pg_trgm; \ 388 CREATE EXTENSION IF NOT EXISTS unaccent;" 389 psql -tAc 'ALTER DATABASE "${repoSettings.database}" OWNER TO "${dbUser}";' 390 391 ''; 392 393 serviceConfig = { 394 Type = "oneshot"; 395 User = config.services.postgresql.superUser; 396 Restart = "on-failure"; 397 }; 398 }; 399 400 systemd.tmpfiles.rules = [ 401 "d /var/lib/mobilizon/sitemap 700 mobilizon mobilizon - -" 402 "d /var/lib/mobilizon/uploads/exports/csv 700 mobilizon mobilizon - -" 403 "Z /var/lib/mobilizon 700 mobilizon mobilizon - -" 404 ]; 405 406 services.postgresql = mkIf isLocalPostgres { 407 enable = true; 408 ensureDatabases = [ repoSettings.database ]; 409 ensureUsers = [ 410 { 411 name = dbUser; 412 # Given that `dbUser` is potentially arbitrarily custom, we will perform 413 # manual fixups in mobilizon-postgres. 414 # TODO(to maintainers of mobilizon): Feel free to simplify your setup by using `ensureDBOwnership`. 415 ensureDBOwnership = false; 416 } 417 ]; 418 extensions = ps: with ps; [ postgis ]; 419 }; 420 421 # Nginx config taken from support/nginx/mobilizon-release.conf 422 services.nginx = 423 let 424 inherit (cfg.settings.":mobilizon".":instance") hostname; 425 proxyPass = "http://[::1]:" + toString cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.port; 426 in 427 lib.mkIf cfg.nginx.enable { 428 enable = true; 429 virtualHosts."${hostname}" = { 430 enableACME = lib.mkDefault true; 431 forceSSL = lib.mkDefault true; 432 locations."/" = { 433 inherit proxyPass; 434 proxyWebsockets = true; 435 recommendedProxySettings = lib.mkDefault true; 436 extraConfig = '' 437 expires off; 438 add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always; 439 ''; 440 }; 441 locations."~ ^/(assets|img)" = { 442 root = "${cfg.package}/lib/mobilizon-${cfg.package.version}/priv/static"; 443 extraConfig = '' 444 access_log off; 445 add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable"; 446 ''; 447 }; 448 locations."~ ^/(media|proxy)" = { 449 inherit proxyPass; 450 recommendedProxySettings = lib.mkDefault true; 451 # Combination of HTTP/1.1 and disabled request buffering is 452 # needed to directly forward chunked responses 453 extraConfig = '' 454 proxy_http_version 1.1; 455 proxy_request_buffering off; 456 access_log off; 457 add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable"; 458 ''; 459 }; 460 }; 461 }; 462 463 users.users.${user} = { 464 description = "Mobilizon daemon user"; 465 group = group; 466 isSystemUser = true; 467 }; 468 469 users.groups.${group} = { }; 470 471 # So that we have the `mobilizon` and `mobilizon_ctl` commands. 472 # The `mobilizon remote` command is useful for dropping a shell into the 473 # running Mobilizon instance, and `mobilizon_ctl` is used for common 474 # management tasks (e.g. adding users). 475 environment.systemPackages = [ launchers ]; 476 }; 477 478 meta.maintainers = with lib.maintainers; [ 479 minijackson 480 erictapen 481 ]; 482}