]> Untitled Git - awful.systems.git/blob - lemmy/prod/module.nix
58cacb89910784721625cb14989a1e594dfbae66
[awful.systems.git] / lemmy / prod / module.nix
1 { lib, pkgs, config, ... }:
2 with lib;
3 let
4   cfg = config.services.lemmy-prod;
5   settingsFormat = pkgs.formats.json { };
6 in
7 {
8   imports = [
9     (mkRemovedOptionModule [ "services" "lemmy-prod" "jwtSecretPath" ] "As of v0.13.0, Lemmy auto-generates the JWT secret.")
10   ];
11
12   options.services.lemmy-prod = {
13
14     enable = mkEnableOption (lib.mdDoc "lemmy a federated alternative to reddit in rust");
15
16     server = {
17       package = mkPackageOptionMD pkgs "lemmy-server" {};
18     };
19
20     ui = {
21       package = mkPackageOptionMD pkgs "lemmy-ui" {};
22
23       port = mkOption {
24         type = types.port;
25         default = 1234;
26         description = lib.mdDoc "Port where lemmy-ui should listen for incoming requests.";
27       };
28     };
29
30     caddy.enable = mkEnableOption (lib.mdDoc "exposing lemmy with the caddy reverse proxy");
31     nginx.enable = mkEnableOption (lib.mdDoc "exposing lemmy with the nginx reverse proxy");
32
33     database = {
34       createLocally = mkEnableOption (lib.mdDoc "creation of database on the instance");
35
36       uri = mkOption {
37         type = with types; nullOr str;
38         default = null;
39         description = lib.mdDoc "The connection URI to use. Takes priority over the configuration file if set.";
40       };
41     };
42
43     settings = mkOption {
44       default = { };
45       description = lib.mdDoc "Lemmy configuration";
46
47       type = types.submodule {
48         freeformType = settingsFormat.type;
49
50         options.hostname = mkOption {
51           type = types.str;
52           default = null;
53           description = lib.mdDoc "The domain name of your instance (eg 'lemmy.ml').";
54         };
55
56         options.port = mkOption {
57           type = types.port;
58           default = 8536;
59           description = lib.mdDoc "Port where lemmy should listen for incoming requests.";
60         };
61
62         options.captcha = {
63           enabled = mkOption {
64             type = types.bool;
65             default = true;
66             description = lib.mdDoc "Enable Captcha.";
67           };
68           difficulty = mkOption {
69             type = types.enum [ "easy" "medium" "hard" ];
70             default = "medium";
71             description = lib.mdDoc "The difficultly of the captcha to solve.";
72           };
73         };
74       };
75     };
76
77   };
78
79   config =
80     lib.mkIf cfg.enable {
81       services.lemmy-prod.settings = (mapAttrs (name: mkDefault)
82         {
83           bind = "127.0.0.1";
84           tls_enabled = true;
85           pictrs_url = with config.services.pict-rs; "http://${address}:${toString port}";
86           actor_name_max_length = 20;
87
88           rate_limit.message = 180;
89           rate_limit.message_per_second = 60;
90           rate_limit.post = 6;
91           rate_limit.post_per_second = 600;
92           rate_limit.register = 3;
93           rate_limit.register_per_second = 3600;
94           rate_limit.image = 6;
95           rate_limit.image_per_second = 3600;
96         } // {
97         database = mapAttrs (name: mkDefault) {
98           user = "lemmy";
99           host = "/run/postgresql";
100           port = 5432;
101           database = "lemmy";
102           pool_size = 5;
103         };
104       });
105
106       services.postgresql = mkIf cfg.database.createLocally {
107         enable = true;
108         ensureDatabases = [ cfg.settings.database.database ];
109         ensureUsers = [{
110           name = cfg.settings.database.user;
111           ensurePermissions."DATABASE ${cfg.settings.database.database}" = "ALL PRIVILEGES";
112         }];
113       };
114
115       services.pict-rs.enable = true;
116
117       services.caddy = mkIf cfg.caddy.enable {
118         enable = mkDefault true;
119         virtualHosts."${cfg.settings.hostname}" = {
120           extraConfig = ''
121             handle_path /static/* {
122               root * ${cfg.ui.package}/dist
123               file_server
124             }
125             @for_backend {
126               path /api/* /pictrs/* /feeds/* /nodeinfo/*
127             }
128             handle @for_backend {
129               reverse_proxy 127.0.0.1:${toString cfg.settings.port}
130             }
131             @post {
132               method POST
133             }
134             handle @post {
135               reverse_proxy 127.0.0.1:${toString cfg.settings.port}
136             }
137             @jsonld {
138               header Accept "application/activity+json"
139               header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
140             }
141             handle @jsonld {
142               reverse_proxy 127.0.0.1:${toString cfg.settings.port}
143             }
144             handle {
145               reverse_proxy 127.0.0.1:${toString cfg.ui.port}
146             }
147           '';
148         };
149       };
150
151       services.nginx = mkIf cfg.nginx.enable {
152         enable = mkDefault true;
153         virtualHosts."${cfg.settings.hostname}".locations = let
154           ui = "http://127.0.0.1:${toString cfg.ui.port}";
155           backend = "http://127.0.0.1:${toString cfg.settings.port}";
156         in {
157           "~ ^/(api|pictrs|feeds|nodeinfo|.well-known)" = {
158             # backend requests
159             proxyPass = backend;
160             proxyWebsockets = true;
161             recommendedProxySettings = true;
162           };
163           "/" = {
164             # mixed frontend and backend requests, based on the request headers
165             proxyPass = "$proxpass";
166             recommendedProxySettings = true;
167             extraConfig = ''
168               set $proxpass "${ui}";
169               if ($http_accept = "application/activity+json") {
170                 set $proxpass "${backend}";
171               }
172               if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") {
173                 set $proxpass "${backend}";
174               }
175               if ($request_method = POST) {
176                 set $proxpass "${backend}";
177               }
178
179               # Cuts off the trailing slash on URLs to make them valid
180               rewrite ^(.+)/+$ $1 permanent;
181             '';
182           };
183         };
184       };
185
186       assertions = [
187         {
188           assertion = cfg.database.createLocally -> cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql";
189           message = "if you want to create the database locally, you need to use a local database";
190         }
191         {
192           assertion = (!(hasAttrByPath ["federation"] cfg.settings)) && (!(hasAttrByPath ["federation" "enabled"] cfg.settings));
193           message = "`services.lemmy.settings.federation` was removed in 0.17.0 and no longer has any effect";
194         }
195       ];
196
197       systemd.services.lemmy-prod = {
198         description = "Lemmy server (production)";
199
200         environment = {
201           LEMMY_CONFIG_LOCATION = "${settingsFormat.generate "config.hjson" cfg.settings}";
202           LEMMY_DATABASE_URL = mkIf (cfg.database.uri != null) cfg.database.uri;
203         };
204
205         documentation = [
206           "https://join-lemmy.org/docs/en/admins/from_scratch.html"
207           "https://join-lemmy.org/docs/en/"
208         ];
209
210         wantedBy = [ "multi-user.target" ];
211
212         after = [ "pict-rs.service" ] ++ lib.optionals cfg.database.createLocally [ "postgresql.service" ];
213
214         requires = lib.optionals cfg.database.createLocally [ "postgresql.service" ];
215
216         serviceConfig = {
217           DynamicUser = true;
218           RuntimeDirectory = "lemmy";
219           ExecStart = "${cfg.server.package}/bin/lemmy_server";
220         };
221       };
222
223       systemd.services.lemmy-ui-prod = {
224         description = "Lemmy UI (production)";
225
226         environment = {
227           LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
228           LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
229           LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
230           LEMMY_HTTPS = "false";
231           NODE_ENV = "production";
232         };
233
234         documentation = [
235           "https://join-lemmy.org/docs/en/admins/from_scratch.html"
236           "https://join-lemmy.org/docs/en/"
237         ];
238
239         wantedBy = [ "multi-user.target" ];
240
241         after = [ "lemmy-prod.service" ];
242
243         requires = [ "lemmy-prod.service" ];
244
245         serviceConfig = {
246           DynamicUser = true;
247           WorkingDirectory = "${cfg.ui.package}";
248           ExecStart = "${pkgs.nodejs}/bin/node ${cfg.ui.package}/dist/js/server.js";
249         };
250       };
251     };
252
253 }