at 24.11-pre 11 kB view raw
1{ config, lib, pkgs, ... }: 2let 3 cfg = config.services.castopod; 4 fpm = config.services.phpfpm.pools.castopod; 5 6 user = "castopod"; 7 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); 17in 18{ 19 meta.doc = ./castopod.md; 20 meta.maintainers = with lib.maintainers; [ alexoundos ]; 21 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). 70 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. 98 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). 134 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 }; 141 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; 151 152 "media.baseURL" = baseURL; 153 "media.root" = "media"; 154 "media.storage" = cfg.dataDir; 155 156 "admin.gateway" = "admin"; 157 "auth.gateway" = "auth"; 158 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_"; 163 164 "cache.handler" = "file"; 165 }; 166 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 }; 185 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} 198 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 202 203 if [ ! -f ${cfg.dataDir}/salt ]; then 204 openssl rand -base64 33 > ${cfg.dataDir}/salt 205 fi 206 207 cat <<'EOF' > ${envFile} 208 ${lib.generators.toKeyValue { } cfg.settings} 209 EOF 210 211 echo "analytics.salt=$(cat ${cfg.dataDir}/salt)" >> ${envFile} 212 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 ''} 218 219 ${lib.optionalString (cfg.environmentFile != null) '' 220 cat "$CREDENTIALS_DIRECTORY/envfile" >> ${envFile} 221 ''} 222 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 }; 239 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 }; 257 258 systemd.timers.castopod-scheduled = { 259 wantedBy = [ "timers.target" ]; 260 timerConfig = { 261 OnCalendar = "*-*-* *:*:00"; 262 Unit = "castopod-scheduled.service"; 263 }; 264 }; 265 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 }; 275 276 services.nginx = lib.mkIf cfg.configureNginx { 277 enable = true; 278 virtualHosts."${cfg.localDomain}" = { 279 root = lib.mkForce "${cfg.package}/share/castopod/public"; 280 281 extraConfig = '' 282 try_files $uri $uri/ /index.php?$args; 283 index index.php index.html; 284 client_max_body_size ${cfg.maxUploadSize}; 285 ''; 286 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 }; 295 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 }; 311 312 users.users.${user} = lib.mapAttrs (_: lib.mkDefault) { 313 description = "Castopod user"; 314 isSystemUser = true; 315 group = config.services.nginx.group; 316 }; 317 }; 318}