1{ lib, pkgs, config, ... }:
2with lib;
3let
4 cfg = config.services.lemmy;
5 settingsFormat = pkgs.formats.json { };
6in
7{
8 meta.maintainers = with maintainers; [ happysalada ];
9 meta.doc = ./lemmy.md;
10
11 imports = [
12 (mkRemovedOptionModule [ "services" "lemmy" "jwtSecretPath" ] "As of v0.13.0, Lemmy auto-generates the JWT secret.")
13 ];
14
15 options.services.lemmy = {
16
17 enable = mkEnableOption (lib.mdDoc "lemmy a federated alternative to reddit in rust");
18
19 ui = {
20 port = mkOption {
21 type = types.port;
22 default = 1234;
23 description = lib.mdDoc "Port where lemmy-ui should listen for incoming requests.";
24 };
25 };
26
27 caddy.enable = mkEnableOption (lib.mdDoc "exposing lemmy with the caddy reverse proxy");
28
29 database.createLocally = mkEnableOption (lib.mdDoc "creation of database on the instance");
30
31 settings = mkOption {
32 default = { };
33 description = lib.mdDoc "Lemmy configuration";
34
35 type = types.submodule {
36 freeformType = settingsFormat.type;
37
38 options.hostname = mkOption {
39 type = types.str;
40 default = null;
41 description = lib.mdDoc "The domain name of your instance (eg 'lemmy.ml').";
42 };
43
44 options.port = mkOption {
45 type = types.port;
46 default = 8536;
47 description = lib.mdDoc "Port where lemmy should listen for incoming requests.";
48 };
49
50 options.federation = {
51 enabled = mkEnableOption (lib.mdDoc "activitypub federation");
52 };
53
54 options.captcha = {
55 enabled = mkOption {
56 type = types.bool;
57 default = true;
58 description = lib.mdDoc "Enable Captcha.";
59 };
60 difficulty = mkOption {
61 type = types.enum [ "easy" "medium" "hard" ];
62 default = "medium";
63 description = lib.mdDoc "The difficultly of the captcha to solve.";
64 };
65 };
66 };
67 };
68
69 };
70
71 config =
72 lib.mkIf cfg.enable {
73 services.lemmy.settings = (mapAttrs (name: mkDefault)
74 {
75 bind = "127.0.0.1";
76 tls_enabled = true;
77 pictrs_url = with config.services.pict-rs; "http://${address}:${toString port}";
78 actor_name_max_length = 20;
79
80 rate_limit.message = 180;
81 rate_limit.message_per_second = 60;
82 rate_limit.post = 6;
83 rate_limit.post_per_second = 600;
84 rate_limit.register = 3;
85 rate_limit.register_per_second = 3600;
86 rate_limit.image = 6;
87 rate_limit.image_per_second = 3600;
88 } // {
89 database = mapAttrs (name: mkDefault) {
90 user = "lemmy";
91 host = "/run/postgresql";
92 port = 5432;
93 database = "lemmy";
94 pool_size = 5;
95 };
96 });
97
98 services.postgresql = mkIf cfg.database.createLocally {
99 enable = true;
100 ensureDatabases = [ cfg.settings.database.database ];
101 ensureUsers = [{
102 name = cfg.settings.database.user;
103 ensurePermissions."DATABASE ${cfg.settings.database.database}" = "ALL PRIVILEGES";
104 }];
105 };
106
107 services.pict-rs.enable = true;
108
109 services.caddy = mkIf cfg.caddy.enable {
110 enable = mkDefault true;
111 virtualHosts."${cfg.settings.hostname}" = {
112 extraConfig = ''
113 handle_path /static/* {
114 root * ${pkgs.lemmy-ui}/dist
115 file_server
116 }
117 @for_backend {
118 path /api/* /pictrs/* /feeds/* /nodeinfo/*
119 }
120 handle @for_backend {
121 reverse_proxy 127.0.0.1:${toString cfg.settings.port}
122 }
123 @post {
124 method POST
125 }
126 handle @post {
127 reverse_proxy 127.0.0.1:${toString cfg.settings.port}
128 }
129 @jsonld {
130 header Accept "application/activity+json"
131 header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
132 }
133 handle @jsonld {
134 reverse_proxy 127.0.0.1:${toString cfg.settings.port}
135 }
136 handle {
137 reverse_proxy 127.0.0.1:${toString cfg.ui.port}
138 }
139 '';
140 };
141 };
142
143 assertions = [{
144 assertion = cfg.database.createLocally -> cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql";
145 message = "if you want to create the database locally, you need to use a local database";
146 }];
147
148 systemd.services.lemmy = {
149 description = "Lemmy server";
150
151 environment = {
152 LEMMY_CONFIG_LOCATION = "/run/lemmy/config.hjson";
153
154 # Verify how this is used, and don't put the password in the nix store
155 LEMMY_DATABASE_URL = with cfg.settings.database;"postgres:///${database}?host=${host}";
156 };
157
158 documentation = [
159 "https://join-lemmy.org/docs/en/admins/from_scratch.html"
160 "https://join-lemmy.org/docs/en/"
161 ];
162
163 wantedBy = [ "multi-user.target" ];
164
165 after = [ "pict-rs.service" ] ++ lib.optionals cfg.database.createLocally [ "postgresql.service" ];
166
167 requires = lib.optionals cfg.database.createLocally [ "postgresql.service" ];
168
169 serviceConfig = {
170 DynamicUser = true;
171 RuntimeDirectory = "lemmy";
172 ExecStartPre = "${pkgs.coreutils}/bin/install -m 600 ${settingsFormat.generate "config.hjson" cfg.settings} /run/lemmy/config.hjson";
173 ExecStart = "${pkgs.lemmy-server}/bin/lemmy_server";
174 };
175 };
176
177 systemd.services.lemmy-ui = {
178 description = "Lemmy ui";
179
180 environment = {
181 LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
182 LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
183 LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
184 LEMMY_HTTPS = "false";
185 };
186
187 documentation = [
188 "https://join-lemmy.org/docs/en/admins/from_scratch.html"
189 "https://join-lemmy.org/docs/en/"
190 ];
191
192 wantedBy = [ "multi-user.target" ];
193
194 after = [ "lemmy.service" ];
195
196 requires = [ "lemmy.service" ];
197
198 serviceConfig = {
199 DynamicUser = true;
200 WorkingDirectory = "${pkgs.lemmy-ui}";
201 ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.lemmy-ui}/dist/js/server.js";
202 };
203 };
204 };
205
206}