···
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 ${cfg.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";
39
+
options.services = {
41
+
enable = mkEnableOption (lib.mdDoc "a Pixelfed instance");
42
+
package = mkPackageOptionMD pkgs "pixelfed" { };
43
+
phpPackage = mkPackageOptionMD pkgs "php81" { };
47
+
default = "pixelfed";
48
+
description = lib.mdDoc ''
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.
61
+
default = "pixelfed";
62
+
description = lib.mdDoc ''
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.
75
+
description = lib.mdDoc ''
76
+
FQDN for the Pixelfed instance.
80
+
secretFile = mkOption {
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.
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.
97
+
type = types.nullOr (types.submodule
98
+
(import ../web-servers/nginx/vhost-options.nix {
102
+
example = lib.literalExpression ''
105
+
"pics.''${config.networking.domain}"
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
116
+
If this is set to null (the default), no nginx virtualHost will be configured.
120
+
redis.createLocally = mkEnableOption
121
+
(lib.mdDoc "a local Redis database using UNIX socket authentication")
127
+
createLocally = mkEnableOption
128
+
(lib.mdDoc "a local database using UNIX socket authentication") // {
131
+
automaticMigrations = mkEnableOption
132
+
(lib.mdDoc "automatic migrations for database schema and data") // {
137
+
type = types.enum [ "mysql" "pgsql" ];
140
+
description = lib.mdDoc ''
141
+
Database engine to use.
142
+
Note that PGSQL is not well supported: https://github.com/pixelfed/pixelfed/issues/2727
148
+
default = "pixelfed";
149
+
description = lib.mdDoc "Database name.";
153
+
maxUploadSize = mkOption {
156
+
description = lib.mdDoc ''
157
+
Max upload size with units.
161
+
poolConfig = mkOption {
162
+
type = with types; attrsOf (oneOf [ int str bool ]);
165
+
description = lib.mdDoc ''
166
+
Options for Pixelfed's PHP-FPM pool.
170
+
dataDir = mkOption {
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.
179
+
runtimeDir = mkOption {
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.
188
+
schedulerInterval = mkOption {
191
+
description = lib.mdDoc "How often the Pixelfed cron task should run";
196
+
config = mkIf cfg.enable {
197
+
users.users.pixelfed = mkIf (cfg.user == "pixelfed") {
198
+
isSystemUser = true;
200
+
extraGroups = lib.optional cfg.redis.createLocally "redis-pixelfed";
202
+
users.groups.pixelfed = mkIf (cfg.group == "pixelfed") { };
204
+
services.redis.servers.pixelfed.enable = lib.mkIf cfg.redis.createLocally true;
205
+
services.pixelfed.settings = mkMerge [
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";
226
+
OAUTH_ENABLED = mkDefault true;
227
+
# https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L351
228
+
EXP_EMC = mkDefault true;
230
+
LOG_CHANNEL = mkDefault "stderr";
231
+
# TODO: find out the correct syntax?
232
+
# TRUST_PROXIES = mkDefault "127.0.0.1/8, ::1/128";
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
+
# Suppport 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;
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.
255
+
environment.systemPackages = [ pixelfed-manage ];
258
+
mkIf (cfg.database.createLocally && cfg.database.type == "mysql") {
259
+
enable = mkDefault true;
260
+
package = mkDefault pkgs.mariadb;
261
+
ensureDatabases = [ cfg.database.name ];
264
+
ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
268
+
services.postgresql =
269
+
mkIf (cfg.database.createLocally && cfg.database.type == "pgsql") {
270
+
enable = mkDefault true;
271
+
ensureDatabases = [ cfg.database.name ];
274
+
ensurePermissions = { };
278
+
# Make each individual option overridable with lib.mkDefault.
279
+
services.pixelfed.poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) {
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";
291
+
services.phpfpm.pools.pixelfed = {
292
+
inherit user group;
293
+
inherit phpPackage;
296
+
post_max_size = ${toString cfg.maxUploadSize}
297
+
upload_max_filesize = ${toString cfg.maxUploadSize}
298
+
max_execution_time = 600;
302
+
"listen.owner" = user;
303
+
"listen.group" = group;
304
+
"listen.mode" = "0660";
305
+
"catch_workers_output" = "yes";
306
+
} // cfg.poolConfig;
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;
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;
329
+
ExecStart = "${pixelfed-manage}/bin/pixelfed-manage horizon";
331
+
lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
334
+
Restart = "on-failure";
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" ];
345
+
OnBootSec = cfg.schedulerInterval;
346
+
OnUnitActiveSec = cfg.schedulerInterval;
350
+
systemd.services.pixelfed-cron = {
351
+
description = "Pixelfed periodic tasks";
352
+
# Ensure image optimizations programs are available.
353
+
path = extraPrograms;
356
+
ExecStart = "${pixelfed-manage}/bin/pixelfed-manage schedule:run";
359
+
StateDirectory = cfg.dataDir;
363
+
systemd.services.pixelfed-data-setup = {
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;
376
+
lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
377
+
LoadCredential = "env-secrets:${cfg.secretFile}";
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
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
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
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.
403
+
# FIXME: require write access to public/ — should be done as part of install — pixelfed-manage horizon:publish
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/*
410
+
# Perform the first migration.
411
+
[[ ! -f ${cfg.dataDir}/.initial-migration ]] && pixelfed-manage migrate --force && touch ${cfg.dataDir}/.initial-migration
413
+
${lib.optionalString cfg.database.automaticMigrations ''
414
+
# Force migrate the database.
415
+
pixelfed-manage migrate --force
418
+
# Import location data
419
+
pixelfed-manage import:cities
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
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
431
+
pixelfed-manage route:cache
432
+
pixelfed-manage view:cache
433
+
pixelfed-manage config:cache
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} - -"
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) {
447
+
virtualHosts."${cfg.domain}" = mkMerge [
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;
455
+
locations."/robots.txt".extraConfig = ''
456
+
access_log off; log_not_found off;
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;
463
+
locations."~ /\\.(?!well-known).*".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};