at master 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 dbUnit = 57 { 58 "pgsql" = "postgresql.target"; 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 } 350 // cfg.poolConfig; 351 }; 352 353 systemd.services.phpfpm-pixelfed.after = [ "pixelfed-data-setup.service" ]; 354 systemd.services.phpfpm-pixelfed.requires = [ 355 "pixelfed-horizon.service" 356 "pixelfed-data-setup.service" 357 ] 358 ++ lib.optional cfg.database.createLocally dbUnit 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 ] 372 ++ (lib.optional cfg.database.createLocally dbUnit) 373 ++ (lib.optional cfg.redis.createLocally redisService); 374 wantedBy = [ "multi-user.target" ]; 375 # Ensure image optimizations programs are available. 376 path = extraPrograms; 377 378 serviceConfig = { 379 Type = "simple"; 380 ExecStart = "${pixelfed-manage}/bin/pixelfed-manage horizon"; 381 StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed"; 382 User = user; 383 Group = group; 384 Restart = "on-failure"; 385 }; 386 }; 387 388 systemd.timers.pixelfed-cron = { 389 description = "Pixelfed periodic tasks timer"; 390 after = [ "pixelfed-data-setup.service" ]; 391 requires = [ "phpfpm-pixelfed.service" ]; 392 wantedBy = [ "timers.target" ]; 393 394 timerConfig = { 395 OnBootSec = cfg.schedulerInterval; 396 OnUnitActiveSec = cfg.schedulerInterval; 397 }; 398 }; 399 400 systemd.services.pixelfed-cron = { 401 description = "Pixelfed periodic tasks"; 402 # Ensure image optimizations programs are available. 403 path = extraPrograms; 404 405 serviceConfig = { 406 ExecStart = "${pixelfed-manage}/bin/pixelfed-manage schedule:run"; 407 User = user; 408 Group = group; 409 StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed"; 410 }; 411 }; 412 413 systemd.services.pixelfed-data-setup = { 414 description = "Pixelfed setup: migrations, environment file update, cache reload, data changes"; 415 wantedBy = [ "multi-user.target" ]; 416 after = lib.optional cfg.database.createLocally dbUnit; 417 requires = lib.optional cfg.database.createLocally dbUnit; 418 path = 419 with pkgs; 420 [ 421 bash 422 pixelfed-manage 423 rsync 424 ] 425 ++ extraPrograms; 426 427 serviceConfig = { 428 Type = "oneshot"; 429 User = user; 430 Group = group; 431 StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed"; 432 LoadCredential = "env-secrets:${cfg.secretFile}"; 433 UMask = "077"; 434 }; 435 436 script = '' 437 # Before running any PHP program, cleanup the code cache. 438 # It's necessary if you upgrade the application otherwise you might 439 # try to import non-existent modules. 440 rm -f ${cfg.runtimeDir}/app.php 441 rm -rf ${cfg.runtimeDir}/cache/* 442 443 # Concatenate non-secret .env and secret .env 444 rm -f ${cfg.dataDir}/.env 445 cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env 446 echo -e '\n' >> ${cfg.dataDir}/.env 447 cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env 448 449 # Link the static storage (package provided) to the runtime storage 450 # Necessary for cities.json and static images. 451 mkdir -p ${cfg.dataDir}/storage 452 rsync -av --no-perms ${pixelfed}/storage-static/ ${cfg.dataDir}/storage 453 chmod -R +w ${cfg.dataDir}/storage 454 455 chmod g+x ${cfg.dataDir}/storage ${cfg.dataDir}/storage/app 456 chmod -R g+rX ${cfg.dataDir}/storage/app/public 457 458 # Link the app.php in the runtime folder. 459 # We cannot link the cache folder only because bootstrap folder needs to be writeable. 460 ln -sf ${pixelfed}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php 461 462 # https://laravel.com/docs/10.x/filesystem#the-public-disk 463 # Creating the public/storage storage/app/public link 464 # is unnecessary as it's part of the installPhase of pixelfed. 465 466 # Install Horizon 467 # FIXME: require write access to public/ should be done as part of install pixelfed-manage horizon:publish 468 469 # Perform the first migration. 470 [[ ! -f ${cfg.dataDir}/.initial-migration ]] && pixelfed-manage migrate --force && touch ${cfg.dataDir}/.initial-migration 471 472 ${lib.optionalString cfg.database.automaticMigrations '' 473 # Force migrate the database. 474 pixelfed-manage migrate --force 475 ''} 476 477 # Import location data 478 pixelfed-manage import:cities 479 480 ${lib.optionalString cfg.settings.ACTIVITY_PUB '' 481 # ActivityPub federation bookkeeping 482 [[ ! -f ${cfg.dataDir}/.instance-actor-created ]] && pixelfed-manage instance:actor && touch ${cfg.dataDir}/.instance-actor-created 483 ''} 484 485 ${lib.optionalString cfg.settings.OAUTH_ENABLED '' 486 # Generate Passport encryption keys 487 [[ ! -f ${cfg.dataDir}/.passport-keys-generated ]] && pixelfed-manage passport:keys && touch ${cfg.dataDir}/.passport-keys-generated 488 ''} 489 490 pixelfed-manage route:cache 491 pixelfed-manage view:cache 492 pixelfed-manage config:cache 493 ''; 494 }; 495 496 systemd.tmpfiles.rules = [ 497 # Cache must live across multiple systemd units runtimes. 498 "d ${cfg.runtimeDir}/ 0700 ${user} ${group} - -" 499 "d ${cfg.runtimeDir}/cache 0700 ${user} ${group} - -" 500 ]; 501 502 # Enable NGINX to access our phpfpm-socket. 503 users.users."${config.services.nginx.user}".extraGroups = [ cfg.group ]; 504 services.nginx = mkIf (cfg.nginx != null) { 505 enable = true; 506 virtualHosts."${cfg.domain}" = mkMerge [ 507 cfg.nginx 508 { 509 root = lib.mkForce "${pixelfed}/public/"; 510 locations."/".tryFiles = "$uri $uri/ /index.php?$query_string"; 511 locations."/favicon.ico".extraConfig = '' 512 access_log off; log_not_found off; 513 ''; 514 locations."/robots.txt".extraConfig = '' 515 access_log off; log_not_found off; 516 ''; 517 locations."~ \\.php$".extraConfig = '' 518 fastcgi_split_path_info ^(.+\.php)(/.+)$; 519 fastcgi_pass unix:${config.services.phpfpm.pools.pixelfed.socket}; 520 fastcgi_index index.php; 521 ''; 522 locations."~ /\\.(?!well-known).*".extraConfig = '' 523 deny all; 524 ''; 525 extraConfig = '' 526 add_header X-Frame-Options "SAMEORIGIN"; 527 add_header X-XSS-Protection "1; mode=block"; 528 add_header X-Content-Type-Options "nosniff"; 529 index index.html index.htm index.php; 530 error_page 404 /index.php; 531 client_max_body_size ${toString cfg.maxUploadSize}; 532 ''; 533 } 534 ]; 535 }; 536 }; 537}