1 { lib, pkgs, config, ... }:
4 cfg = config.services.lemmy-prod;
5 settingsFormat = pkgs.formats.json { };
8 (mkRemovedOptionModule [ "services" "lemmy-prod" "jwtSecretPath" ]
9 "As of v0.13.0, Lemmy auto-generates the JWT secret.")
12 options.services.lemmy-prod = {
14 enable = mkEnableOption
15 (lib.mdDoc "lemmy a federated alternative to reddit in rust");
17 server = { package = mkPackageOptionMD pkgs "lemmy-server" { }; };
20 package = mkPackageOptionMD pkgs "lemmy-ui" { };
26 lib.mdDoc "Port where lemmy-ui should listen for incoming requests.";
31 mkEnableOption (lib.mdDoc "exposing lemmy with the caddy reverse proxy");
33 mkEnableOption (lib.mdDoc "exposing lemmy with the nginx reverse proxy");
37 mkEnableOption (lib.mdDoc "creation of database on the instance");
40 type = with types; nullOr str;
42 description = lib.mdDoc
43 "The connection URI to use. Takes priority over the configuration file if set.";
49 description = lib.mdDoc "Lemmy configuration";
51 type = types.submodule {
52 freeformType = settingsFormat.type;
54 options.hostname = mkOption {
58 lib.mdDoc "The domain name of your instance (eg 'lemmy.ml').";
61 options.port = mkOption {
65 lib.mdDoc "Port where lemmy should listen for incoming requests.";
72 description = lib.mdDoc "Enable Captcha.";
74 difficulty = mkOption {
75 type = types.enum [ "easy" "medium" "hard" ];
77 description = lib.mdDoc "The difficultly of the captcha to solve.";
85 config = lib.mkIf cfg.enable {
86 services.lemmy-prod.settings = (mapAttrs (name: mkDefault) {
89 pictrs_url = with config.services.pict-rs;
90 "http://${address}:${toString port}";
91 actor_name_max_length = 20;
93 rate_limit.message = 180;
94 rate_limit.message_per_second = 60;
96 rate_limit.post_per_second = 600;
97 rate_limit.register = 3;
98 rate_limit.register_per_second = 3600;
100 rate_limit.image_per_second = 3600;
102 database = mapAttrs (name: mkDefault) {
104 host = "/run/postgresql";
111 services.postgresql = mkIf cfg.database.createLocally {
113 ensureDatabases = [ cfg.settings.database.database ];
115 name = cfg.settings.database.user;
116 ensurePermissions."DATABASE ${cfg.settings.database.database}" =
121 services.pict-rs.enable = true;
123 services.caddy = mkIf cfg.caddy.enable {
124 enable = mkDefault true;
125 virtualHosts."${cfg.settings.hostname}" = {
127 handle_path /static/* {
128 root * ${cfg.ui.package}/dist
132 path /api/* /pictrs/* /feeds/* /nodeinfo/*
134 handle @for_backend {
135 reverse_proxy 127.0.0.1:${toString cfg.settings.port}
141 reverse_proxy 127.0.0.1:${toString cfg.settings.port}
144 header Accept "application/activity+json"
145 header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
148 reverse_proxy 127.0.0.1:${toString cfg.settings.port}
151 reverse_proxy 127.0.0.1:${toString cfg.ui.port}
158 ui = "http://127.0.0.1:${toString cfg.ui.port}";
159 backend = "http://127.0.0.1:${toString cfg.settings.port}";
160 in mkIf cfg.nginx.enable {
161 enable = mkDefault true;
163 worker_connections 20000;
165 appendHttpConfig = ''
166 map "$request_method:$http_accept" $proxpass {
167 # If no explicit matches exists below, send traffic to lemmy-ui
170 # GET/HEAD requests that accepts ActivityPub or Linked Data JSON should go to lemmy.
172 # These requests are used by Mastodon and other fediverse instances to look up profile information,
173 # discover site information and so on.
174 "~^(?:GET|HEAD):.*?application\/(?:activity|ld)\+json" "${backend}";
176 # All non-GET/HEAD requests should go to lemmy
178 # Rather than calling out POST, PUT, DELETE, PATCH, CONNECT and all the verbs manually
179 # we simply negate the GET|HEAD pattern from above and accept all possibly $http_accept values
180 "~^(?!(GET|HEAD)).*:" "${backend}";
183 virtualHosts."${cfg.settings.hostname}".locations = {
184 "~ ^/(api|pictrs|feeds|nodeinfo|.well-known)" = {
187 proxyWebsockets = true;
188 recommendedProxySettings = true;
191 # mixed frontend and backend requests, based on the request headers
192 proxyPass = "$proxpass";
193 recommendedProxySettings = true;
195 # Cuts off the trailing slash on URLs to make them valid
196 rewrite ^(.+)/+$ $1 permanent;
204 assertion = cfg.database.createLocally -> cfg.settings.database.host
205 == "localhost" || cfg.settings.database.host == "/run/postgresql";
207 "if you want to create the database locally, you need to use a local database";
210 assertion = (!(hasAttrByPath [ "federation" ] cfg.settings))
211 && (!(hasAttrByPath [ "federation" "enabled" ] cfg.settings));
213 "`services.lemmy.settings.federation` was removed in 0.17.0 and no longer has any effect";
217 systemd.services.lemmy-prod = {
218 description = "Lemmy server (production)";
221 LEMMY_CONFIG_LOCATION =
222 "${settingsFormat.generate "config.hjson" cfg.settings}";
223 LEMMY_DATABASE_URL = mkIf (cfg.database.uri != null) cfg.database.uri;
227 "https://join-lemmy.org/docs/en/admins/from_scratch.html"
228 "https://join-lemmy.org/docs/en/"
231 wantedBy = [ "multi-user.target" ];
233 after = [ "pict-rs.service" ]
234 ++ lib.optionals cfg.database.createLocally [ "postgresql.service" ];
237 lib.optionals cfg.database.createLocally [ "postgresql.service" ];
241 RuntimeDirectory = "lemmy";
242 ExecStart = "${cfg.server.package}/bin/lemmy_server";
246 systemd.services.lemmy-ui-prod = {
247 description = "Lemmy UI (production)";
250 LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
251 LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
252 LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
253 LEMMY_HTTPS = "false";
254 NODE_ENV = "production";
258 "https://join-lemmy.org/docs/en/admins/from_scratch.html"
259 "https://join-lemmy.org/docs/en/"
262 wantedBy = [ "multi-user.target" ];
264 after = [ "lemmy-prod.service" ];
266 requires = [ "lemmy-prod.service" ];
270 WorkingDirectory = "${cfg.ui.package}";
272 "${pkgs.nodejs}/bin/node ${cfg.ui.package}/dist/js/server.js";