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