1 { pkgs, lib, config, ... }:
6 cfg = config.services.mobilizon;
11 settingsFormat = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; };
13 configFile = settingsFormat.generate "mobilizon-config.exs" cfg.settings;
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
18 launchers = pkgs.stdenv.mkDerivation rec {
19 pname = "${cfg.package.pname}-launchers";
20 inherit (cfg.package) version;
24 nativeBuildInputs = with pkgs; [ makeWrapper ];
34 --run '. ${secretEnvFile}' \
35 --set MOBILIZON_CONFIG_PATH "${configFile}" \
36 --set-default RELEASE_TMP "/tmp"
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"
47 repoSettings = cfg.settings.":mobilizon"."Mobilizon.Storage.Repo";
48 instanceSettings = cfg.settings.":mobilizon".":instance";
50 isLocalPostgres = repoSettings.socket_dir != null;
52 dbUser = if repoSettings.username != null then repoSettings.username else "mobilizon";
54 postgresql = config.services.postgresql.package;
55 postgresqlSocketDir = "/var/run/postgresql";
57 secretEnvFile = "/var/lib/mobilizon/secret-env.sh";
61 services.mobilizon = {
62 enable = mkEnableOption "Mobilizon federated organization and mobilization platform";
64 nginx.enable = lib.mkOption {
65 type = lib.types.bool;
68 Whether an Nginx virtual host should be
69 set up to serve Mobilizon.
73 package = mkPackageOption pkgs "mobilizon" { };
78 elixirTypes = settingsFormat.lib.types;
81 freeformType = settingsFormat.type;
86 "Mobilizon.Web.Endpoint" = {
88 type = elixirTypes.str;
89 defaultText = lib.literalMD ''
90 ''${settings.":mobilizon".":instance".hostname}
93 Your instance's hostname for generating URLs throughout the app
99 type = elixirTypes.port;
102 The port to run the server
106 type = elixirTypes.tuple;
107 default = settingsFormat.lib.mkTuple [ 0 0 0 0 0 0 0 1 ];
109 The IP address to listen on. Defaults to [::1] notated as a byte tuple.
114 has_reverse_proxy = mkOption {
115 type = elixirTypes.bool;
118 Whether you use a reverse proxy
125 type = elixirTypes.str;
127 The fallback instance name if not configured into the admin UI
131 hostname = mkOption {
132 type = elixirTypes.str;
134 Your instance's hostname
138 email_from = mkOption {
139 type = elixirTypes.str;
140 defaultText = literalExpression ''
141 noreply@''${settings.":mobilizon".":instance".hostname}
144 The email for the From: header in emails
148 email_reply_to = mkOption {
149 type = elixirTypes.str;
150 defaultText = literalExpression ''
154 The email for the Reply-To: header in emails
159 "Mobilizon.Storage.Repo" = {
160 socket_dir = mkOption {
161 type = types.nullOr elixirTypes.str;
162 default = postgresqlSocketDir;
164 Path to the postgres socket directory.
166 Set this to null if you want to connect to a remote database.
168 If non-null, the local PostgreSQL server will be configured with
169 the configured database, permissions, and required extensions.
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>
177 username = mkOption {
178 type = types.nullOr elixirTypes.str;
181 User used to connect to the database
185 database = mkOption {
186 type = types.nullOr elixirTypes.str;
187 default = "mobilizon_prod";
199 Mobilizon Elixir documentation, see
200 <https://docs.joinmobilizon.org/administration/configure/reference/>
201 for supported values.
207 config = mkIf cfg.enable {
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.";
216 services.mobilizon.settings = {
218 "Mobilizon.Web.Endpoint" = {
220 url.host = mkDefault instanceSettings.hostname;
222 settingsFormat.lib.mkGetEnv { envVariable = "MOBILIZON_INSTANCE_SECRET"; };
225 "Mobilizon.Web.Auth.Guardian".secret_key =
226 settingsFormat.lib.mkGetEnv { envVariable = "MOBILIZON_AUTH_SECRET"; };
229 registrations_open = mkDefault false;
230 demo = mkDefault false;
231 email_from = mkDefault "noreply@${instanceSettings.hostname}";
232 email_reply_to = mkDefault instanceSettings.email_from;
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;
242 ":tzdata".":data_dir" = "/var/lib/mobilizon/tzdata/";
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";
250 wantedBy = [ "multi-user.target" ];
266 ExecStartPre = "${launchers}/bin/mobilizon_ctl migrate";
267 ExecStart = "${launchers}/bin/mobilizon start";
268 ExecStop = "${launchers}/bin/mobilizon stop";
273 StateDirectory = "mobilizon";
275 Restart = "on-failure";
278 ProtectSystem = "full";
279 NoNewPrivileges = true;
281 ReadWritePaths = mkIf isLocalPostgres postgresqlSocketDir;
285 # Create the needed secrets before running Mobilizon, so that they are not
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
292 systemd.services.mobilizon-setup-secrets = {
293 description = "Mobilizon setup secrets";
294 before = [ "mobilizon.service" ];
295 wantedBy = [ "mobilizon.service" ];
300 # https://framagit.org/framasoft/mobilizon/-/blob/1.0.7/lib/mix/tasks/mobilizon/instance.ex#L132-133
302 "IO.puts(:crypto.strong_rand_bytes(64)" +
303 "|> Base.encode64()" +
304 "|> binary_part(0, 64))";
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)))";
311 ${cfg.package.elixirPackage}/bin/elixir --eval '${str}'
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})'
332 StateDirectory = "mobilizon";
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";
341 after = [ "postgresql.service" ];
342 before = [ "mobilizon.service" "mobilizon-setup-secrets.service" ];
343 wantedBy = [ "mobilizon.service" ];
345 path = [ postgresql ];
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.
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}";'
365 User = config.services.postgresql.superUser;
369 systemd.tmpfiles.rules = [
370 "d /var/lib/mobilizon/uploads/exports/csv 700 mobilizon mobilizon - -"
371 "Z /var/lib/mobilizon 700 mobilizon mobilizon - -"
374 services.postgresql = mkIf isLocalPostgres {
376 ensureDatabases = [ repoSettings.database ];
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;
386 extraPlugins = ps: with ps; [ postgis ];
389 # Nginx config taken from support/nginx/mobilizon-release.conf
392 inherit (cfg.settings.":mobilizon".":instance") hostname;
393 proxyPass = "http://[::1]:"
394 + toString cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.port;
396 lib.mkIf cfg.nginx.enable {
398 virtualHosts."${hostname}" = {
399 enableACME = lib.mkDefault true;
400 forceSSL = lib.mkDefault true;
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;
413 locations."~ ^/(js|css|img)" = {
414 root = "${cfg.package}/lib/mobilizon-${cfg.package.version}/priv/static";
418 add_header Cache-Control "public, max-age=31536000, immutable";
421 locations."~ ^/(media|proxy)" = {
426 add_header Cache-Control "public, max-age=31536000, immutable";
432 users.users.${user} = {
433 description = "Mobilizon daemon user";
438 users.groups.${group} = { };
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 ];
447 meta.maintainers = with lib.maintainers; [ minijackson erictapen ];