11 cfg = config.services.mobilizon;
16 settingsFormat = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; };
18 configFile = settingsFormat.generate "mobilizon-config.exs" cfg.settings;
20 # Make a package containing launchers with the correct envirenment, instead of
21 # setting it with systemd services, so that the user can also use them without
23 launchers = pkgs.stdenv.mkDerivation rec {
24 pname = "${cfg.package.pname}-launchers";
25 inherit (cfg.package) version;
29 nativeBuildInputs = with pkgs; [ makeWrapper ];
39 --run '. ${secretEnvFile}' \
40 --set MOBILIZON_CONFIG_PATH "${configFile}" \
41 --set-default RELEASE_TMP "/tmp"
44 $src/bin/mobilizon_ctl \
45 $out/bin/mobilizon_ctl \
46 --run '. ${secretEnvFile}' \
47 --set MOBILIZON_CONFIG_PATH "${configFile}" \
48 --set-default RELEASE_TMP "/tmp"
52 repoSettings = cfg.settings.":mobilizon"."Mobilizon.Storage.Repo";
53 instanceSettings = cfg.settings.":mobilizon".":instance";
55 isLocalPostgres = repoSettings.socket_dir != null;
57 dbUser = if repoSettings.username != null then repoSettings.username else "mobilizon";
59 postgresql = config.services.postgresql.package;
60 postgresqlSocketDir = "/run/postgresql";
62 secretEnvFile = "/var/lib/mobilizon/secret-env.sh";
66 services.mobilizon = {
67 enable = mkEnableOption "Mobilizon federated organization and mobilization platform";
69 nginx.enable = lib.mkOption {
70 type = lib.types.bool;
73 Whether an Nginx virtual host should be
74 set up to serve Mobilizon.
78 package = mkPackageOption pkgs "mobilizon" { };
83 elixirTypes = settingsFormat.lib.types;
86 freeformType = settingsFormat.type;
91 "Mobilizon.Web.Endpoint" = {
93 type = elixirTypes.str;
94 defaultText = lib.literalMD ''
95 ''${settings.":mobilizon".":instance".hostname}
98 Your instance's hostname for generating URLs throughout the app
104 type = elixirTypes.port;
107 The port to run the server
111 type = elixirTypes.tuple;
112 default = settingsFormat.lib.mkTuple [
123 The IP address to listen on. Defaults to [::1] notated as a byte tuple.
128 has_reverse_proxy = mkOption {
129 type = elixirTypes.bool;
132 Whether you use a reverse proxy
139 type = elixirTypes.str;
141 The fallback instance name if not configured into the admin UI
145 hostname = mkOption {
146 type = elixirTypes.str;
148 Your instance's hostname
152 email_from = mkOption {
153 type = elixirTypes.str;
154 defaultText = literalExpression ''
155 noreply@''${settings.":mobilizon".":instance".hostname}
158 The email for the From: header in emails
162 email_reply_to = mkOption {
163 type = elixirTypes.str;
164 defaultText = literalExpression ''
168 The email for the Reply-To: header in emails
173 "Mobilizon.Storage.Repo" = {
174 socket_dir = mkOption {
175 type = types.nullOr elixirTypes.str;
176 default = postgresqlSocketDir;
178 Path to the postgres socket directory.
180 Set this to null if you want to connect to a remote database.
182 If non-null, the local PostgreSQL server will be configured with
183 the configured database, permissions, and required extensions.
185 If connecting to a remote database, please follow the
186 instructions on how to setup your database:
187 <https://docs.joinmobilizon.org/administration/install/release/#database-setup>
191 username = mkOption {
192 type = types.nullOr elixirTypes.str;
195 User used to connect to the database
199 database = mkOption {
200 type = types.nullOr elixirTypes.str;
201 default = "mobilizon_prod";
213 Mobilizon Elixir documentation, see
214 <https://docs.joinmobilizon.org/administration/configure/reference/>
215 for supported values.
221 config = mkIf cfg.enable {
228 cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.ip == settingsFormat.lib.mkTuple [
239 message = "Setting the IP mobilizon listens on is only possible when the nginx config is not used, as it is hardcoded there.";
243 services.mobilizon.settings = {
245 "Mobilizon.Web.Endpoint" = {
247 url.host = mkDefault instanceSettings.hostname;
248 secret_key_base = settingsFormat.lib.mkGetEnv { envVariable = "MOBILIZON_INSTANCE_SECRET"; };
251 "Mobilizon.Web.Auth.Guardian".secret_key = settingsFormat.lib.mkGetEnv {
252 envVariable = "MOBILIZON_AUTH_SECRET";
256 registrations_open = mkDefault false;
257 demo = mkDefault false;
258 email_from = mkDefault "noreply@${instanceSettings.hostname}";
259 email_reply_to = mkDefault instanceSettings.email_from;
262 "Mobilizon.Storage.Repo" = {
263 # Forced by upstream since it uses PostgreSQL-specific extensions
264 adapter = settingsFormat.lib.mkAtom "Ecto.Adapters.Postgres";
265 pool_size = mkDefault 10;
269 ":tzdata".":data_dir" = "/var/lib/mobilizon/tzdata/";
272 # This somewhat follows upstream's systemd service here:
273 # https://framagit.org/framasoft/mobilizon/-/blob/master/support/systemd/mobilizon.service
274 systemd.services.mobilizon = {
275 description = "Mobilizon federated organization and mobilization platform";
277 wantedBy = [ "multi-user.target" ];
293 ExecStartPre = "${launchers}/bin/mobilizon_ctl migrate";
294 ExecStart = "${launchers}/bin/mobilizon start";
295 ExecStop = "${launchers}/bin/mobilizon stop";
300 StateDirectory = "mobilizon";
302 Restart = "on-failure";
305 ProtectSystem = "full";
306 NoNewPrivileges = true;
308 ReadWritePaths = mkIf isLocalPostgres postgresqlSocketDir;
312 # Create the needed secrets before running Mobilizon, so that they are not
315 # Since some of these tasks are quite common for Elixir projects (COOKIE for
316 # every BEAM project, Phoenix and Guardian are also quite common), this
317 # service could be abstracted in the future, and used by other Elixir
319 systemd.services.mobilizon-setup-secrets = {
320 description = "Mobilizon setup secrets";
321 before = [ "mobilizon.service" ];
322 wantedBy = [ "mobilizon.service" ];
327 # https://framagit.org/framasoft/mobilizon/-/blob/1.0.7/lib/mix/tasks/mobilizon/instance.ex#L132-133
329 "IO.puts(:crypto.strong_rand_bytes(64)" + "|> Base.encode64()" + "|> binary_part(0, 64))";
332 # https://github.com/elixir-lang/elixir/blob/v1.11.3/lib/mix/lib/mix/release.ex#L499
333 genCookie = "IO.puts(Base.encode32(:crypto.strong_rand_bytes(32)))";
336 ${cfg.package.elixirPackage}/bin/elixir --eval '${str}'
342 if [ ! -f "${secretEnvFile}" ]; then
343 install -m 600 /dev/null "${secretEnvFile}"
344 cat > "${secretEnvFile}" <<EOF
345 # This file was automatically generated by mobilizon-setup-secrets.service
346 export MOBILIZON_AUTH_SECRET='$(${evalElixir genSecret})'
347 export MOBILIZON_INSTANCE_SECRET='$(${evalElixir genSecret})'
348 export RELEASE_COOKIE='$(${evalElixir genCookie})'
357 StateDirectory = "mobilizon";
361 # Add the required PostgreSQL extensions to the local PostgreSQL server,
362 # if local PostgreSQL is configured.
363 systemd.services.mobilizon-postgresql = mkIf isLocalPostgres {
364 description = "Mobilizon PostgreSQL setup";
366 after = [ "postgresql.service" ];
369 "mobilizon-setup-secrets.service"
371 wantedBy = [ "mobilizon.service" ];
373 path = [ postgresql ];
376 # https://framagit.org/framasoft/mobilizon/-/blob/1.1.0/priv/templates/setup_db.eex
377 # TODO(to maintainers of mobilizon): the owner database alteration is necessary
378 # as PostgreSQL 15 changed their behaviors w.r.t. to privileges.
379 # See https://github.com/NixOS/nixpkgs/issues/216989 to get rid
380 # of that workaround.
382 psql "${repoSettings.database}" -c "\
383 CREATE EXTENSION IF NOT EXISTS postgis; \
384 CREATE EXTENSION IF NOT EXISTS pg_trgm; \
385 CREATE EXTENSION IF NOT EXISTS unaccent;"
386 psql -tAc 'ALTER DATABASE "${repoSettings.database}" OWNER TO "${dbUser}";'
392 User = config.services.postgresql.superUser;
396 systemd.tmpfiles.rules = [
397 "d /var/lib/mobilizon/uploads/exports/csv 700 mobilizon mobilizon - -"
398 "Z /var/lib/mobilizon 700 mobilizon mobilizon - -"
401 services.postgresql = mkIf isLocalPostgres {
403 ensureDatabases = [ repoSettings.database ];
407 # Given that `dbUser` is potentially arbitrarily custom, we will perform
408 # manual fixups in mobilizon-postgres.
409 # TODO(to maintainers of mobilizon): Feel free to simplify your setup by using `ensureDBOwnership`.
410 ensureDBOwnership = false;
413 extensions = ps: with ps; [ postgis ];
416 # Nginx config taken from support/nginx/mobilizon-release.conf
419 inherit (cfg.settings.":mobilizon".":instance") hostname;
420 proxyPass = "http://[::1]:" + toString cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.port;
422 lib.mkIf cfg.nginx.enable {
424 virtualHosts."${hostname}" = {
425 enableACME = lib.mkDefault true;
426 forceSSL = lib.mkDefault true;
428 proxy_http_version 1.1;
429 proxy_set_header Upgrade $http_upgrade;
430 proxy_set_header Connection "upgrade";
431 proxy_set_header Host $host;
432 proxy_set_header X-Real-IP $remote_addr;
433 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
434 proxy_set_header X-Forwarded-Proto $scheme;
439 locations."~ ^/(js|css|img)" = {
440 root = "${cfg.package}/lib/mobilizon-${cfg.package.version}/priv/static";
444 add_header Cache-Control "public, max-age=31536000, immutable";
447 locations."~ ^/(media|proxy)" = {
452 add_header Cache-Control "public, max-age=31536000, immutable";
458 users.users.${user} = {
459 description = "Mobilizon daemon user";
464 users.groups.${group} = { };
466 # So that we have the `mobilizon` and `mobilizon_ctl` commands.
467 # The `mobilizon remote` command is useful for dropping a shell into the
468 # running Mobilizon instance, and `mobilizon_ctl` is used for common
469 # management tasks (e.g. adding users).
470 environment.systemPackages = [ launchers ];
473 meta.maintainers = with lib.maintainers; [