nixpkgs/nixos/modules/services/web-apps/castopod.nix

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

319 lines
10 KiB
Nix
Raw Normal View History

2023-04-07 12:59:08 +00:00
{ config, lib, pkgs, ... }:
let
cfg = config.services.castopod;
fpm = config.services.phpfpm.pools.castopod;
user = "castopod";
# https://docs.castopod.org/getting-started/install.html#requirements
phpPackage = pkgs.php.withExtensions ({ enabled, all }: with all; [
intl
curl
mbstring
gd
exif
mysqlnd
] ++ enabled);
in
{
meta.doc = ./castopod.md;
meta.maintainers = with lib.maintainers; [ alexoundos misuzu ];
options.services = {
castopod = {
enable = lib.mkEnableOption (lib.mdDoc "Castopod");
package = lib.mkOption {
type = lib.types.package;
default = pkgs.castopod;
defaultText = lib.literalMD "pkgs.castopod";
description = lib.mdDoc "Which Castopod package to use.";
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/castopod";
description = lib.mdDoc ''
The path where castopod stores all data. This path must be in sync
with the castopod package (where it is hardcoded during the build in
accordance with its own `dataDir` argument).
'';
};
2023-04-07 12:59:08 +00:00
database = {
createLocally = lib.mkOption {
type = lib.types.bool;
default = true;
description = lib.mdDoc ''
Create the database and database user locally.
'';
};
hostname = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = lib.mdDoc "Database hostname.";
};
name = lib.mkOption {
type = lib.types.str;
default = "castopod";
description = lib.mdDoc "Database name.";
};
user = lib.mkOption {
type = lib.types.str;
default = user;
description = lib.mdDoc "Database user.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/keys/castopod-dbpassword";
description = lib.mdDoc ''
A file containing the password corresponding to
[](#opt-services.castopod.database.user).
2024-02-23 21:27:24 +00:00
This file is loaded using systemd LoadCredentials.
2023-04-07 12:59:08 +00:00
'';
};
};
settings = lib.mkOption {
type = with lib.types; attrsOf (oneOf [ str int bool ]);
default = { };
example = {
"email.protocol" = "smtp";
"email.SMTPHost" = "localhost";
"email.SMTPUser" = "myuser";
"email.fromEmail" = "castopod@example.com";
};
description = lib.mdDoc ''
Environment variables used for Castopod.
See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example)
for available environment variables.
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/keys/castopod-env";
description = lib.mdDoc ''
Environment file to inject e.g. secrets into the configuration.
See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example)
for available environment variables.
2024-02-23 21:27:24 +00:00
This file is loaded using systemd LoadCredentials.
2023-04-07 12:59:08 +00:00
'';
};
configureNginx = lib.mkOption {
type = lib.types.bool;
default = true;
description = lib.mdDoc "Configure nginx as a reverse proxy for CastoPod.";
};
localDomain = lib.mkOption {
type = lib.types.str;
example = "castopod.example.org";
description = lib.mdDoc "The domain serving your CastoPod instance.";
};
poolSettings = lib.mkOption {
type = with lib.types; attrsOf (oneOf [ str int bool ]);
default = {
"pm" = "dynamic";
"pm.max_children" = "32";
"pm.start_servers" = "2";
"pm.min_spare_servers" = "2";
"pm.max_spare_servers" = "4";
"pm.max_requests" = "500";
};
description = lib.mdDoc ''
Options for Castopod's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
'';
};
maxUploadSize = lib.mkOption {
type = lib.types.str;
default = "512M";
description = lib.mdDoc ''
Maximum supported size for a file upload in. Maximum HTTP body
size is set to this value for nginx and PHP (because castopod doesn't
support chunked uploads yet:
https://code.castopod.org/adaures/castopod/-/issues/330).
Note, that practical upload size limit is smaller. For example, with
512 MiB setting - around 500 MiB is possible.
'';
};
2023-04-07 12:59:08 +00:00
};
};
config = lib.mkIf cfg.enable {
services.castopod.settings =
let
sslEnabled = with config.services.nginx.virtualHosts.${cfg.localDomain}; addSSL || forceSSL || onlySSL || enableACME || useACMEHost != null;
baseURL = "http${lib.optionalString sslEnabled "s"}://${cfg.localDomain}";
in
lib.mapAttrs (_: lib.mkDefault) {
2023-04-07 12:59:08 +00:00
"app.forceGlobalSecureRequests" = sslEnabled;
"app.baseURL" = baseURL;
"media.baseURL" = baseURL;
2023-04-07 12:59:08 +00:00
"media.root" = "media";
"media.storage" = cfg.dataDir;
2023-04-07 12:59:08 +00:00
"admin.gateway" = "admin";
"auth.gateway" = "auth";
"database.default.hostname" = cfg.database.hostname;
"database.default.database" = cfg.database.name;
"database.default.username" = cfg.database.user;
"database.default.DBPrefix" = "cp_";
"cache.handler" = "file";
};
services.phpfpm.pools.castopod = {
inherit user;
group = config.services.nginx.group;
inherit phpPackage;
2023-04-07 12:59:08 +00:00
phpOptions = ''
# https://code.castopod.org/adaures/castopod/-/blob/develop/docker/production/common/uploads.template.ini
2023-04-07 12:59:08 +00:00
file_uploads = On
memory_limit = 512M
upload_max_filesize = ${cfg.maxUploadSize}
post_max_size = ${cfg.maxUploadSize}
2023-04-07 12:59:08 +00:00
max_execution_time = 300
max_input_time = 300
'';
settings = {
"listen.owner" = config.services.nginx.user;
"listen.group" = config.services.nginx.group;
} // cfg.poolSettings;
};
systemd.services.castopod-setup = {
after = lib.optional config.services.mysql.enable "mysql.service";
requires = lib.optional config.services.mysql.enable "mysql.service";
wantedBy = [ "multi-user.target" ];
path = [ pkgs.openssl phpPackage ];
script =
let
envFile = "${cfg.dataDir}/.env";
2023-04-07 12:59:08 +00:00
media = "${cfg.settings."media.storage"}/${cfg.settings."media.root"}";
in
''
mkdir -p ${cfg.dataDir}/writable/{cache,logs,session,temp,uploads}
2023-04-07 12:59:08 +00:00
if [ ! -d ${lib.escapeShellArg media} ]; then
cp --no-preserve=mode,ownership -r ${cfg.package}/share/castopod/public/media ${lib.escapeShellArg media}
fi
if [ ! -f ${cfg.dataDir}/salt ]; then
openssl rand -base64 33 > ${cfg.dataDir}/salt
2023-04-07 12:59:08 +00:00
fi
cat <<'EOF' > ${envFile}
${lib.generators.toKeyValue { } cfg.settings}
EOF
echo "analytics.salt=$(cat ${cfg.dataDir}/salt)" >> ${envFile}
2023-04-07 12:59:08 +00:00
${if (cfg.database.passwordFile != null) then ''
2024-02-23 21:27:24 +00:00
echo "database.default.password=$(cat "$CREDENTIALS_DIRECTORY/dbpasswordfile)" >> ${envFile}
2023-04-07 12:59:08 +00:00
'' else ''
echo "database.default.password=" >> ${envFile}
''}
${lib.optionalString (cfg.environmentFile != null) ''
2024-02-23 21:27:24 +00:00
cat "$CREDENTIALS_DIRECTORY/envfile" >> ${envFile}
2023-04-07 12:59:08 +00:00
''}
php ${cfg.package}/share/castopod/spark castopod:database-update
2023-04-07 12:59:08 +00:00
'';
serviceConfig = {
StateDirectory = "castopod";
2024-02-23 21:27:24 +00:00
LoadCredential = lib.optional (cfg.environmentFile != null)
"envfile:${cfg.environmentFile}"
++ (lib.optional (cfg.database.passwordFile != null)
"dbpasswordfile:${cfg.database.passwordFile}");
2023-04-07 12:59:08 +00:00
WorkingDirectory = "${cfg.package}/share/castopod";
Type = "oneshot";
RemainAfterExit = true;
User = user;
Group = config.services.nginx.group;
ReadWritePaths = cfg.dataDir;
2023-04-07 12:59:08 +00:00
};
};
systemd.services.castopod-scheduled = {
after = [ "castopod-setup.service" ];
wantedBy = [ "multi-user.target" ];
path = [ phpPackage ];
script = ''
php ${cfg.package}/share/castopod/spark tasks:run
2023-04-07 12:59:08 +00:00
'';
serviceConfig = {
StateDirectory = "castopod";
WorkingDirectory = "${cfg.package}/share/castopod";
Type = "oneshot";
User = user;
Group = config.services.nginx.group;
ReadWritePaths = cfg.dataDir;
LogLevelMax = "notice"; # otherwise periodic tasks flood the journal
2023-04-07 12:59:08 +00:00
};
};
systemd.timers.castopod-scheduled = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* *:*:00";
Unit = "castopod-scheduled.service";
};
};
services.mysql = lib.mkIf cfg.database.createLocally {
enable = true;
package = lib.mkDefault pkgs.mariadb;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [{
name = cfg.database.user;
ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
}];
};
services.nginx = lib.mkIf cfg.configureNginx {
enable = true;
virtualHosts."${cfg.localDomain}" = {
root = lib.mkForce "${cfg.package}/share/castopod/public";
extraConfig = ''
try_files $uri $uri/ /index.php?$args;
index index.php index.html;
client_max_body_size ${cfg.maxUploadSize};
2023-04-07 12:59:08 +00:00
'';
locations."^~ /${cfg.settings."media.root"}/" = {
root = cfg.settings."media.storage";
extraConfig = ''
add_header Access-Control-Allow-Origin "*";
expires max;
access_log off;
'';
};
locations."~ \.php$" = {
fastcgiParams = {
SERVER_NAME = "$host";
};
extraConfig = ''
fastcgi_intercept_errors on;
fastcgi_index index.php;
fastcgi_pass unix:${fpm.socket};
try_files $uri =404;
fastcgi_read_timeout 3600;
fastcgi_send_timeout 3600;
'';
};
};
};
users.users.${user} = lib.mapAttrs (_: lib.mkDefault) {
2023-04-07 12:59:08 +00:00
description = "Castopod user";
isSystemUser = true;
group = config.services.nginx.group;
};
};
}