1 { config, lib, pkgs, ... }:
3 cfg = config.services.castopod;
4 fpm = config.services.phpfpm.pools.castopod;
8 # https://docs.castopod.org/getting-started/install.html#requirements
9 phpPackage = pkgs.php.withExtensions ({ enabled, all }: with all; [
19 meta.doc = ./castopod.md;
20 meta.maintainers = with lib.maintainers; [ alexoundos ];
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.";
31 dataDir = lib.mkOption {
32 type = lib.types.path;
33 default = "/var/lib/castopod";
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).
41 createLocally = lib.mkOption {
42 type = lib.types.bool;
45 Create the database and database user locally.
48 hostname = lib.mkOption {
50 default = "localhost";
51 description = "Database hostname.";
56 description = "Database name.";
61 description = "Database user.";
63 passwordFile = lib.mkOption {
64 type = lib.types.nullOr lib.types.path;
66 example = "/run/keys/castopod-dbpassword";
68 A file containing the password corresponding to
69 [](#opt-services.castopod.database.user).
71 This file is loaded using systemd LoadCredentials.
75 settings = lib.mkOption {
76 type = with lib.types; attrsOf (oneOf [ str int bool ]);
79 "email.protocol" = "smtp";
80 "email.SMTPHost" = "localhost";
81 "email.SMTPUser" = "myuser";
82 "email.fromEmail" = "castopod@example.com";
85 Environment variables used for Castopod.
86 See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example)
87 for available environment variables.
90 environmentFile = lib.mkOption {
91 type = lib.types.nullOr lib.types.path;
93 example = "/run/keys/castopod-env";
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.
102 configureNginx = lib.mkOption {
103 type = lib.types.bool;
105 description = "Configure nginx as a reverse proxy for CastoPod.";
107 localDomain = lib.mkOption {
108 type = lib.types.str;
109 example = "castopod.example.org";
110 description = "The domain serving your CastoPod instance.";
112 poolSettings = lib.mkOption {
113 type = with lib.types; attrsOf (oneOf [ str int bool ]);
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";
123 Options for Castopod's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
126 maxUploadSize = lib.mkOption {
127 type = lib.types.str;
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.
142 config = lib.mkIf cfg.enable {
143 services.castopod.settings =
145 sslEnabled = with config.services.nginx.virtualHosts.${cfg.localDomain}; addSSL || forceSSL || onlySSL || enableACME || useACMEHost != null;
146 baseURL = "http${lib.optionalString sslEnabled "s"}://${cfg.localDomain}";
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";
167 services.phpfpm.pools.castopod = {
169 group = config.services.nginx.group;
172 # https://code.castopod.org/adaures/castopod/-/blob/develop/docker/production/common/uploads.template.ini
175 upload_max_filesize = ${cfg.maxUploadSize}
176 post_max_size = ${cfg.maxUploadSize}
177 max_execution_time = 300
181 "listen.owner" = config.services.nginx.user;
182 "listen.group" = config.services.nginx.group;
183 } // cfg.poolSettings;
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 ];
193 envFile = "${cfg.dataDir}/.env";
194 media = "${cfg.settings."media.storage"}/${cfg.settings."media.root"}";
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}
203 if [ ! -f ${cfg.dataDir}/salt ]; then
204 openssl rand -base64 33 > ${cfg.dataDir}/salt
207 cat <<'EOF' > ${envFile}
208 ${lib.generators.toKeyValue { } cfg.settings}
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}
216 echo "database.default.password=" >> ${envFile}
219 ${lib.optionalString (cfg.environmentFile != null) ''
220 cat "$CREDENTIALS_DIRECTORY/envfile" >> ${envFile}
223 php ${cfg.package}/share/castopod/spark castopod:database-update
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";
233 RemainAfterExit = true;
235 Group = config.services.nginx.group;
236 ReadWritePaths = cfg.dataDir;
240 systemd.services.castopod-scheduled = {
241 after = [ "castopod-setup.service" ];
242 wantedBy = [ "multi-user.target" ];
243 path = [ phpPackage ];
245 php ${cfg.package}/share/castopod/spark tasks:run
248 StateDirectory = "castopod";
249 WorkingDirectory = "${cfg.package}/share/castopod";
252 Group = config.services.nginx.group;
253 ReadWritePaths = cfg.dataDir;
254 LogLevelMax = "notice"; # otherwise periodic tasks flood the journal
258 systemd.timers.castopod-scheduled = {
259 wantedBy = [ "timers.target" ];
261 OnCalendar = "*-*-* *:*:00";
262 Unit = "castopod-scheduled.service";
266 services.mysql = lib.mkIf cfg.database.createLocally {
268 package = lib.mkDefault pkgs.mariadb;
269 ensureDatabases = [ cfg.database.name ];
271 name = cfg.database.user;
272 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
276 services.nginx = lib.mkIf cfg.configureNginx {
278 virtualHosts."${cfg.localDomain}" = {
279 root = lib.mkForce "${cfg.package}/share/castopod/public";
282 try_files $uri $uri/ /index.php?$args;
283 index index.php index.html;
284 client_max_body_size ${cfg.maxUploadSize};
287 locations."^~ /${cfg.settings."media.root"}/" = {
288 root = cfg.settings."media.storage";
290 add_header Access-Control-Allow-Origin "*";
296 locations."~ \.php$" = {
298 SERVER_NAME = "$host";
301 fastcgi_intercept_errors on;
302 fastcgi_index index.php;
303 fastcgi_pass unix:${fpm.socket};
305 fastcgi_read_timeout 3600;
306 fastcgi_send_timeout 3600;
312 users.users.${user} = lib.mapAttrs (_: lib.mkDefault) {
313 description = "Castopod user";
315 group = config.services.nginx.group;