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