at 25.11-pre 12 kB view raw
1{ 2 lib, 3 pkgs, 4 config, 5 utils, 6 ... 7}: 8with lib; 9let 10 cfg = config.services.lemmy; 11 settingsFormat = pkgs.formats.json { }; 12in 13{ 14 meta.maintainers = with maintainers; [ happysalada ]; 15 meta.doc = ./lemmy.md; 16 17 imports = [ 18 (mkRemovedOptionModule [ 19 "services" 20 "lemmy" 21 "jwtSecretPath" 22 ] "As of v0.13.0, Lemmy auto-generates the JWT secret.") 23 ]; 24 25 options.services.lemmy = { 26 27 enable = mkEnableOption "lemmy a federated alternative to reddit in rust"; 28 29 server = { 30 package = mkPackageOption pkgs "lemmy-server" { }; 31 }; 32 33 ui = { 34 package = mkPackageOption pkgs "lemmy-ui" { }; 35 36 port = mkOption { 37 type = types.port; 38 default = 1234; 39 description = "Port where lemmy-ui should listen for incoming requests."; 40 }; 41 }; 42 43 caddy.enable = mkEnableOption "exposing lemmy with the caddy reverse proxy"; 44 nginx.enable = mkEnableOption "exposing lemmy with the nginx reverse proxy"; 45 46 database = { 47 createLocally = mkEnableOption "creation of database on the instance"; 48 49 uri = mkOption { 50 type = with types; nullOr str; 51 default = null; 52 description = "The connection URI to use. Takes priority over the configuration file if set."; 53 }; 54 55 uriFile = mkOption { 56 type = with types; nullOr path; 57 default = null; 58 description = "File which contains the database uri."; 59 }; 60 }; 61 62 pictrsApiKeyFile = mkOption { 63 type = with types; nullOr path; 64 default = null; 65 description = "File which contains the value of `pictrs.api_key`."; 66 }; 67 68 smtpPasswordFile = mkOption { 69 type = with types; nullOr path; 70 default = null; 71 description = "File which contains the value of `email.smtp_password`."; 72 }; 73 74 adminPasswordFile = mkOption { 75 type = with types; nullOr path; 76 default = null; 77 description = "File which contains the value of `setup.admin_password`."; 78 }; 79 80 settings = mkOption { 81 default = { }; 82 description = "Lemmy configuration"; 83 84 type = types.submodule { 85 freeformType = settingsFormat.type; 86 87 options.hostname = mkOption { 88 type = types.str; 89 default = null; 90 description = "The domain name of your instance (eg 'lemmy.ml')."; 91 }; 92 93 options.port = mkOption { 94 type = types.port; 95 default = 8536; 96 description = "Port where lemmy should listen for incoming requests."; 97 }; 98 99 options.captcha = { 100 enabled = mkOption { 101 type = types.bool; 102 default = true; 103 description = "Enable Captcha."; 104 }; 105 difficulty = mkOption { 106 type = types.enum [ 107 "easy" 108 "medium" 109 "hard" 110 ]; 111 default = "medium"; 112 description = "The difficultly of the captcha to solve."; 113 }; 114 }; 115 }; 116 }; 117 }; 118 119 config = 120 let 121 secretOptions = { 122 pictrsApiKeyFile = { 123 setting = [ 124 "pictrs" 125 "api_key" 126 ]; 127 path = cfg.pictrsApiKeyFile; 128 }; 129 smtpPasswordFile = { 130 setting = [ 131 "email" 132 "smtp_password" 133 ]; 134 path = cfg.smtpPasswordFile; 135 }; 136 adminPasswordFile = { 137 setting = [ 138 "setup" 139 "admin_password" 140 ]; 141 path = cfg.adminPasswordFile; 142 }; 143 uriFile = { 144 setting = [ 145 "database" 146 "uri" 147 ]; 148 path = cfg.database.uriFile; 149 }; 150 }; 151 secrets = lib.filterAttrs (option: data: data.path != null) secretOptions; 152 in 153 lib.mkIf cfg.enable { 154 services.lemmy.settings = 155 lib.attrsets.recursiveUpdate 156 ( 157 mapAttrs (name: mkDefault) { 158 bind = "127.0.0.1"; 159 tls_enabled = true; 160 pictrs = { 161 url = with config.services.pict-rs; "http://${address}:${toString port}"; 162 }; 163 actor_name_max_length = 20; 164 165 rate_limit.message = 180; 166 rate_limit.message_per_second = 60; 167 rate_limit.post = 6; 168 rate_limit.post_per_second = 600; 169 rate_limit.register = 3; 170 rate_limit.register_per_second = 3600; 171 rate_limit.image = 6; 172 rate_limit.image_per_second = 3600; 173 } 174 // { 175 database = mapAttrs (name: mkDefault) { 176 user = "lemmy"; 177 host = "/run/postgresql"; 178 port = 5432; 179 database = "lemmy"; 180 pool_size = 5; 181 }; 182 } 183 ) 184 ( 185 lib.foldlAttrs ( 186 acc: option: data: 187 acc // lib.setAttrByPath data.setting { _secret = option; } 188 ) { } secrets 189 ); 190 # the option name is the id of the credential loaded by LoadCredential 191 192 services.postgresql = mkIf cfg.database.createLocally { 193 enable = true; 194 ensureDatabases = [ cfg.settings.database.database ]; 195 ensureUsers = [ 196 { 197 name = cfg.settings.database.user; 198 ensureDBOwnership = true; 199 } 200 ]; 201 }; 202 203 services.pict-rs.enable = true; 204 205 services.caddy = mkIf cfg.caddy.enable { 206 enable = mkDefault true; 207 virtualHosts."${cfg.settings.hostname}" = { 208 extraConfig = '' 209 handle_path /static/* { 210 root * ${cfg.ui.package}/dist 211 file_server 212 } 213 handle_path /static/${cfg.ui.package.passthru.commit_sha}/* { 214 root * ${cfg.ui.package}/dist 215 file_server 216 } 217 @for_backend { 218 path /api/* /pictrs/* /feeds/* /nodeinfo/* 219 } 220 handle @for_backend { 221 reverse_proxy 127.0.0.1:${toString cfg.settings.port} 222 } 223 @post { 224 method POST 225 } 226 handle @post { 227 reverse_proxy 127.0.0.1:${toString cfg.settings.port} 228 } 229 @jsonld { 230 header Accept "application/activity+json" 231 header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" 232 } 233 handle @jsonld { 234 reverse_proxy 127.0.0.1:${toString cfg.settings.port} 235 } 236 handle { 237 reverse_proxy 127.0.0.1:${toString cfg.ui.port} 238 } 239 ''; 240 }; 241 }; 242 243 services.nginx = mkIf cfg.nginx.enable { 244 enable = mkDefault true; 245 virtualHosts."${cfg.settings.hostname}".locations = 246 let 247 ui = "http://127.0.0.1:${toString cfg.ui.port}"; 248 backend = "http://127.0.0.1:${toString cfg.settings.port}"; 249 in 250 { 251 "~ ^/(api|pictrs|feeds|nodeinfo|.well-known)" = { 252 # backend requests 253 proxyPass = backend; 254 proxyWebsockets = true; 255 recommendedProxySettings = true; 256 }; 257 "/" = { 258 # mixed frontend and backend requests, based on the request headers 259 extraConfig = '' 260 set $proxpass "${ui}"; 261 if ($http_accept = "application/activity+json") { 262 set $proxpass "${backend}"; 263 } 264 if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") { 265 set $proxpass "${backend}"; 266 } 267 if ($request_method = POST) { 268 set $proxpass "${backend}"; 269 } 270 271 # Cuts off the trailing slash on URLs to make them valid 272 rewrite ^(.+)/+$ $1 permanent; 273 274 proxy_pass $proxpass; 275 # Proxied `Host` header is required to validate ActivityPub HTTP signatures for incoming events. 276 # The other headers are optional, for the sake of better log data. 277 proxy_set_header X-Real-IP $remote_addr; 278 proxy_set_header Host $host; 279 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 280 ''; 281 }; 282 }; 283 }; 284 285 assertions = [ 286 { 287 assertion = 288 cfg.database.createLocally 289 -> cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql"; 290 message = "if you want to create the database locally, you need to use a local database"; 291 } 292 { 293 assertion = 294 (!(hasAttrByPath [ "federation" ] cfg.settings)) 295 && (!(hasAttrByPath [ "federation" "enabled" ] cfg.settings)); 296 message = "`services.lemmy.settings.federation` was removed in 0.17.0 and no longer has any effect"; 297 } 298 { 299 assertion = cfg.database.uriFile != null -> cfg.database.uri == null && !cfg.database.createLocally; 300 message = "specifying a database uri while also specifying a database uri file is not allowed"; 301 } 302 ]; 303 304 systemd.services.lemmy = 305 let 306 substitutedConfig = "/run/lemmy/config.hjson"; 307 in 308 { 309 description = "Lemmy server"; 310 311 environment = { 312 LEMMY_CONFIG_LOCATION = 313 if secrets == { } then settingsFormat.generate "config.hjson" cfg.settings else substitutedConfig; 314 LEMMY_DATABASE_URL = 315 if cfg.database.uri != null then 316 cfg.database.uri 317 else 318 (mkIf (cfg.database.createLocally) "postgres:///lemmy?host=/run/postgresql&user=lemmy"); 319 }; 320 321 documentation = [ 322 "https://join-lemmy.org/docs/en/admins/from_scratch.html" 323 "https://join-lemmy.org/docs/en/" 324 ]; 325 326 wantedBy = [ "multi-user.target" ]; 327 328 after = [ "pict-rs.service" ] ++ lib.optionals cfg.database.createLocally [ "postgresql.service" ]; 329 330 requires = lib.optionals cfg.database.createLocally [ "postgresql.service" ]; 331 332 # substitute secrets and prevent others from reading the result 333 # if somehow $CREDENTIALS_DIRECTORY is not set we fail 334 preStart = mkIf (secrets != { }) '' 335 set -u 336 umask u=rw,g=,o= 337 cd "$CREDENTIALS_DIRECTORY" 338 ${utils.genJqSecretsReplacementSnippet cfg.settings substitutedConfig} 339 ''; 340 341 serviceConfig = { 342 DynamicUser = true; 343 RuntimeDirectory = "lemmy"; 344 ExecStart = "${cfg.server.package}/bin/lemmy_server"; 345 LoadCredential = lib.foldlAttrs ( 346 acc: option: data: 347 acc ++ [ "${option}:${toString data.path}" ] 348 ) [ ] secrets; 349 PrivateTmp = true; 350 MemoryDenyWriteExecute = true; 351 NoNewPrivileges = true; 352 }; 353 }; 354 355 systemd.services.lemmy-ui = { 356 description = "Lemmy ui"; 357 358 environment = { 359 LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}"; 360 LEMMY_UI_LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}"; 361 LEMMY_UI_LEMMY_EXTERNAL_HOST = cfg.settings.hostname; 362 LEMMY_UI_HTTPS = "false"; 363 NODE_ENV = "production"; 364 }; 365 366 documentation = [ 367 "https://join-lemmy.org/docs/en/admins/from_scratch.html" 368 "https://join-lemmy.org/docs/en/" 369 ]; 370 371 wantedBy = [ "multi-user.target" ]; 372 373 after = [ "lemmy.service" ]; 374 375 requires = [ "lemmy.service" ]; 376 377 serviceConfig = { 378 DynamicUser = true; 379 WorkingDirectory = "${cfg.ui.package}"; 380 ExecStart = "${pkgs.nodejs}/bin/node ${cfg.ui.package}/dist/js/server.js"; 381 }; 382 }; 383 }; 384 385}