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