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