1 { lib, pkgs, config, utils, ... }:
4 cfg = config.services.lemmy;
5 settingsFormat = pkgs.formats.json { };
8 meta.maintainers = with maintainers; [ happysalada ];
12 (mkRemovedOptionModule [ "services" "lemmy" "jwtSecretPath" ] "As of v0.13.0, Lemmy auto-generates the JWT secret.")
15 options.services.lemmy = {
17 enable = mkEnableOption "lemmy a federated alternative to reddit in rust";
20 package = mkPackageOption pkgs "lemmy-server" {};
24 package = mkPackageOption pkgs "lemmy-ui" {};
29 description = "Port where lemmy-ui should listen for incoming requests.";
33 caddy.enable = mkEnableOption "exposing lemmy with the caddy reverse proxy";
34 nginx.enable = mkEnableOption "exposing lemmy with the nginx reverse proxy";
37 createLocally = mkEnableOption "creation of database on the instance";
40 type = with types; nullOr str;
42 description = "The connection URI to use. Takes priority over the configuration file if set.";
46 type = with types; nullOr path;
48 description = "File which contains the database uri.";
52 pictrsApiKeyFile = mkOption {
53 type = with types; nullOr path;
55 description = "File which contains the value of `pictrs.api_key`.";
58 smtpPasswordFile = mkOption {
59 type = with types; nullOr path;
61 description = "File which contains the value of `email.smtp_password`.";
64 adminPasswordFile = mkOption {
65 type = with types; nullOr path;
67 description = "File which contains the value of `setup.admin_password`.";
72 description = "Lemmy configuration";
74 type = types.submodule {
75 freeformType = settingsFormat.type;
77 options.hostname = mkOption {
80 description = "The domain name of your instance (eg 'lemmy.ml').";
83 options.port = mkOption {
86 description = "Port where lemmy should listen for incoming requests.";
93 description = "Enable Captcha.";
95 difficulty = mkOption {
96 type = types.enum [ "easy" "medium" "hard" ];
98 description = "The difficultly of the captcha to solve.";
108 pictrsApiKeyFile = { setting = [ "pictrs" "api_key" ]; path = cfg.pictrsApiKeyFile; };
109 smtpPasswordFile = { setting = [ "email" "smtp_password" ]; path = cfg.smtpPasswordFile; };
110 adminPasswordFile = { setting = [ "setup" "admin_password" ]; path = cfg.adminPasswordFile; };
111 uriFile = { setting = [ "database" "uri" ]; path = cfg.database.uriFile; };
113 secrets = lib.filterAttrs (option: data: data.path != null) secretOptions;
115 lib.mkIf cfg.enable {
116 services.lemmy.settings = lib.attrsets.recursiveUpdate (mapAttrs (name: mkDefault)
121 url = with config.services.pict-rs; "http://${address}:${toString port}";
123 actor_name_max_length = 20;
125 rate_limit.message = 180;
126 rate_limit.message_per_second = 60;
128 rate_limit.post_per_second = 600;
129 rate_limit.register = 3;
130 rate_limit.register_per_second = 3600;
131 rate_limit.image = 6;
132 rate_limit.image_per_second = 3600;
134 database = mapAttrs (name: mkDefault) {
136 host = "/run/postgresql";
141 }) (lib.foldlAttrs (acc: option: data: acc // lib.setAttrByPath data.setting { _secret = option; }) {} secrets);
142 # the option name is the id of the credential loaded by LoadCredential
144 services.postgresql = mkIf cfg.database.createLocally {
146 ensureDatabases = [ cfg.settings.database.database ];
148 name = cfg.settings.database.user;
149 ensureDBOwnership = true;
153 services.pict-rs.enable = true;
155 services.caddy = mkIf cfg.caddy.enable {
156 enable = mkDefault true;
157 virtualHosts."${cfg.settings.hostname}" = {
159 handle_path /static/* {
160 root * ${cfg.ui.package}/dist
163 handle_path /static/${cfg.ui.package.passthru.commit_sha}/* {
164 root * ${cfg.ui.package}/dist
168 path /api/* /pictrs/* /feeds/* /nodeinfo/*
170 handle @for_backend {
171 reverse_proxy 127.0.0.1:${toString cfg.settings.port}
177 reverse_proxy 127.0.0.1:${toString cfg.settings.port}
180 header Accept "application/activity+json"
181 header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
184 reverse_proxy 127.0.0.1:${toString cfg.settings.port}
187 reverse_proxy 127.0.0.1:${toString cfg.ui.port}
193 services.nginx = mkIf cfg.nginx.enable {
194 enable = mkDefault true;
195 virtualHosts."${cfg.settings.hostname}".locations = let
196 ui = "http://127.0.0.1:${toString cfg.ui.port}";
197 backend = "http://127.0.0.1:${toString cfg.settings.port}";
199 "~ ^/(api|pictrs|feeds|nodeinfo|.well-known)" = {
202 proxyWebsockets = true;
203 recommendedProxySettings = true;
206 # mixed frontend and backend requests, based on the request headers
207 recommendedProxySettings = true;
209 set $proxpass "${ui}";
210 if ($http_accept = "application/activity+json") {
211 set $proxpass "${backend}";
213 if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") {
214 set $proxpass "${backend}";
216 if ($request_method = POST) {
217 set $proxpass "${backend}";
220 # Cuts off the trailing slash on URLs to make them valid
221 rewrite ^(.+)/+$ $1 permanent;
223 proxy_pass $proxpass;
231 assertion = cfg.database.createLocally -> cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql";
232 message = "if you want to create the database locally, you need to use a local database";
235 assertion = (!(hasAttrByPath ["federation"] cfg.settings)) && (!(hasAttrByPath ["federation" "enabled"] cfg.settings));
236 message = "`services.lemmy.settings.federation` was removed in 0.17.0 and no longer has any effect";
239 assertion = cfg.database.uriFile != null -> cfg.database.uri == null && !cfg.database.createLocally;
240 message = "specifying a database uri while also specifying a database uri file is not allowed";
244 systemd.services.lemmy = let
245 substitutedConfig = "/run/lemmy/config.hjson";
247 description = "Lemmy server";
250 LEMMY_CONFIG_LOCATION = if secrets == {} then settingsFormat.generate "config.hjson" cfg.settings else substitutedConfig;
251 LEMMY_DATABASE_URL = if cfg.database.uri != null then cfg.database.uri else (mkIf (cfg.database.createLocally) "postgres:///lemmy?host=/run/postgresql&user=lemmy");
255 "https://join-lemmy.org/docs/en/admins/from_scratch.html"
256 "https://join-lemmy.org/docs/en/"
259 wantedBy = [ "multi-user.target" ];
261 after = [ "pict-rs.service" ] ++ lib.optionals cfg.database.createLocally [ "postgresql.service" ];
263 requires = lib.optionals cfg.database.createLocally [ "postgresql.service" ];
265 # substitute secrets and prevent others from reading the result
266 # if somehow $CREDENTIALS_DIRECTORY is not set we fail
267 preStart = mkIf (secrets != {}) ''
270 cd "$CREDENTIALS_DIRECTORY"
271 ${utils.genJqSecretsReplacementSnippet cfg.settings substitutedConfig}
276 RuntimeDirectory = "lemmy";
277 ExecStart = "${cfg.server.package}/bin/lemmy_server";
278 LoadCredential = lib.foldlAttrs (acc: option: data: acc ++ [ "${option}:${toString data.path}" ]) [] secrets;
280 MemoryDenyWriteExecute = true;
281 NoNewPrivileges = true;
285 systemd.services.lemmy-ui = {
286 description = "Lemmy ui";
289 LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
290 LEMMY_UI_LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
291 LEMMY_UI_LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
292 LEMMY_UI_HTTPS = "false";
293 NODE_ENV = "production";
297 "https://join-lemmy.org/docs/en/admins/from_scratch.html"
298 "https://join-lemmy.org/docs/en/"
301 wantedBy = [ "multi-user.target" ];
303 after = [ "lemmy.service" ];
305 requires = [ "lemmy.service" ];
309 WorkingDirectory = "${cfg.ui.package}";
310 ExecStart = "${pkgs.nodejs}/bin/node ${cfg.ui.package}/dist/js/server.js";