1 { config, lib, pkgs, ... }:
6 cfg = config.services.pixelfed;
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 }:
16 ++ (with all; [ bcmath ctype curl mbstring gd intl zip redis imagick ]);
19 pkgs.writeText "pixelfed-env" (lib.generators.toKeyValue { } cfg.settings);
21 pixelfed-manage = pkgs.writeShellScriptBin "pixelfed-manage" ''
24 if [[ "$USER" != ${user} ]]; then
25 sudo='exec /run/wrappers/bin/sudo -u ${user}'
27 $sudo ${phpPackage}/bin/php artisan "$@"
30 "pgsql" = "/run/postgresql";
31 "mysql" = "/run/mysqld/mysqld.sock";
32 }.${cfg.database.type};
34 "pgsql" = "postgresql.service";
35 "mysql" = "mysql.service";
36 }.${cfg.database.type};
37 redisService = "redis-pixelfed.service";
41 enable = mkEnableOption "a Pixelfed instance";
42 package = mkPackageOption pkgs "pixelfed" { };
43 phpPackage = mkPackageOption pkgs "php82" { };
49 User account under which pixelfed runs.
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.
63 Group account under which pixelfed runs.
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.
76 FQDN for the Pixelfed instance.
80 secretFile = mkOption {
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.
89 type = with types; (attrsOf (oneOf [ bool int str ]));
91 .env settings for Pixelfed.
92 Secrets should use `secretFile` option instead.
97 type = types.nullOr (types.submodule
98 (import ../web-servers/nginx/vhost-options.nix {
102 example = lib.literalExpression ''
105 "pics.''${config.networking.domain}"
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
116 If this is set to null (the default), no nginx virtualHost will be configured.
120 redis.createLocally = mkEnableOption "a local Redis database using UNIX socket authentication"
126 createLocally = mkEnableOption "a local database using UNIX socket authentication" // {
129 automaticMigrations = mkEnableOption "automatic migrations for database schema and data" // {
134 type = types.enum [ "mysql" "pgsql" ];
138 Database engine to use.
139 Note that PGSQL is not well supported: https://github.com/pixelfed/pixelfed/issues/2727
145 default = "pixelfed";
146 description = "Database name.";
150 maxUploadSize = mkOption {
154 Max upload size with units.
158 poolConfig = mkOption {
159 type = with types; attrsOf (oneOf [ int str bool ]);
163 Options for Pixelfed's PHP-FPM pool.
169 default = "/var/lib/pixelfed";
171 State directory of the `pixelfed` user which holds
172 the application's state and data.
176 runtimeDir = mkOption {
178 default = "/run/pixelfed";
180 Ruutime directory of the `pixelfed` user which holds
181 the application's caches and temporary files.
185 schedulerInterval = mkOption {
188 description = "How often the Pixelfed cron task should run";
193 config = mkIf cfg.enable {
194 users.users.pixelfed = mkIf (cfg.user == "pixelfed") {
197 extraGroups = lib.optional cfg.redis.createLocally "redis-pixelfed";
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 [
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";
223 OAUTH_ENABLED = mkDefault true;
224 # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L351
225 EXP_EMC = mkDefault true;
227 LOG_CHANNEL = mkDefault "stderr";
228 # TODO: find out the correct syntax?
229 # TRUST_PROXIES = mkDefault "127.0.0.1/8, ::1/128";
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;
242 (mkIf (cfg.database.createLocally) {
243 DB_CONNECTION = cfg.database.type;
244 DB_SOCKET = dbSocket;
245 DB_DATABASE = cfg.database.name;
247 # No TCP/IP connection.
252 environment.systemPackages = [ pixelfed-manage ];
255 mkIf (cfg.database.createLocally && cfg.database.type == "mysql") {
256 enable = mkDefault true;
257 package = mkDefault pkgs.mariadb;
258 ensureDatabases = [ cfg.database.name ];
261 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
265 services.postgresql =
266 mkIf (cfg.database.createLocally && cfg.database.type == "pgsql") {
267 enable = mkDefault true;
268 ensureDatabases = [ cfg.database.name ];
274 # Make each individual option overridable with lib.mkDefault.
275 services.pixelfed.poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) {
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";
287 services.phpfpm.pools.pixelfed = {
292 post_max_size = ${toString cfg.maxUploadSize}
293 upload_max_filesize = ${toString cfg.maxUploadSize}
294 max_execution_time = 600;
298 "listen.owner" = user;
299 "listen.group" = group;
300 "listen.mode" = "0660";
301 "catch_workers_output" = "yes";
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;
325 ExecStart = "${pixelfed-manage}/bin/pixelfed-manage horizon";
327 lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
330 Restart = "on-failure";
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" ];
341 OnBootSec = cfg.schedulerInterval;
342 OnUnitActiveSec = cfg.schedulerInterval;
346 systemd.services.pixelfed-cron = {
347 description = "Pixelfed periodic tasks";
348 # Ensure image optimizations programs are available.
349 path = extraPrograms;
352 ExecStart = "${pixelfed-manage}/bin/pixelfed-manage schedule:run";
356 lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
360 systemd.services.pixelfed-data-setup = {
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;
373 lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
374 LoadCredential = "env-secrets:${cfg.secretFile}";
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.
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
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
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
432 pixelfed-manage route:cache
433 pixelfed-manage view:cache
434 pixelfed-manage config:cache
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} - -"
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) {
448 virtualHosts."${cfg.domain}" = mkMerge [
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;
456 locations."/robots.txt".extraConfig = ''
457 access_log off; log_not_found off;
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;
464 locations."~ /\\.(?!well-known).*".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};