vuls: init at 0.27.0 (#348530)
[NixPkgs.git] / nixos / modules / services / web-apps / castopod.nix
blobd3750c3dd3938abaf574b45464e40ad00c366428
1 { config, lib, pkgs, ... }:
2 let
3   cfg = config.services.castopod;
4   fpm = config.services.phpfpm.pools.castopod;
6   user = "castopod";
8   # https://docs.castopod.org/getting-started/install.html#requirements
9   phpPackage = pkgs.php.withExtensions ({ enabled, all }: with all; [
10     intl
11     curl
12     mbstring
13     gd
14     exif
15     mysqlnd
16   ] ++ enabled);
19   meta.doc = ./castopod.md;
20   meta.maintainers = with lib.maintainers; [ alexoundos ];
22   options.services = {
23     castopod = {
24       enable = lib.mkEnableOption "Castopod, a hosting platform for podcasters";
25       package = lib.mkOption {
26         type = lib.types.package;
27         default = pkgs.castopod;
28         defaultText = lib.literalMD "pkgs.castopod";
29         description = "Which Castopod package to use.";
30       };
31       dataDir = lib.mkOption {
32         type = lib.types.path;
33         default = "/var/lib/castopod";
34         description = ''
35           The path where castopod stores all data. This path must be in sync
36           with the castopod package (where it is hardcoded during the build in
37           accordance with its own `dataDir` argument).
38         '';
39       };
40       database = {
41         createLocally = lib.mkOption {
42           type = lib.types.bool;
43           default = true;
44           description = ''
45             Create the database and database user locally.
46           '';
47         };
48         hostname = lib.mkOption {
49           type = lib.types.str;
50           default = "localhost";
51           description = "Database hostname.";
52         };
53         name = lib.mkOption {
54           type = lib.types.str;
55           default = "castopod";
56           description = "Database name.";
57         };
58         user = lib.mkOption {
59           type = lib.types.str;
60           default = user;
61           description = "Database user.";
62         };
63         passwordFile = lib.mkOption {
64           type = lib.types.nullOr lib.types.path;
65           default = null;
66           example = "/run/keys/castopod-dbpassword";
67           description = ''
68             A file containing the password corresponding to
69             [](#opt-services.castopod.database.user).
71             This file is loaded using systemd LoadCredentials.
72           '';
73         };
74       };
75       settings = lib.mkOption {
76         type = with lib.types; attrsOf (oneOf [ str int bool ]);
77         default = { };
78         example = {
79           "email.protocol" = "smtp";
80           "email.SMTPHost" = "localhost";
81           "email.SMTPUser" = "myuser";
82           "email.fromEmail" = "castopod@example.com";
83         };
84         description = ''
85           Environment variables used for Castopod.
86           See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example)
87           for available environment variables.
88         '';
89       };
90       environmentFile = lib.mkOption {
91         type = lib.types.nullOr lib.types.path;
92         default = null;
93         example = "/run/keys/castopod-env";
94         description = ''
95           Environment file to inject e.g. secrets into the configuration.
96           See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example)
97           for available environment variables.
99           This file is loaded using systemd LoadCredentials.
100         '';
101       };
102       configureNginx = lib.mkOption {
103         type = lib.types.bool;
104         default = true;
105         description = "Configure nginx as a reverse proxy for CastoPod.";
106       };
107       localDomain = lib.mkOption {
108         type = lib.types.str;
109         example = "castopod.example.org";
110         description = "The domain serving your CastoPod instance.";
111       };
112       poolSettings = lib.mkOption {
113         type = with lib.types; attrsOf (oneOf [ str int bool ]);
114         default = {
115           "pm" = "dynamic";
116           "pm.max_children" = "32";
117           "pm.start_servers" = "2";
118           "pm.min_spare_servers" = "2";
119           "pm.max_spare_servers" = "4";
120           "pm.max_requests" = "500";
121         };
122         description = ''
123           Options for Castopod's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
124         '';
125       };
126       maxUploadSize = lib.mkOption {
127         type = lib.types.str;
128         default = "512M";
129         description = ''
130           Maximum supported size for a file upload in. Maximum HTTP body
131           size is set to this value for nginx and PHP (because castopod doesn't
132           support chunked uploads yet:
133           https://code.castopod.org/adaures/castopod/-/issues/330).
135           Note, that practical upload size limit is smaller. For example, with
136           512 MiB setting - around 500 MiB is possible.
137         '';
138       };
139     };
140   };
142   config = lib.mkIf cfg.enable {
143     services.castopod.settings =
144       let
145         sslEnabled = with config.services.nginx.virtualHosts.${cfg.localDomain}; addSSL || forceSSL || onlySSL || enableACME || useACMEHost != null;
146         baseURL = "http${lib.optionalString sslEnabled "s"}://${cfg.localDomain}";
147       in
148       lib.mapAttrs (_: lib.mkDefault) {
149         "app.forceGlobalSecureRequests" = sslEnabled;
150         "app.baseURL" = baseURL;
152         "media.baseURL" = baseURL;
153         "media.root" = "media";
154         "media.storage" = cfg.dataDir;
156         "admin.gateway" = "admin";
157         "auth.gateway" = "auth";
159         "database.default.hostname" = cfg.database.hostname;
160         "database.default.database" = cfg.database.name;
161         "database.default.username" = cfg.database.user;
162         "database.default.DBPrefix" = "cp_";
164         "cache.handler" = "file";
165       };
167     services.phpfpm.pools.castopod = {
168       inherit user;
169       group = config.services.nginx.group;
170       inherit phpPackage;
171       phpOptions = ''
172         # https://code.castopod.org/adaures/castopod/-/blob/develop/docker/production/common/uploads.template.ini
173         file_uploads = On
174         memory_limit = 512M
175         upload_max_filesize = ${cfg.maxUploadSize}
176         post_max_size = ${cfg.maxUploadSize}
177         max_execution_time = 300
178         max_input_time = 300
179       '';
180       settings = {
181         "listen.owner" = config.services.nginx.user;
182         "listen.group" = config.services.nginx.group;
183       } // cfg.poolSettings;
184     };
186     systemd.services.castopod-setup = {
187       after = lib.optional config.services.mysql.enable "mysql.service";
188       requires = lib.optional config.services.mysql.enable "mysql.service";
189       wantedBy = [ "multi-user.target" ];
190       path = [ pkgs.openssl phpPackage ];
191       script =
192         let
193           envFile = "${cfg.dataDir}/.env";
194           media = "${cfg.settings."media.storage"}/${cfg.settings."media.root"}";
195         in
196         ''
197           mkdir -p ${cfg.dataDir}/writable/{cache,logs,session,temp,uploads}
199           if [ ! -d ${lib.escapeShellArg media} ]; then
200             cp --no-preserve=mode,ownership -r ${cfg.package}/share/castopod/public/media ${lib.escapeShellArg media}
201           fi
203           if [ ! -f ${cfg.dataDir}/salt ]; then
204             openssl rand -base64 33 > ${cfg.dataDir}/salt
205           fi
207           cat <<'EOF' > ${envFile}
208           ${lib.generators.toKeyValue { } cfg.settings}
209           EOF
211           echo "analytics.salt=$(cat ${cfg.dataDir}/salt)" >> ${envFile}
213           ${if (cfg.database.passwordFile != null) then ''
214             echo "database.default.password=$(cat "$CREDENTIALS_DIRECTORY/dbpasswordfile)" >> ${envFile}
215           '' else ''
216             echo "database.default.password=" >> ${envFile}
217           ''}
219           ${lib.optionalString (cfg.environmentFile != null) ''
220             cat "$CREDENTIALS_DIRECTORY/envfile" >> ${envFile}
221           ''}
223           php ${cfg.package}/share/castopod/spark castopod:database-update
224         '';
225       serviceConfig = {
226         StateDirectory = "castopod";
227         LoadCredential = lib.optional (cfg.environmentFile != null)
228           "envfile:${cfg.environmentFile}"
229         ++ (lib.optional (cfg.database.passwordFile != null)
230           "dbpasswordfile:${cfg.database.passwordFile}");
231         WorkingDirectory = "${cfg.package}/share/castopod";
232         Type = "oneshot";
233         RemainAfterExit = true;
234         User = user;
235         Group = config.services.nginx.group;
236         ReadWritePaths = cfg.dataDir;
237       };
238     };
240     systemd.services.castopod-scheduled = {
241       after = [ "castopod-setup.service" ];
242       wantedBy = [ "multi-user.target" ];
243       path = [ phpPackage ];
244       script = ''
245         php ${cfg.package}/share/castopod/spark tasks:run
246       '';
247       serviceConfig = {
248         StateDirectory = "castopod";
249         WorkingDirectory = "${cfg.package}/share/castopod";
250         Type = "oneshot";
251         User = user;
252         Group = config.services.nginx.group;
253         ReadWritePaths = cfg.dataDir;
254         LogLevelMax = "notice"; # otherwise periodic tasks flood the journal
255       };
256     };
258     systemd.timers.castopod-scheduled = {
259       wantedBy = [ "timers.target" ];
260       timerConfig = {
261         OnCalendar = "*-*-* *:*:00";
262         Unit = "castopod-scheduled.service";
263       };
264     };
266     services.mysql = lib.mkIf cfg.database.createLocally {
267       enable = true;
268       package = lib.mkDefault pkgs.mariadb;
269       ensureDatabases = [ cfg.database.name ];
270       ensureUsers = [{
271         name = cfg.database.user;
272         ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
273       }];
274     };
276     services.nginx = lib.mkIf cfg.configureNginx {
277       enable = true;
278       virtualHosts."${cfg.localDomain}" = {
279         root = lib.mkForce "${cfg.package}/share/castopod/public";
281         extraConfig = ''
282           try_files $uri $uri/ /index.php?$args;
283           index index.php index.html;
284           client_max_body_size ${cfg.maxUploadSize};
285         '';
287         locations."^~ /${cfg.settings."media.root"}/" = {
288           root = cfg.settings."media.storage";
289           extraConfig = ''
290             add_header Access-Control-Allow-Origin "*";
291             expires max;
292             access_log off;
293           '';
294         };
296         locations."~ \.php$" = {
297           fastcgiParams = {
298             SERVER_NAME = "$host";
299           };
300           extraConfig = ''
301             fastcgi_intercept_errors on;
302             fastcgi_index index.php;
303             fastcgi_pass unix:${fpm.socket};
304             try_files $uri =404;
305             fastcgi_read_timeout 3600;
306             fastcgi_send_timeout 3600;
307           '';
308         };
309       };
310     };
312     users.users.${user} = lib.mapAttrs (_: lib.mkDefault) {
313       description = "Castopod user";
314       isSystemUser = true;
315       group = config.services.nginx.group;
316     };
317   };