at 24.11-pre 17 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 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"; 38in { 39 options.services = { 40 pixelfed = { 41 enable = mkEnableOption "a Pixelfed instance"; 42 package = mkPackageOption pkgs "pixelfed" { }; 43 phpPackage = mkPackageOption pkgs "php81" { }; 44 45 user = mkOption { 46 type = types.str; 47 default = "pixelfed"; 48 description = '' 49 User account under which pixelfed runs. 50 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 }; 58 59 group = mkOption { 60 type = types.str; 61 default = "pixelfed"; 62 description = '' 63 Group account under which pixelfed runs. 64 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 }; 72 73 domain = mkOption { 74 type = types.str; 75 description = '' 76 FQDN for the Pixelfed instance. 77 ''; 78 }; 79 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 }; 87 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 }; 95 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 }; 119 120 redis.createLocally = mkEnableOption "a local Redis database using UNIX socket authentication" 121 // { 122 default = true; 123 }; 124 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 }; 132 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 }; 142 143 name = mkOption { 144 type = types.str; 145 default = "pixelfed"; 146 description = "Database name."; 147 }; 148 }; 149 150 maxUploadSize = mkOption { 151 type = types.str; 152 default = "8M"; 153 description = '' 154 Max upload size with units. 155 ''; 156 }; 157 158 poolConfig = mkOption { 159 type = with types; attrsOf (oneOf [ int str bool ]); 160 default = { }; 161 162 description = '' 163 Options for Pixelfed's PHP-FPM pool. 164 ''; 165 }; 166 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 }; 175 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 }; 184 185 schedulerInterval = mkOption { 186 type = types.str; 187 default = "1d"; 188 description = "How often the Pixelfed cron task should run"; 189 }; 190 }; 191 }; 192 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") { }; 200 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 ]; 251 252 environment.systemPackages = [ pixelfed-manage ]; 253 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 }; 264 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 }; 273 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 }; 286 287 services.phpfpm.pools.pixelfed = { 288 inherit user group; 289 inherit phpPackage; 290 291 phpOptions = '' 292 post_max_size = ${toString cfg.maxUploadSize} 293 upload_max_filesize = ${toString cfg.maxUploadSize} 294 max_execution_time = 600; 295 ''; 296 297 settings = { 298 "listen.owner" = user; 299 "listen.group" = group; 300 "listen.mode" = "0660"; 301 "catch_workers_output" = "yes"; 302 } // cfg.poolConfig; 303 }; 304 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; 312 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; 322 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 }; 333 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" ]; 339 340 timerConfig = { 341 OnBootSec = cfg.schedulerInterval; 342 OnUnitActiveSec = cfg.schedulerInterval; 343 }; 344 }; 345 346 systemd.services.pixelfed-cron = { 347 description = "Pixelfed periodic tasks"; 348 # Ensure image optimizations programs are available. 349 path = extraPrograms; 350 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 }; 359 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; 367 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 }; 377 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/* 384 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 390 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 396 397 chmod g+x ${cfg.dataDir}/storage ${cfg.dataDir}/storage/app 398 chmod -R g+rX ${cfg.dataDir}/storage/app/public 399 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 403 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. 407 408 # Install Horizon 409 # FIXME: require write access to public/  should be done as part of install pixelfed-manage horizon:publish 410 411 # Perform the first migration. 412 [[ ! -f ${cfg.dataDir}/.initial-migration ]] && pixelfed-manage migrate --force && touch ${cfg.dataDir}/.initial-migration 413 414 ${lib.optionalString cfg.database.automaticMigrations '' 415 # Force migrate the database. 416 pixelfed-manage migrate --force 417 ''} 418 419 # Import location data 420 pixelfed-manage import:cities 421 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 ''} 426 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 ''} 431 432 pixelfed-manage route:cache 433 pixelfed-manage view:cache 434 pixelfed-manage config:cache 435 ''; 436 }; 437 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 ]; 443 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 }; 479}