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