2024-12-10 20:27:17 +01:00
|
|
|
{
|
|
|
|
lib,
|
|
|
|
pkgs,
|
|
|
|
config,
|
|
|
|
utils,
|
|
|
|
...
|
|
|
|
}:
|
2021-09-23 10:27:42 -04:00
|
|
|
with lib;
|
|
|
|
let
|
|
|
|
cfg = config.services.lemmy;
|
|
|
|
settingsFormat = pkgs.formats.json { };
|
|
|
|
in
|
|
|
|
{
|
|
|
|
meta.maintainers = with maintainers; [ happysalada ];
|
2023-01-25 00:33:40 +01:00
|
|
|
meta.doc = ./lemmy.md;
|
2021-09-23 10:27:42 -04:00
|
|
|
|
2022-09-12 10:34:55 +02:00
|
|
|
imports = [
|
2024-12-10 20:27:17 +01:00
|
|
|
(mkRemovedOptionModule [
|
|
|
|
"services"
|
|
|
|
"lemmy"
|
|
|
|
"jwtSecretPath"
|
|
|
|
] "As of v0.13.0, Lemmy auto-generates the JWT secret.")
|
2022-09-12 10:34:55 +02:00
|
|
|
];
|
|
|
|
|
2021-09-23 10:27:42 -04:00
|
|
|
options.services.lemmy = {
|
|
|
|
|
2024-04-13 14:54:15 +02:00
|
|
|
enable = mkEnableOption "lemmy a federated alternative to reddit in rust";
|
2021-09-23 10:27:42 -04:00
|
|
|
|
2023-06-06 08:19:37 -07:00
|
|
|
server = {
|
2024-12-10 20:27:17 +01:00
|
|
|
package = mkPackageOption pkgs "lemmy-server" { };
|
2023-06-06 08:19:37 -07:00
|
|
|
};
|
|
|
|
|
2021-09-23 10:27:42 -04:00
|
|
|
ui = {
|
2024-12-10 20:27:17 +01:00
|
|
|
package = mkPackageOption pkgs "lemmy-ui" { };
|
2023-06-06 08:19:37 -07:00
|
|
|
|
2021-09-23 10:27:42 -04:00
|
|
|
port = mkOption {
|
|
|
|
type = types.port;
|
|
|
|
default = 1234;
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "Port where lemmy-ui should listen for incoming requests.";
|
2021-09-23 10:27:42 -04:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2024-04-13 14:54:15 +02:00
|
|
|
caddy.enable = mkEnableOption "exposing lemmy with the caddy reverse proxy";
|
|
|
|
nginx.enable = mkEnableOption "exposing lemmy with the nginx reverse proxy";
|
2021-09-23 10:27:42 -04:00
|
|
|
|
2023-06-06 08:11:49 -07:00
|
|
|
database = {
|
2024-04-13 14:54:15 +02:00
|
|
|
createLocally = mkEnableOption "creation of database on the instance";
|
2023-06-06 08:11:49 -07:00
|
|
|
|
|
|
|
uri = mkOption {
|
|
|
|
type = with types; nullOr str;
|
|
|
|
default = null;
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "The connection URI to use. Takes priority over the configuration file if set.";
|
2023-06-06 08:11:49 -07:00
|
|
|
};
|
2023-07-04 23:49:12 +02:00
|
|
|
|
|
|
|
uriFile = mkOption {
|
|
|
|
type = with types; nullOr path;
|
|
|
|
default = null;
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "File which contains the database uri.";
|
2023-07-04 23:49:12 +02:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
pictrsApiKeyFile = mkOption {
|
|
|
|
type = with types; nullOr path;
|
|
|
|
default = null;
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "File which contains the value of `pictrs.api_key`.";
|
2023-07-04 23:49:12 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
smtpPasswordFile = mkOption {
|
|
|
|
type = with types; nullOr path;
|
|
|
|
default = null;
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "File which contains the value of `email.smtp_password`.";
|
2023-07-04 23:49:12 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
adminPasswordFile = mkOption {
|
|
|
|
type = with types; nullOr path;
|
|
|
|
default = null;
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "File which contains the value of `setup.admin_password`.";
|
2023-06-06 08:11:49 -07:00
|
|
|
};
|
2022-09-19 00:09:38 -07:00
|
|
|
|
2021-09-23 10:27:42 -04:00
|
|
|
settings = mkOption {
|
|
|
|
default = { };
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "Lemmy configuration";
|
2021-09-23 10:27:42 -04:00
|
|
|
|
|
|
|
type = types.submodule {
|
|
|
|
freeformType = settingsFormat.type;
|
|
|
|
|
|
|
|
options.hostname = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = null;
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "The domain name of your instance (eg 'lemmy.ml').";
|
2021-09-23 10:27:42 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
options.port = mkOption {
|
|
|
|
type = types.port;
|
|
|
|
default = 8536;
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "Port where lemmy should listen for incoming requests.";
|
2021-09-23 10:27:42 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
options.captcha = {
|
|
|
|
enabled = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "Enable Captcha.";
|
2021-09-23 10:27:42 -04:00
|
|
|
};
|
|
|
|
difficulty = mkOption {
|
2024-12-10 20:27:17 +01:00
|
|
|
type = types.enum [
|
|
|
|
"easy"
|
|
|
|
"medium"
|
|
|
|
"hard"
|
|
|
|
];
|
2021-09-23 10:27:42 -04:00
|
|
|
default = "medium";
|
2024-04-13 14:54:15 +02:00
|
|
|
description = "The difficultly of the captcha to solve.";
|
2021-09-23 10:27:42 -04:00
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
config =
|
2023-07-04 23:49:12 +02:00
|
|
|
let
|
|
|
|
secretOptions = {
|
2024-12-10 20:27:17 +01:00
|
|
|
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;
|
|
|
|
};
|
2023-07-04 23:49:12 +02:00
|
|
|
};
|
|
|
|
secrets = lib.filterAttrs (option: data: data.path != null) secretOptions;
|
|
|
|
in
|
2021-09-23 10:27:42 -04:00
|
|
|
lib.mkIf cfg.enable {
|
2024-12-10 20:27:17 +01:00
|
|
|
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
|
2021-09-23 10:27:42 -04:00
|
|
|
|
2022-09-16 00:28:29 -07:00
|
|
|
services.postgresql = mkIf cfg.database.createLocally {
|
2022-09-21 00:48:02 -07:00
|
|
|
enable = true;
|
|
|
|
ensureDatabases = [ cfg.settings.database.database ];
|
2024-12-10 20:27:17 +01:00
|
|
|
ensureUsers = [
|
|
|
|
{
|
|
|
|
name = cfg.settings.database.user;
|
|
|
|
ensureDBOwnership = true;
|
|
|
|
}
|
|
|
|
];
|
2021-09-23 10:27:42 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
services.pict-rs.enable = true;
|
|
|
|
|
|
|
|
services.caddy = mkIf cfg.caddy.enable {
|
|
|
|
enable = mkDefault true;
|
|
|
|
virtualHosts."${cfg.settings.hostname}" = {
|
|
|
|
extraConfig = ''
|
|
|
|
handle_path /static/* {
|
2023-06-06 08:19:37 -07:00
|
|
|
root * ${cfg.ui.package}/dist
|
2021-09-23 10:27:42 -04:00
|
|
|
file_server
|
|
|
|
}
|
2023-08-27 19:27:40 +08:00
|
|
|
handle_path /static/${cfg.ui.package.passthru.commit_sha}/* {
|
2023-08-16 21:39:11 +02:00
|
|
|
root * ${cfg.ui.package}/dist
|
|
|
|
file_server
|
|
|
|
}
|
2021-09-23 10:27:42 -04:00
|
|
|
@for_backend {
|
2022-09-09 01:18:41 -07:00
|
|
|
path /api/* /pictrs/* /feeds/* /nodeinfo/*
|
2021-09-23 10:27:42 -04:00
|
|
|
}
|
|
|
|
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}
|
|
|
|
}
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-05-17 20:20:13 +00:00
|
|
|
services.nginx = mkIf cfg.nginx.enable {
|
|
|
|
enable = mkDefault true;
|
2024-12-10 20:27:17 +01:00
|
|
|
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;
|
|
|
|
'';
|
|
|
|
};
|
2023-05-17 20:20:13 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-06-15 08:26:40 -07:00
|
|
|
assertions = [
|
|
|
|
{
|
2024-12-10 20:27:17 +01:00
|
|
|
assertion =
|
|
|
|
cfg.database.createLocally
|
|
|
|
-> cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql";
|
2023-06-15 08:26:40 -07:00
|
|
|
message = "if you want to create the database locally, you need to use a local database";
|
|
|
|
}
|
|
|
|
{
|
2024-12-10 20:27:17 +01:00
|
|
|
assertion =
|
|
|
|
(!(hasAttrByPath [ "federation" ] cfg.settings))
|
|
|
|
&& (!(hasAttrByPath [ "federation" "enabled" ] cfg.settings));
|
2023-06-15 08:26:40 -07:00
|
|
|
message = "`services.lemmy.settings.federation` was removed in 0.17.0 and no longer has any effect";
|
|
|
|
}
|
2023-07-04 23:49:12 +02:00
|
|
|
{
|
|
|
|
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";
|
|
|
|
}
|
2023-06-15 08:26:40 -07:00
|
|
|
];
|
2021-09-23 10:27:42 -04:00
|
|
|
|
2024-12-10 20:27:17 +01:00
|
|
|
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");
|
|
|
|
};
|
2021-09-23 10:27:42 -04:00
|
|
|
|
2024-12-10 20:27:17 +01:00
|
|
|
documentation = [
|
|
|
|
"https://join-lemmy.org/docs/en/admins/from_scratch.html"
|
|
|
|
"https://join-lemmy.org/docs/en/"
|
|
|
|
];
|
2021-09-23 10:27:42 -04:00
|
|
|
|
2024-12-10 20:27:17 +01:00
|
|
|
wantedBy = [ "multi-user.target" ];
|
2021-09-23 10:27:42 -04:00
|
|
|
|
2024-12-10 20:27:17 +01:00
|
|
|
after = [ "pict-rs.service" ] ++ lib.optionals cfg.database.createLocally [ "postgresql.service" ];
|
2021-09-23 10:27:42 -04:00
|
|
|
|
2024-12-10 20:27:17 +01:00
|
|
|
requires = lib.optionals cfg.database.createLocally [ "postgresql.service" ];
|
2021-09-23 10:27:42 -04:00
|
|
|
|
2024-12-10 20:27:17 +01:00
|
|
|
# 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}
|
|
|
|
'';
|
2023-06-24 19:39:01 +02:00
|
|
|
|
2024-12-10 20:27:17 +01:00
|
|
|
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;
|
|
|
|
};
|
2021-09-23 10:27:42 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
systemd.services.lemmy-ui = {
|
|
|
|
description = "Lemmy ui";
|
|
|
|
|
|
|
|
environment = {
|
|
|
|
LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
|
2023-07-08 11:56:14 +12:00
|
|
|
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";
|
2023-07-13 14:03:40 +12:00
|
|
|
NODE_ENV = "production";
|
2021-09-23 10:27:42 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
documentation = [
|
2023-04-26 22:00:46 +00:00
|
|
|
"https://join-lemmy.org/docs/en/admins/from_scratch.html"
|
|
|
|
"https://join-lemmy.org/docs/en/"
|
2021-09-23 10:27:42 -04:00
|
|
|
];
|
|
|
|
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
|
|
|
|
after = [ "lemmy.service" ];
|
|
|
|
|
|
|
|
requires = [ "lemmy.service" ];
|
|
|
|
|
|
|
|
serviceConfig = {
|
|
|
|
DynamicUser = true;
|
2023-06-06 08:19:37 -07:00
|
|
|
WorkingDirectory = "${cfg.ui.package}";
|
|
|
|
ExecStart = "${pkgs.nodejs}/bin/node ${cfg.ui.package}/dist/js/server.js";
|
2021-09-23 10:27:42 -04:00
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|