0
0
Fork 0
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-07-20 09:00:41 +03:00
nixpkgs/nixos/modules/services/web-apps/lemmy.nix
Wolfgang Walther 41c5662cbe
nixos/postgresql: move postStart into separate unit
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>
2025-06-24 15:26:47 +02:00

385 lines
12 KiB
Nix

{
lib,
pkgs,
config,
utils,
...
}:
with lib;
let
cfg = config.services.lemmy;
settingsFormat = pkgs.formats.json { };
in
{
meta.maintainers = with maintainers; [ happysalada ];
meta.doc = ./lemmy.md;
imports = [
(mkRemovedOptionModule [
"services"
"lemmy"
"jwtSecretPath"
] "As of v0.13.0, Lemmy auto-generates the JWT secret.")
];
options.services.lemmy = {
enable = mkEnableOption "lemmy a federated alternative to reddit in rust";
server = {
package = mkPackageOption pkgs "lemmy-server" { };
};
ui = {
package = mkPackageOption pkgs "lemmy-ui" { };
port = mkOption {
type = types.port;
default = 1234;
description = "Port where lemmy-ui should listen for incoming requests.";
};
};
caddy.enable = mkEnableOption "exposing lemmy with the caddy reverse proxy";
nginx.enable = mkEnableOption "exposing lemmy with the nginx reverse proxy";
database = {
createLocally = mkEnableOption "creation of database on the instance";
uri = mkOption {
type = with types; nullOr str;
default = null;
description = "The connection URI to use. Takes priority over the configuration file if set.";
};
uriFile = mkOption {
type = with types; nullOr path;
default = null;
description = "File which contains the database uri.";
};
};
pictrsApiKeyFile = mkOption {
type = with types; nullOr path;
default = null;
description = "File which contains the value of `pictrs.api_key`.";
};
smtpPasswordFile = mkOption {
type = with types; nullOr path;
default = null;
description = "File which contains the value of `email.smtp_password`.";
};
adminPasswordFile = mkOption {
type = with types; nullOr path;
default = null;
description = "File which contains the value of `setup.admin_password`.";
};
settings = mkOption {
default = { };
description = "Lemmy configuration";
type = types.submodule {
freeformType = settingsFormat.type;
options.hostname = mkOption {
type = types.str;
default = null;
description = "The domain name of your instance (eg 'lemmy.ml').";
};
options.port = mkOption {
type = types.port;
default = 8536;
description = "Port where lemmy should listen for incoming requests.";
};
options.captcha = {
enabled = mkOption {
type = types.bool;
default = true;
description = "Enable Captcha.";
};
difficulty = mkOption {
type = types.enum [
"easy"
"medium"
"hard"
];
default = "medium";
description = "The difficultly of the captcha to solve.";
};
};
};
};
};
config =
let
secretOptions = {
pictrsApiKeyFile = {
setting = [
"pictrs"
"api_key"
];
path = cfg.pictrsApiKeyFile;
};
smtpPasswordFile = {
setting = [
"email"
"smtp_password"
];
path = cfg.smtpPasswordFile;
};
adminPasswordFile = {
setting = [
"setup"
"admin_password"
];
path = cfg.adminPasswordFile;
};
uriFile = {
setting = [
"database"
"uri"
];
path = cfg.database.uriFile;
};
};
secrets = lib.filterAttrs (option: data: data.path != null) secretOptions;
in
lib.mkIf cfg.enable {
services.lemmy.settings =
lib.attrsets.recursiveUpdate
(
mapAttrs (name: mkDefault) {
bind = "127.0.0.1";
tls_enabled = true;
pictrs = {
url = with config.services.pict-rs; "http://${address}:${toString port}";
};
actor_name_max_length = 20;
rate_limit.message = 180;
rate_limit.message_per_second = 60;
rate_limit.post = 6;
rate_limit.post_per_second = 600;
rate_limit.register = 3;
rate_limit.register_per_second = 3600;
rate_limit.image = 6;
rate_limit.image_per_second = 3600;
}
// {
database = mapAttrs (name: mkDefault) {
user = "lemmy";
host = "/run/postgresql";
port = 5432;
database = "lemmy";
pool_size = 5;
};
}
)
(
lib.foldlAttrs (
acc: option: data:
acc // lib.setAttrByPath data.setting { _secret = option; }
) { } secrets
);
# the option name is the id of the credential loaded by LoadCredential
services.postgresql = mkIf cfg.database.createLocally {
enable = true;
ensureDatabases = [ cfg.settings.database.database ];
ensureUsers = [
{
name = cfg.settings.database.user;
ensureDBOwnership = true;
}
];
};
services.pict-rs.enable = true;
services.caddy = mkIf cfg.caddy.enable {
enable = mkDefault true;
virtualHosts."${cfg.settings.hostname}" = {
extraConfig = ''
handle_path /static/* {
root * ${cfg.ui.package}/dist
file_server
}
handle_path /static/${cfg.ui.package.passthru.commit_sha}/* {
root * ${cfg.ui.package}/dist
file_server
}
@for_backend {
path /api/* /pictrs/* /feeds/* /nodeinfo/*
}
handle @for_backend {
reverse_proxy 127.0.0.1:${toString cfg.settings.port}
}
@post {
method POST
}
handle @post {
reverse_proxy 127.0.0.1:${toString cfg.settings.port}
}
@jsonld {
header Accept "application/activity+json"
header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
}
handle @jsonld {
reverse_proxy 127.0.0.1:${toString cfg.settings.port}
}
handle {
reverse_proxy 127.0.0.1:${toString cfg.ui.port}
}
'';
};
};
services.nginx = mkIf cfg.nginx.enable {
enable = mkDefault true;
virtualHosts."${cfg.settings.hostname}".locations =
let
ui = "http://127.0.0.1:${toString cfg.ui.port}";
backend = "http://127.0.0.1:${toString cfg.settings.port}";
in
{
"~ ^/(api|pictrs|feeds|nodeinfo|.well-known)" = {
# backend requests
proxyPass = backend;
proxyWebsockets = true;
recommendedProxySettings = true;
};
"/" = {
# mixed frontend and backend requests, based on the request headers
extraConfig = ''
set $proxpass "${ui}";
if ($http_accept = "application/activity+json") {
set $proxpass "${backend}";
}
if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") {
set $proxpass "${backend}";
}
if ($request_method = POST) {
set $proxpass "${backend}";
}
# Cuts off the trailing slash on URLs to make them valid
rewrite ^(.+)/+$ $1 permanent;
proxy_pass $proxpass;
# Proxied `Host` header is required to validate ActivityPub HTTP signatures for incoming events.
# The other headers are optional, for the sake of better log data.
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
'';
};
};
};
assertions = [
{
assertion =
cfg.database.createLocally
-> cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql";
message = "if you want to create the database locally, you need to use a local database";
}
{
assertion =
(!(hasAttrByPath [ "federation" ] cfg.settings))
&& (!(hasAttrByPath [ "federation" "enabled" ] cfg.settings));
message = "`services.lemmy.settings.federation` was removed in 0.17.0 and no longer has any effect";
}
{
assertion = cfg.database.uriFile != null -> cfg.database.uri == null && !cfg.database.createLocally;
message = "specifying a database uri while also specifying a database uri file is not allowed";
}
];
systemd.services.lemmy =
let
substitutedConfig = "/run/lemmy/config.hjson";
in
{
description = "Lemmy server";
environment = {
LEMMY_CONFIG_LOCATION =
if secrets == { } then settingsFormat.generate "config.hjson" cfg.settings else substitutedConfig;
LEMMY_DATABASE_URL =
if cfg.database.uri != null then
cfg.database.uri
else
(mkIf (cfg.database.createLocally) "postgres:///lemmy?host=/run/postgresql&user=lemmy");
};
documentation = [
"https://join-lemmy.org/docs/en/admins/from_scratch.html"
"https://join-lemmy.org/docs/en/"
];
wantedBy = [ "multi-user.target" ];
after = [ "pict-rs.service" ] ++ lib.optionals cfg.database.createLocally [ "postgresql.target" ];
requires = lib.optionals cfg.database.createLocally [ "postgresql.target" ];
# substitute secrets and prevent others from reading the result
# if somehow $CREDENTIALS_DIRECTORY is not set we fail
preStart = mkIf (secrets != { }) ''
set -u
umask u=rw,g=,o=
cd "$CREDENTIALS_DIRECTORY"
${utils.genJqSecretsReplacementSnippet cfg.settings substitutedConfig}
'';
serviceConfig = {
DynamicUser = true;
RuntimeDirectory = "lemmy";
ExecStart = "${cfg.server.package}/bin/lemmy_server";
LoadCredential = lib.foldlAttrs (
acc: option: data:
acc ++ [ "${option}:${toString data.path}" ]
) [ ] secrets;
PrivateTmp = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
};
};
systemd.services.lemmy-ui = {
description = "Lemmy ui";
environment = {
LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
LEMMY_UI_LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
LEMMY_UI_LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
LEMMY_UI_HTTPS = "false";
NODE_ENV = "production";
};
documentation = [
"https://join-lemmy.org/docs/en/admins/from_scratch.html"
"https://join-lemmy.org/docs/en/"
];
wantedBy = [ "multi-user.target" ];
after = [ "lemmy.service" ];
requires = [ "lemmy.service" ];
serviceConfig = {
DynamicUser = true;
WorkingDirectory = "${cfg.ui.package}";
ExecStart = "${pkgs.nodejs}/bin/node ${cfg.ui.package}/dist/js/server.js";
};
};
};
}