vuls: init at 0.27.0 (#348530)
[NixPkgs.git] / nixos / modules / services / web-apps / pixelfed.nix
blob62db479da3d4190ebd75d509fb1361f811319ff5
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.pixelfed;
7   user = cfg.user;
8   group = cfg.group;
9   pixelfed = cfg.package.override { inherit (cfg) dataDir runtimeDir; };
10   # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L185-L190
11   extraPrograms = with pkgs; [ jpegoptim optipng pngquant gifsicle ffmpeg ];
12   # Ensure PHP extensions: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L135-L147
13   phpPackage = cfg.phpPackage.buildEnv {
14     extensions = { enabled, all }:
15       enabled
16       ++ (with all; [ bcmath ctype curl mbstring gd intl zip redis imagick ]);
17   };
18   configFile =
19     pkgs.writeText "pixelfed-env" (lib.generators.toKeyValue { } cfg.settings);
20   # Management script
21   pixelfed-manage = pkgs.writeShellScriptBin "pixelfed-manage" ''
22     cd ${pixelfed}
23     sudo=exec
24     if [[ "$USER" != ${user} ]]; then
25       sudo='exec /run/wrappers/bin/sudo -u ${user}'
26     fi
27     $sudo ${phpPackage}/bin/php artisan "$@"
28   '';
29   dbSocket = {
30     "pgsql" = "/run/postgresql";
31     "mysql" = "/run/mysqld/mysqld.sock";
32   }.${cfg.database.type};
33   dbService = {
34     "pgsql" = "postgresql.service";
35     "mysql" = "mysql.service";
36   }.${cfg.database.type};
37   redisService = "redis-pixelfed.service";
38 in {
39   options.services = {
40     pixelfed = {
41       enable = mkEnableOption "a Pixelfed instance";
42       package = mkPackageOption pkgs "pixelfed" { };
43       phpPackage = mkPackageOption pkgs "php82" { };
45       user = mkOption {
46         type = types.str;
47         default = "pixelfed";
48         description = ''
49           User account under which pixelfed runs.
51           ::: {.note}
52           If left as the default value this user will automatically be created
53           on system activation, otherwise you are responsible for
54           ensuring the user exists before the pixelfed application starts.
55           :::
56         '';
57       };
59       group = mkOption {
60         type = types.str;
61         default = "pixelfed";
62         description = ''
63           Group account under which pixelfed runs.
65           ::: {.note}
66           If left as the default value this group will automatically be created
67           on system activation, otherwise you are responsible for
68           ensuring the group exists before the pixelfed application starts.
69           :::
70         '';
71       };
73       domain = mkOption {
74         type = types.str;
75         description = ''
76           FQDN for the Pixelfed instance.
77         '';
78       };
80       secretFile = mkOption {
81         type = types.path;
82         description = ''
83           A secret file to be sourced for the .env settings.
84           Place `APP_KEY` and other settings that should not end up in the Nix store here.
85         '';
86       };
88       settings = mkOption {
89         type = with types; (attrsOf (oneOf [ bool int str ]));
90         description = ''
91           .env settings for Pixelfed.
92           Secrets should use `secretFile` option instead.
93         '';
94       };
96       nginx = mkOption {
97         type = types.nullOr (types.submodule
98           (import ../web-servers/nginx/vhost-options.nix {
99             inherit config lib;
100           }));
101         default = null;
102         example = lib.literalExpression ''
103           {
104             serverAliases = [
105               "pics.''${config.networking.domain}"
106             ];
107             enableACME = true;
108             forceHttps = true;
109           }
110         '';
111         description = ''
112           With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr.
113           Set to {} if you do not need any customization to the virtual host.
114           If enabled, then by default, the {option}`serverName` is
115           `''${domain}`,
116           If this is set to null (the default), no nginx virtualHost will be configured.
117         '';
118       };
120       redis.createLocally = mkEnableOption "a local Redis database using UNIX socket authentication"
121         // {
122           default = true;
123         };
125       database = {
126         createLocally = mkEnableOption "a local database using UNIX socket authentication" // {
127             default = true;
128           };
129         automaticMigrations = mkEnableOption "automatic migrations for database schema and data" // {
130             default = true;
131           };
133         type = mkOption {
134           type = types.enum [ "mysql" "pgsql" ];
135           example = "pgsql";
136           default = "mysql";
137           description = ''
138             Database engine to use.
139             Note that PGSQL is not well supported: https://github.com/pixelfed/pixelfed/issues/2727
140           '';
141         };
143         name = mkOption {
144           type = types.str;
145           default = "pixelfed";
146           description = "Database name.";
147         };
148       };
150       maxUploadSize = mkOption {
151         type = types.str;
152         default = "8M";
153         description = ''
154           Max upload size with units.
155         '';
156       };
158       poolConfig = mkOption {
159         type = with types; attrsOf (oneOf [ int str bool ]);
160         default = { };
162         description = ''
163           Options for Pixelfed's PHP-FPM pool.
164         '';
165       };
167       dataDir = mkOption {
168         type = types.str;
169         default = "/var/lib/pixelfed";
170         description = ''
171           State directory of the `pixelfed` user which holds
172           the application's state and data.
173         '';
174       };
176       runtimeDir = mkOption {
177         type = types.str;
178         default = "/run/pixelfed";
179         description = ''
180           Ruutime directory of the `pixelfed` user which holds
181           the application's caches and temporary files.
182         '';
183       };
185       schedulerInterval = mkOption {
186         type = types.str;
187         default = "1d";
188         description = "How often the Pixelfed cron task should run";
189       };
190     };
191   };
193   config = mkIf cfg.enable {
194     users.users.pixelfed = mkIf (cfg.user == "pixelfed") {
195       isSystemUser = true;
196       group = cfg.group;
197       extraGroups = lib.optional cfg.redis.createLocally "redis-pixelfed";
198     };
199     users.groups.pixelfed = mkIf (cfg.group == "pixelfed") { };
201     services.redis.servers.pixelfed.enable = lib.mkIf cfg.redis.createLocally true;
202     services.pixelfed.settings = mkMerge [
203       ({
204         APP_ENV = mkDefault "production";
205         APP_DEBUG = mkDefault false;
206         # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L312-L316
207         APP_URL = mkDefault "https://${cfg.domain}";
208         ADMIN_DOMAIN = mkDefault cfg.domain;
209         APP_DOMAIN = mkDefault cfg.domain;
210         SESSION_DOMAIN = mkDefault cfg.domain;
211         SESSION_SECURE_COOKIE = mkDefault true;
212         OPEN_REGISTRATION = mkDefault false;
213         # ActivityPub: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L360-L364
214         ACTIVITY_PUB = mkDefault true;
215         AP_REMOTE_FOLLOW = mkDefault true;
216         AP_INBOX = mkDefault true;
217         AP_OUTBOX = mkDefault true;
218         AP_SHAREDINBOX = mkDefault true;
219         # Image optimization: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L367-L404
220         PF_OPTIMIZE_IMAGES = mkDefault true;
221         IMAGE_DRIVER = mkDefault "imagick";
222         # Mobile APIs
223         OAUTH_ENABLED = mkDefault true;
224         # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L351
225         EXP_EMC = mkDefault true;
226         # Defer to systemd
227         LOG_CHANNEL = mkDefault "stderr";
228         # TODO: find out the correct syntax?
229         # TRUST_PROXIES = mkDefault "127.0.0.1/8, ::1/128";
230       })
231       (mkIf (cfg.redis.createLocally) {
232         BROADCAST_DRIVER = mkDefault "redis";
233         CACHE_DRIVER = mkDefault "redis";
234         QUEUE_DRIVER = mkDefault "redis";
235         SESSION_DRIVER = mkDefault "redis";
236         WEBSOCKET_REPLICATION_MODE = mkDefault "redis";
237         # Support phpredis and predis configuration-style.
238         REDIS_SCHEME = "unix";
239         REDIS_HOST = config.services.redis.servers.pixelfed.unixSocket;
240         REDIS_PATH = config.services.redis.servers.pixelfed.unixSocket;
241       })
242       (mkIf (cfg.database.createLocally) {
243         DB_CONNECTION = cfg.database.type;
244         DB_SOCKET = dbSocket;
245         DB_DATABASE = cfg.database.name;
246         DB_USERNAME = user;
247         # No TCP/IP connection.
248         DB_PORT = 0;
249       })
250     ];
252     environment.systemPackages = [ pixelfed-manage ];
254     services.mysql =
255       mkIf (cfg.database.createLocally && cfg.database.type == "mysql") {
256         enable = mkDefault true;
257         package = mkDefault pkgs.mariadb;
258         ensureDatabases = [ cfg.database.name ];
259         ensureUsers = [{
260           name = user;
261           ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
262         }];
263       };
265     services.postgresql =
266       mkIf (cfg.database.createLocally && cfg.database.type == "pgsql") {
267         enable = mkDefault true;
268         ensureDatabases = [ cfg.database.name ];
269         ensureUsers = [{
270           name = user;
271         }];
272       };
274     # Make each individual option overridable with lib.mkDefault.
275     services.pixelfed.poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) {
276       "pm" = "dynamic";
277       "php_admin_value[error_log]" = "stderr";
278       "php_admin_flag[log_errors]" = true;
279       "catch_workers_output" = true;
280       "pm.max_children" = "32";
281       "pm.start_servers" = "2";
282       "pm.min_spare_servers" = "2";
283       "pm.max_spare_servers" = "4";
284       "pm.max_requests" = "500";
285     };
287     services.phpfpm.pools.pixelfed = {
288       inherit user group;
289       inherit phpPackage;
291       phpOptions = ''
292         post_max_size = ${toString cfg.maxUploadSize}
293         upload_max_filesize = ${toString cfg.maxUploadSize}
294         max_execution_time = 600;
295       '';
297       settings = {
298         "listen.owner" = user;
299         "listen.group" = group;
300         "listen.mode" = "0660";
301         "catch_workers_output" = "yes";
302       } // cfg.poolConfig;
303     };
305     systemd.services.phpfpm-pixelfed.after = [ "pixelfed-data-setup.service" ];
306     systemd.services.phpfpm-pixelfed.requires =
307       [ "pixelfed-horizon.service" "pixelfed-data-setup.service" ]
308       ++ lib.optional cfg.database.createLocally dbService
309       ++ lib.optional cfg.redis.createLocally redisService;
310     # Ensure image optimizations programs are available.
311     systemd.services.phpfpm-pixelfed.path = extraPrograms;
313     systemd.services.pixelfed-horizon = {
314       description = "Pixelfed task queueing via Laravel Horizon framework";
315       after = [ "network.target" "pixelfed-data-setup.service" ];
316       requires = [ "pixelfed-data-setup.service" ]
317         ++ (lib.optional cfg.database.createLocally dbService)
318         ++ (lib.optional cfg.redis.createLocally redisService);
319       wantedBy = [ "multi-user.target" ];
320       # Ensure image optimizations programs are available.
321       path = extraPrograms;
323       serviceConfig = {
324         Type = "simple";
325         ExecStart = "${pixelfed-manage}/bin/pixelfed-manage horizon";
326         StateDirectory =
327           lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
328         User = user;
329         Group = group;
330         Restart = "on-failure";
331       };
332     };
334     systemd.timers.pixelfed-cron = {
335       description = "Pixelfed periodic tasks timer";
336       after = [ "pixelfed-data-setup.service" ];
337       requires = [ "phpfpm-pixelfed.service" ];
338       wantedBy = [ "timers.target" ];
340       timerConfig = {
341         OnBootSec = cfg.schedulerInterval;
342         OnUnitActiveSec = cfg.schedulerInterval;
343       };
344     };
346     systemd.services.pixelfed-cron = {
347       description = "Pixelfed periodic tasks";
348       # Ensure image optimizations programs are available.
349       path = extraPrograms;
351       serviceConfig = {
352         ExecStart = "${pixelfed-manage}/bin/pixelfed-manage schedule:run";
353         User = user;
354         Group = group;
355         StateDirectory =
356           lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
357       };
358     };
360     systemd.services.pixelfed-data-setup = {
361       description =
362         "Pixelfed setup: migrations, environment file update, cache reload, data changes";
363       wantedBy = [ "multi-user.target" ];
364       after = lib.optional cfg.database.createLocally dbService;
365       requires = lib.optional cfg.database.createLocally dbService;
366       path = with pkgs; [ bash pixelfed-manage rsync ] ++ extraPrograms;
368       serviceConfig = {
369         Type = "oneshot";
370         User = user;
371         Group = group;
372         StateDirectory =
373           lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
374         LoadCredential = "env-secrets:${cfg.secretFile}";
375         UMask = "077";
376       };
378       script = ''
379         # Before running any PHP program, cleanup the code cache.
380         # It's necessary if you upgrade the application otherwise you might
381         # try to import non-existent modules.
382         rm -f ${cfg.runtimeDir}/app.php
383         rm -rf ${cfg.runtimeDir}/cache/*
385         # Concatenate non-secret .env and secret .env
386         rm -f ${cfg.dataDir}/.env
387         cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env
388         echo -e '\n' >> ${cfg.dataDir}/.env
389         cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env
391         # Link the static storage (package provided) to the runtime storage
392         # Necessary for cities.json and static images.
393         mkdir -p ${cfg.dataDir}/storage
394         rsync -av --no-perms ${pixelfed}/storage-static/ ${cfg.dataDir}/storage
395         chmod -R +w ${cfg.dataDir}/storage
397         chmod g+x ${cfg.dataDir}/storage ${cfg.dataDir}/storage/app
398         chmod -R g+rX ${cfg.dataDir}/storage/app/public
400         # Link the app.php in the runtime folder.
401         # We cannot link the cache folder only because bootstrap folder needs to be writeable.
402         ln -sf ${pixelfed}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php
404         # https://laravel.com/docs/10.x/filesystem#the-public-disk
405         # Creating the public/storage → storage/app/public link
406         # is unnecessary as it's part of the installPhase of pixelfed.
408         # Install Horizon
409         # FIXME: require write access to public/ — should be done as part of install — pixelfed-manage horizon:publish
411         # Perform the first migration.
412         [[ ! -f ${cfg.dataDir}/.initial-migration ]] && pixelfed-manage migrate --force && touch ${cfg.dataDir}/.initial-migration
414         ${lib.optionalString cfg.database.automaticMigrations ''
415           # Force migrate the database.
416           pixelfed-manage migrate --force
417         ''}
419         # Import location data
420         pixelfed-manage import:cities
422         ${lib.optionalString cfg.settings.ACTIVITY_PUB ''
423           # ActivityPub federation bookkeeping
424           [[ ! -f ${cfg.dataDir}/.instance-actor-created ]] && pixelfed-manage instance:actor && touch ${cfg.dataDir}/.instance-actor-created
425         ''}
427         ${lib.optionalString cfg.settings.OAUTH_ENABLED ''
428           # Generate Passport encryption keys
429           [[ ! -f ${cfg.dataDir}/.passport-keys-generated ]] && pixelfed-manage passport:keys && touch ${cfg.dataDir}/.passport-keys-generated
430         ''}
432         pixelfed-manage route:cache
433         pixelfed-manage view:cache
434         pixelfed-manage config:cache
435       '';
436     };
438     systemd.tmpfiles.rules = [
439       # Cache must live across multiple systemd units runtimes.
440       "d ${cfg.runtimeDir}/                         0700 ${user} ${group} - -"
441       "d ${cfg.runtimeDir}/cache                    0700 ${user} ${group} - -"
442     ];
444     # Enable NGINX to access our phpfpm-socket.
445     users.users."${config.services.nginx.user}".extraGroups = [ cfg.group ];
446     services.nginx = mkIf (cfg.nginx != null) {
447       enable = true;
448       virtualHosts."${cfg.domain}" = mkMerge [
449         cfg.nginx
450         {
451           root = lib.mkForce "${pixelfed}/public/";
452           locations."/".tryFiles = "$uri $uri/ /index.php?$query_string";
453           locations."/favicon.ico".extraConfig = ''
454             access_log off; log_not_found off;
455           '';
456           locations."/robots.txt".extraConfig = ''
457             access_log off; log_not_found off;
458           '';
459           locations."~ \\.php$".extraConfig = ''
460             fastcgi_split_path_info ^(.+\.php)(/.+)$;
461             fastcgi_pass unix:${config.services.phpfpm.pools.pixelfed.socket};
462             fastcgi_index index.php;
463           '';
464           locations."~ /\\.(?!well-known).*".extraConfig = ''
465             deny all;
466           '';
467           extraConfig = ''
468             add_header X-Frame-Options "SAMEORIGIN";
469             add_header X-XSS-Protection "1; mode=block";
470             add_header X-Content-Type-Options "nosniff";
471             index index.html index.htm index.php;
472             error_page 404 /index.php;
473             client_max_body_size ${toString cfg.maxUploadSize};
474           '';
475         }
476       ];
477     };
478   };