at 23.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 ${cfg.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 (lib.mdDoc "a Pixelfed instance"); 42 package = mkPackageOptionMD pkgs "pixelfed" { }; 43 phpPackage = mkPackageOptionMD pkgs "php81" { }; 44 45 user = mkOption { 46 type = types.str; 47 default = "pixelfed"; 48 description = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 76 FQDN for the Pixelfed instance. 77 ''; 78 }; 79 80 secretFile = mkOption { 81 type = types.path; 82 description = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 121 (lib.mdDoc "a local Redis database using UNIX socket authentication") 122 // { 123 default = true; 124 }; 125 126 database = { 127 createLocally = mkEnableOption 128 (lib.mdDoc "a local database using UNIX socket authentication") // { 129 default = true; 130 }; 131 automaticMigrations = mkEnableOption 132 (lib.mdDoc "automatic migrations for database schema and data") // { 133 default = true; 134 }; 135 136 type = mkOption { 137 type = types.enum [ "mysql" "pgsql" ]; 138 example = "pgsql"; 139 default = "mysql"; 140 description = lib.mdDoc '' 141 Database engine to use. 142 Note that PGSQL is not well supported: https://github.com/pixelfed/pixelfed/issues/2727 143 ''; 144 }; 145 146 name = mkOption { 147 type = types.str; 148 default = "pixelfed"; 149 description = lib.mdDoc "Database name."; 150 }; 151 }; 152 153 maxUploadSize = mkOption { 154 type = types.str; 155 default = "8M"; 156 description = lib.mdDoc '' 157 Max upload size with units. 158 ''; 159 }; 160 161 poolConfig = mkOption { 162 type = with types; attrsOf (oneOf [ int str bool ]); 163 default = { }; 164 165 description = lib.mdDoc '' 166 Options for Pixelfed's PHP-FPM pool. 167 ''; 168 }; 169 170 dataDir = mkOption { 171 type = types.str; 172 default = "/var/lib/pixelfed"; 173 description = lib.mdDoc '' 174 State directory of the `pixelfed` user which holds 175 the application's state and data. 176 ''; 177 }; 178 179 runtimeDir = mkOption { 180 type = types.str; 181 default = "/run/pixelfed"; 182 description = lib.mdDoc '' 183 Ruutime directory of the `pixelfed` user which holds 184 the application's caches and temporary files. 185 ''; 186 }; 187 188 schedulerInterval = mkOption { 189 type = types.str; 190 default = "1d"; 191 description = lib.mdDoc "How often the Pixelfed cron task should run"; 192 }; 193 }; 194 }; 195 196 config = mkIf cfg.enable { 197 users.users.pixelfed = mkIf (cfg.user == "pixelfed") { 198 isSystemUser = true; 199 group = cfg.group; 200 extraGroups = lib.optional cfg.redis.createLocally "redis-pixelfed"; 201 }; 202 users.groups.pixelfed = mkIf (cfg.group == "pixelfed") { }; 203 204 services.redis.servers.pixelfed.enable = lib.mkIf cfg.redis.createLocally true; 205 services.pixelfed.settings = mkMerge [ 206 ({ 207 APP_ENV = mkDefault "production"; 208 APP_DEBUG = mkDefault false; 209 # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L312-L316 210 APP_URL = mkDefault "https://${cfg.domain}"; 211 ADMIN_DOMAIN = mkDefault cfg.domain; 212 APP_DOMAIN = mkDefault cfg.domain; 213 SESSION_DOMAIN = mkDefault cfg.domain; 214 SESSION_SECURE_COOKIE = mkDefault true; 215 OPEN_REGISTRATION = mkDefault false; 216 # ActivityPub: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L360-L364 217 ACTIVITY_PUB = mkDefault true; 218 AP_REMOTE_FOLLOW = mkDefault true; 219 AP_INBOX = mkDefault true; 220 AP_OUTBOX = mkDefault true; 221 AP_SHAREDINBOX = mkDefault true; 222 # Image optimization: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L367-L404 223 PF_OPTIMIZE_IMAGES = mkDefault true; 224 IMAGE_DRIVER = mkDefault "imagick"; 225 # Mobile APIs 226 OAUTH_ENABLED = mkDefault true; 227 # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L351 228 EXP_EMC = mkDefault true; 229 # Defer to systemd 230 LOG_CHANNEL = mkDefault "stderr"; 231 # TODO: find out the correct syntax? 232 # TRUST_PROXIES = mkDefault "127.0.0.1/8, ::1/128"; 233 }) 234 (mkIf (cfg.redis.createLocally) { 235 BROADCAST_DRIVER = mkDefault "redis"; 236 CACHE_DRIVER = mkDefault "redis"; 237 QUEUE_DRIVER = mkDefault "redis"; 238 SESSION_DRIVER = mkDefault "redis"; 239 WEBSOCKET_REPLICATION_MODE = mkDefault "redis"; 240 # Support phpredis and predis configuration-style. 241 REDIS_SCHEME = "unix"; 242 REDIS_HOST = config.services.redis.servers.pixelfed.unixSocket; 243 REDIS_PATH = config.services.redis.servers.pixelfed.unixSocket; 244 }) 245 (mkIf (cfg.database.createLocally) { 246 DB_CONNECTION = cfg.database.type; 247 DB_SOCKET = dbSocket; 248 DB_DATABASE = cfg.database.name; 249 DB_USERNAME = user; 250 # No TCP/IP connection. 251 DB_PORT = 0; 252 }) 253 ]; 254 255 environment.systemPackages = [ pixelfed-manage ]; 256 257 services.mysql = 258 mkIf (cfg.database.createLocally && cfg.database.type == "mysql") { 259 enable = mkDefault true; 260 package = mkDefault pkgs.mariadb; 261 ensureDatabases = [ cfg.database.name ]; 262 ensureUsers = [{ 263 name = user; 264 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; 265 }]; 266 }; 267 268 services.postgresql = 269 mkIf (cfg.database.createLocally && cfg.database.type == "pgsql") { 270 enable = mkDefault true; 271 ensureDatabases = [ cfg.database.name ]; 272 ensureUsers = [{ 273 name = user; 274 ensurePermissions = { }; 275 }]; 276 }; 277 278 # Make each individual option overridable with lib.mkDefault. 279 services.pixelfed.poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) { 280 "pm" = "dynamic"; 281 "php_admin_value[error_log]" = "stderr"; 282 "php_admin_flag[log_errors]" = true; 283 "catch_workers_output" = true; 284 "pm.max_children" = "32"; 285 "pm.start_servers" = "2"; 286 "pm.min_spare_servers" = "2"; 287 "pm.max_spare_servers" = "4"; 288 "pm.max_requests" = "500"; 289 }; 290 291 services.phpfpm.pools.pixelfed = { 292 inherit user group; 293 inherit phpPackage; 294 295 phpOptions = '' 296 post_max_size = ${toString cfg.maxUploadSize} 297 upload_max_filesize = ${toString cfg.maxUploadSize} 298 max_execution_time = 600; 299 ''; 300 301 settings = { 302 "listen.owner" = user; 303 "listen.group" = group; 304 "listen.mode" = "0660"; 305 "catch_workers_output" = "yes"; 306 } // cfg.poolConfig; 307 }; 308 309 systemd.services.phpfpm-pixelfed.after = [ "pixelfed-data-setup.service" ]; 310 systemd.services.phpfpm-pixelfed.requires = 311 [ "pixelfed-horizon.service" "pixelfed-data-setup.service" ] 312 ++ lib.optional cfg.database.createLocally dbService 313 ++ lib.optional cfg.redis.createLocally redisService; 314 # Ensure image optimizations programs are available. 315 systemd.services.phpfpm-pixelfed.path = extraPrograms; 316 317 systemd.services.pixelfed-horizon = { 318 description = "Pixelfed task queueing via Laravel Horizon framework"; 319 after = [ "network.target" "pixelfed-data-setup.service" ]; 320 requires = [ "pixelfed-data-setup.service" ] 321 ++ (lib.optional cfg.database.createLocally dbService) 322 ++ (lib.optional cfg.redis.createLocally redisService); 323 wantedBy = [ "multi-user.target" ]; 324 # Ensure image optimizations programs are available. 325 path = extraPrograms; 326 327 serviceConfig = { 328 Type = "simple"; 329 ExecStart = "${pixelfed-manage}/bin/pixelfed-manage horizon"; 330 StateDirectory = 331 lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed"; 332 User = user; 333 Group = group; 334 Restart = "on-failure"; 335 }; 336 }; 337 338 systemd.timers.pixelfed-cron = { 339 description = "Pixelfed periodic tasks timer"; 340 after = [ "pixelfed-data-setup.service" ]; 341 requires = [ "phpfpm-pixelfed.service" ]; 342 wantedBy = [ "timers.target" ]; 343 344 timerConfig = { 345 OnBootSec = cfg.schedulerInterval; 346 OnUnitActiveSec = cfg.schedulerInterval; 347 }; 348 }; 349 350 systemd.services.pixelfed-cron = { 351 description = "Pixelfed periodic tasks"; 352 # Ensure image optimizations programs are available. 353 path = extraPrograms; 354 355 serviceConfig = { 356 ExecStart = "${pixelfed-manage}/bin/pixelfed-manage schedule:run"; 357 User = user; 358 Group = group; 359 StateDirectory = cfg.dataDir; 360 }; 361 }; 362 363 systemd.services.pixelfed-data-setup = { 364 description = 365 "Pixelfed setup: migrations, environment file update, cache reload, data changes"; 366 wantedBy = [ "multi-user.target" ]; 367 after = lib.optional cfg.database.createLocally dbService; 368 requires = lib.optional cfg.database.createLocally dbService; 369 path = with pkgs; [ bash pixelfed-manage rsync ] ++ extraPrograms; 370 371 serviceConfig = { 372 Type = "oneshot"; 373 User = user; 374 Group = group; 375 StateDirectory = 376 lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed"; 377 LoadCredential = "env-secrets:${cfg.secretFile}"; 378 UMask = "077"; 379 }; 380 381 script = '' 382 # Concatenate non-secret .env and secret .env 383 rm -f ${cfg.dataDir}/.env 384 cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env 385 echo -e '\n' >> ${cfg.dataDir}/.env 386 cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env 387 388 # Link the static storage (package provided) to the runtime storage 389 # Necessary for cities.json and static images. 390 mkdir -p ${cfg.dataDir}/storage 391 rsync -av --no-perms ${pixelfed}/storage-static/ ${cfg.dataDir}/storage 392 chmod -R +w ${cfg.dataDir}/storage 393 394 # Link the app.php in the runtime folder. 395 # We cannot link the cache folder only because bootstrap folder needs to be writeable. 396 ln -sf ${pixelfed}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php 397 398 # https://laravel.com/docs/10.x/filesystem#the-public-disk 399 # Creating the public/storage storage/app/public link 400 # is unnecessary as it's part of the installPhase of pixelfed. 401 402 # Install Horizon 403 # FIXME: require write access to public/  should be done as part of install pixelfed-manage horizon:publish 404 405 # Before running any PHP program, cleanup the bootstrap. 406 # It's necessary if you upgrade the application otherwise you might 407 # try to import non-existent modules. 408 rm -rf ${cfg.runtimeDir}/bootstrap/* 409 410 # Perform the first migration. 411 [[ ! -f ${cfg.dataDir}/.initial-migration ]] && pixelfed-manage migrate --force && touch ${cfg.dataDir}/.initial-migration 412 413 ${lib.optionalString cfg.database.automaticMigrations '' 414 # Force migrate the database. 415 pixelfed-manage migrate --force 416 ''} 417 418 # Import location data 419 pixelfed-manage import:cities 420 421 ${lib.optionalString cfg.settings.ACTIVITY_PUB '' 422 # ActivityPub federation bookkeeping 423 [[ ! -f ${cfg.dataDir}/.instance-actor-created ]] && pixelfed-manage instance:actor && touch ${cfg.dataDir}/.instance-actor-created 424 ''} 425 426 ${lib.optionalString cfg.settings.OAUTH_ENABLED '' 427 # Generate Passport encryption keys 428 [[ ! -f ${cfg.dataDir}/.passport-keys-generated ]] && pixelfed-manage passport:keys && touch ${cfg.dataDir}/.passport-keys-generated 429 ''} 430 431 pixelfed-manage route:cache 432 pixelfed-manage view:cache 433 pixelfed-manage config:cache 434 ''; 435 }; 436 437 systemd.tmpfiles.rules = [ 438 # Cache must live across multiple systemd units runtimes. 439 "d ${cfg.runtimeDir}/ 0700 ${user} ${group} - -" 440 "d ${cfg.runtimeDir}/cache 0700 ${user} ${group} - -" 441 ]; 442 443 # Enable NGINX to access our phpfpm-socket. 444 users.users."${config.services.nginx.group}".extraGroups = [ cfg.group ]; 445 services.nginx = mkIf (cfg.nginx != null) { 446 enable = true; 447 virtualHosts."${cfg.domain}" = mkMerge [ 448 cfg.nginx 449 { 450 root = lib.mkForce "${pixelfed}/public/"; 451 locations."/".tryFiles = "$uri $uri/ /index.php?query_string"; 452 locations."/favicon.ico".extraConfig = '' 453 access_log off; log_not_found off; 454 ''; 455 locations."/robots.txt".extraConfig = '' 456 access_log off; log_not_found off; 457 ''; 458 locations."~ \\.php$".extraConfig = '' 459 fastcgi_split_path_info ^(.+\.php)(/.+)$; 460 fastcgi_pass unix:${config.services.phpfpm.pools.pixelfed.socket}; 461 fastcgi_index index.php; 462 ''; 463 locations."~ /\\.(?!well-known).*".extraConfig = '' 464 deny all; 465 ''; 466 extraConfig = '' 467 add_header X-Frame-Options "SAMEORIGIN"; 468 add_header X-XSS-Protection "1; mode=block"; 469 add_header X-Content-Type-Options "nosniff"; 470 index index.html index.htm index.php; 471 error_page 404 /index.php; 472 client_max_body_size ${toString cfg.maxUploadSize}; 473 ''; 474 } 475 ]; 476 }; 477 }; 478}