
This avoids restarting the postgresql server, when only ensureDatabases or ensureUsers have been changed. It will also allow to properly wait for recovery to finish later. To wait for "postgresql is ready" in other services, we now provide a postgresql.target. Resolves #400018 Co-authored-by: Marcel <me@m4rc3l.de>
483 lines
15 KiB
Nix
483 lines
15 KiB
Nix
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
|
|
let
|
|
inherit (lib)
|
|
mkEnableOption
|
|
mkPackageOption
|
|
mkOption
|
|
mkDefault
|
|
mkIf
|
|
types
|
|
literalExpression
|
|
;
|
|
|
|
cfg = config.services.mobilizon;
|
|
|
|
user = "mobilizon";
|
|
group = "mobilizon";
|
|
|
|
settingsFormat = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; };
|
|
|
|
configFile = settingsFormat.generate "mobilizon-config.exs" cfg.settings;
|
|
|
|
# Make a package containing launchers with the correct envirenment, instead of
|
|
# setting it with systemd services, so that the user can also use them without
|
|
# troubles
|
|
launchers =
|
|
pkgs.runCommand "${cfg.package.pname}-launchers-${cfg.package.version}"
|
|
{
|
|
src = cfg.package;
|
|
nativeBuildInputs = with pkgs; [ makeWrapper ];
|
|
}
|
|
''
|
|
mkdir -p $out/bin
|
|
|
|
makeWrapper \
|
|
$src/bin/mobilizon \
|
|
$out/bin/mobilizon \
|
|
--run '. ${secretEnvFile}' \
|
|
--set MOBILIZON_CONFIG_PATH "${configFile}" \
|
|
--set-default RELEASE_TMP "/tmp"
|
|
|
|
makeWrapper \
|
|
$src/bin/mobilizon_ctl \
|
|
$out/bin/mobilizon_ctl \
|
|
--run '. ${secretEnvFile}' \
|
|
--set MOBILIZON_CONFIG_PATH "${configFile}" \
|
|
--set-default RELEASE_TMP "/tmp"
|
|
'';
|
|
|
|
repoSettings = cfg.settings.":mobilizon"."Mobilizon.Storage.Repo";
|
|
instanceSettings = cfg.settings.":mobilizon".":instance";
|
|
|
|
isLocalPostgres = repoSettings.socket_dir != null;
|
|
|
|
dbUser = if repoSettings.username != null then repoSettings.username else "mobilizon";
|
|
|
|
postgresql = config.services.postgresql.package;
|
|
postgresqlSocketDir = "/run/postgresql";
|
|
|
|
secretEnvFile = "/var/lib/mobilizon/secret-env.sh";
|
|
in
|
|
{
|
|
options = {
|
|
services.mobilizon = {
|
|
enable = mkEnableOption "Mobilizon federated organization and mobilization platform";
|
|
|
|
nginx.enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = ''
|
|
Whether an Nginx virtual host should be
|
|
set up to serve Mobilizon.
|
|
'';
|
|
};
|
|
|
|
package = mkPackageOption pkgs "mobilizon" { };
|
|
|
|
settings = mkOption {
|
|
type =
|
|
let
|
|
elixirTypes = settingsFormat.lib.types;
|
|
in
|
|
types.submodule {
|
|
freeformType = settingsFormat.type;
|
|
|
|
options = {
|
|
":mobilizon" = {
|
|
|
|
"Mobilizon.Web.Endpoint" = {
|
|
url.host = mkOption {
|
|
type = elixirTypes.str;
|
|
defaultText = lib.literalMD ''
|
|
''${settings.":mobilizon".":instance".hostname}
|
|
'';
|
|
description = ''
|
|
Your instance's hostname for generating URLs throughout the app
|
|
'';
|
|
};
|
|
|
|
http = {
|
|
port = mkOption {
|
|
type = elixirTypes.port;
|
|
default = 4000;
|
|
description = ''
|
|
The port to run the server
|
|
'';
|
|
};
|
|
ip = mkOption {
|
|
type = elixirTypes.tuple;
|
|
default = settingsFormat.lib.mkTuple [
|
|
0
|
|
0
|
|
0
|
|
0
|
|
0
|
|
0
|
|
0
|
|
1
|
|
];
|
|
description = ''
|
|
The IP address to listen on. Defaults to [::1] notated as a byte tuple.
|
|
'';
|
|
};
|
|
};
|
|
|
|
has_reverse_proxy = mkOption {
|
|
type = elixirTypes.bool;
|
|
default = true;
|
|
description = ''
|
|
Whether you use a reverse proxy
|
|
'';
|
|
};
|
|
};
|
|
|
|
":instance" = {
|
|
name = mkOption {
|
|
type = elixirTypes.str;
|
|
description = ''
|
|
The fallback instance name if not configured into the admin UI
|
|
'';
|
|
};
|
|
|
|
hostname = mkOption {
|
|
type = elixirTypes.str;
|
|
description = ''
|
|
Your instance's hostname
|
|
'';
|
|
};
|
|
|
|
email_from = mkOption {
|
|
type = elixirTypes.str;
|
|
defaultText = literalExpression ''
|
|
noreply@''${settings.":mobilizon".":instance".hostname}
|
|
'';
|
|
description = ''
|
|
The email for the From: header in emails
|
|
'';
|
|
};
|
|
|
|
email_reply_to = mkOption {
|
|
type = elixirTypes.str;
|
|
defaultText = literalExpression ''
|
|
''${email_from}
|
|
'';
|
|
description = ''
|
|
The email for the Reply-To: header in emails
|
|
'';
|
|
};
|
|
};
|
|
|
|
"Mobilizon.Storage.Repo" = {
|
|
socket_dir = mkOption {
|
|
type = types.nullOr elixirTypes.str;
|
|
default = postgresqlSocketDir;
|
|
description = ''
|
|
Path to the postgres socket directory.
|
|
|
|
Set this to null if you want to connect to a remote database.
|
|
|
|
If non-null, the local PostgreSQL server will be configured with
|
|
the configured database, permissions, and required extensions.
|
|
|
|
If connecting to a remote database, please follow the
|
|
instructions on how to setup your database:
|
|
<https://docs.joinmobilizon.org/administration/install/release/#database-setup>
|
|
'';
|
|
};
|
|
|
|
username = mkOption {
|
|
type = types.nullOr elixirTypes.str;
|
|
default = user;
|
|
description = ''
|
|
User used to connect to the database
|
|
'';
|
|
};
|
|
|
|
database = mkOption {
|
|
type = types.nullOr elixirTypes.str;
|
|
default = "mobilizon_prod";
|
|
description = ''
|
|
Name of the database
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
default = { };
|
|
|
|
description = ''
|
|
Mobilizon Elixir documentation, see
|
|
<https://docs.joinmobilizon.org/administration/configure/reference/>
|
|
for supported values.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
|
|
assertions = [
|
|
{
|
|
assertion =
|
|
cfg.nginx.enable
|
|
-> (
|
|
cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.ip == settingsFormat.lib.mkTuple [
|
|
0
|
|
0
|
|
0
|
|
0
|
|
0
|
|
0
|
|
0
|
|
1
|
|
]
|
|
);
|
|
message = "Setting the IP mobilizon listens on is only possible when the nginx config is not used, as it is hardcoded there.";
|
|
}
|
|
];
|
|
|
|
services.mobilizon.settings = {
|
|
":mobilizon" = {
|
|
"Mobilizon.Web.Endpoint" = {
|
|
server = true;
|
|
url.host = mkDefault instanceSettings.hostname;
|
|
secret_key_base = settingsFormat.lib.mkGetEnv { envVariable = "MOBILIZON_INSTANCE_SECRET"; };
|
|
};
|
|
|
|
"Mobilizon.Web.Auth.Guardian".secret_key = settingsFormat.lib.mkGetEnv {
|
|
envVariable = "MOBILIZON_AUTH_SECRET";
|
|
};
|
|
|
|
":instance" = {
|
|
registrations_open = mkDefault false;
|
|
demo = mkDefault false;
|
|
email_from = mkDefault "noreply@${instanceSettings.hostname}";
|
|
email_reply_to = mkDefault instanceSettings.email_from;
|
|
};
|
|
|
|
"Mobilizon.Storage.Repo" = {
|
|
# Forced by upstream since it uses PostgreSQL-specific extensions
|
|
adapter = settingsFormat.lib.mkAtom "Ecto.Adapters.Postgres";
|
|
pool_size = mkDefault 10;
|
|
};
|
|
};
|
|
|
|
":tzdata".":data_dir" = "/var/lib/mobilizon/tzdata/";
|
|
};
|
|
|
|
# This somewhat follows upstream's systemd service here:
|
|
# https://framagit.org/framasoft/mobilizon/-/blob/master/support/systemd/mobilizon.service
|
|
systemd.services.mobilizon = {
|
|
description = "Mobilizon federated organization and mobilization platform";
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
path = with pkgs; [
|
|
gawk
|
|
imagemagick
|
|
libwebp
|
|
file
|
|
|
|
# Optional:
|
|
gifsicle
|
|
jpegoptim
|
|
optipng
|
|
pngquant
|
|
];
|
|
|
|
serviceConfig = {
|
|
ExecStartPre = "${launchers}/bin/mobilizon_ctl migrate";
|
|
ExecStart = "${launchers}/bin/mobilizon start";
|
|
ExecStop = "${launchers}/bin/mobilizon stop";
|
|
|
|
User = user;
|
|
Group = group;
|
|
|
|
StateDirectory = "mobilizon";
|
|
|
|
Restart = "on-failure";
|
|
|
|
PrivateTmp = true;
|
|
ProtectSystem = "full";
|
|
NoNewPrivileges = true;
|
|
|
|
ReadWritePaths = mkIf isLocalPostgres postgresqlSocketDir;
|
|
};
|
|
};
|
|
|
|
# Create the needed secrets before running Mobilizon, so that they are not
|
|
# in the nix store
|
|
#
|
|
# Since some of these tasks are quite common for Elixir projects (COOKIE for
|
|
# every BEAM project, Phoenix and Guardian are also quite common), this
|
|
# service could be abstracted in the future, and used by other Elixir
|
|
# projects.
|
|
systemd.services.mobilizon-setup-secrets = {
|
|
description = "Mobilizon setup secrets";
|
|
before = [ "mobilizon.service" ];
|
|
wantedBy = [ "mobilizon.service" ];
|
|
|
|
script =
|
|
let
|
|
# Taken from here:
|
|
# https://framagit.org/framasoft/mobilizon/-/blob/1.0.7/lib/mix/tasks/mobilizon/instance.ex#L132-133
|
|
genSecret =
|
|
"IO.puts(:crypto.strong_rand_bytes(64)" + "|> Base.encode64()" + "|> binary_part(0, 64))";
|
|
|
|
# Taken from here:
|
|
# https://github.com/elixir-lang/elixir/blob/v1.11.3/lib/mix/lib/mix/release.ex#L499
|
|
genCookie = "IO.puts(Base.encode32(:crypto.strong_rand_bytes(32)))";
|
|
|
|
evalElixir = str: ''
|
|
${cfg.package.elixirPackage}/bin/elixir --eval '${str}'
|
|
'';
|
|
in
|
|
''
|
|
set -euxo pipefail
|
|
|
|
if [ ! -f "${secretEnvFile}" ]; then
|
|
install -m 600 /dev/null "${secretEnvFile}"
|
|
cat > "${secretEnvFile}" <<EOF
|
|
# This file was automatically generated by mobilizon-setup-secrets.service
|
|
export MOBILIZON_AUTH_SECRET='$(${evalElixir genSecret})'
|
|
export MOBILIZON_INSTANCE_SECRET='$(${evalElixir genSecret})'
|
|
export RELEASE_COOKIE='$(${evalElixir genCookie})'
|
|
EOF
|
|
fi
|
|
'';
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = user;
|
|
Group = group;
|
|
StateDirectory = "mobilizon";
|
|
};
|
|
};
|
|
|
|
# Add the required PostgreSQL extensions to the local PostgreSQL server,
|
|
# if local PostgreSQL is configured.
|
|
systemd.services.mobilizon-postgresql = mkIf isLocalPostgres {
|
|
description = "Mobilizon PostgreSQL setup";
|
|
|
|
after = [ "postgresql.target" ];
|
|
before = [
|
|
"mobilizon.service"
|
|
"mobilizon-setup-secrets.service"
|
|
];
|
|
wantedBy = [ "mobilizon.service" ];
|
|
|
|
path = [ postgresql ];
|
|
|
|
# Taken from here:
|
|
# https://framagit.org/framasoft/mobilizon/-/blob/1.1.0/priv/templates/setup_db.eex
|
|
# TODO(to maintainers of mobilizon): the owner database alteration is necessary
|
|
# as PostgreSQL 15 changed their behaviors w.r.t. to privileges.
|
|
# See https://github.com/NixOS/nixpkgs/issues/216989 to get rid
|
|
# of that workaround.
|
|
script = ''
|
|
psql "${repoSettings.database}" -c "\
|
|
CREATE EXTENSION IF NOT EXISTS postgis; \
|
|
CREATE EXTENSION IF NOT EXISTS pg_trgm; \
|
|
CREATE EXTENSION IF NOT EXISTS unaccent;"
|
|
psql -tAc 'ALTER DATABASE "${repoSettings.database}" OWNER TO "${dbUser}";'
|
|
|
|
'';
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = config.services.postgresql.superUser;
|
|
Restart = "on-failure";
|
|
};
|
|
};
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d /var/lib/mobilizon/sitemap 700 mobilizon mobilizon - -"
|
|
"d /var/lib/mobilizon/uploads/exports/csv 700 mobilizon mobilizon - -"
|
|
"Z /var/lib/mobilizon 700 mobilizon mobilizon - -"
|
|
];
|
|
|
|
services.postgresql = mkIf isLocalPostgres {
|
|
enable = true;
|
|
ensureDatabases = [ repoSettings.database ];
|
|
ensureUsers = [
|
|
{
|
|
name = dbUser;
|
|
# Given that `dbUser` is potentially arbitrarily custom, we will perform
|
|
# manual fixups in mobilizon-postgres.
|
|
# TODO(to maintainers of mobilizon): Feel free to simplify your setup by using `ensureDBOwnership`.
|
|
ensureDBOwnership = false;
|
|
}
|
|
];
|
|
extensions = ps: with ps; [ postgis ];
|
|
};
|
|
|
|
# Nginx config taken from support/nginx/mobilizon-release.conf
|
|
services.nginx =
|
|
let
|
|
inherit (cfg.settings.":mobilizon".":instance") hostname;
|
|
proxyPass = "http://[::1]:" + toString cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.port;
|
|
in
|
|
lib.mkIf cfg.nginx.enable {
|
|
enable = true;
|
|
virtualHosts."${hostname}" = {
|
|
enableACME = lib.mkDefault true;
|
|
forceSSL = lib.mkDefault true;
|
|
locations."/" = {
|
|
inherit proxyPass;
|
|
proxyWebsockets = true;
|
|
recommendedProxySettings = lib.mkDefault true;
|
|
extraConfig = ''
|
|
expires off;
|
|
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
|
'';
|
|
};
|
|
locations."~ ^/(assets|img)" = {
|
|
root = "${cfg.package}/lib/mobilizon-${cfg.package.version}/priv/static";
|
|
extraConfig = ''
|
|
access_log off;
|
|
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
|
'';
|
|
};
|
|
locations."~ ^/(media|proxy)" = {
|
|
inherit proxyPass;
|
|
recommendedProxySettings = lib.mkDefault true;
|
|
# Combination of HTTP/1.1 and disabled request buffering is
|
|
# needed to directly forward chunked responses
|
|
extraConfig = ''
|
|
proxy_http_version 1.1;
|
|
proxy_request_buffering off;
|
|
access_log off;
|
|
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
users.users.${user} = {
|
|
description = "Mobilizon daemon user";
|
|
group = group;
|
|
isSystemUser = true;
|
|
};
|
|
|
|
users.groups.${group} = { };
|
|
|
|
# So that we have the `mobilizon` and `mobilizon_ctl` commands.
|
|
# The `mobilizon remote` command is useful for dropping a shell into the
|
|
# running Mobilizon instance, and `mobilizon_ctl` is used for common
|
|
# management tasks (e.g. adding users).
|
|
environment.systemPackages = [ launchers ];
|
|
};
|
|
|
|
meta.maintainers = with lib.maintainers; [
|
|
minijackson
|
|
erictapen
|
|
];
|
|
}
|