mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-01-04 03:53:56 +00:00
d99dd867fb
The "bootstrap" directory only exists within the app's package. The cached code is placed at the root of the runtime directory instead.
484 lines
17 KiB
Nix
484 lines
17 KiB
Nix
{ config, lib, pkgs, ... }:
|
||
|
||
with lib;
|
||
|
||
let
|
||
cfg = config.services.pixelfed;
|
||
user = cfg.user;
|
||
group = cfg.group;
|
||
pixelfed = cfg.package.override { inherit (cfg) dataDir runtimeDir; };
|
||
# https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L185-L190
|
||
extraPrograms = with pkgs; [ jpegoptim optipng pngquant gifsicle ffmpeg ];
|
||
# Ensure PHP extensions: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L135-L147
|
||
phpPackage = cfg.phpPackage.buildEnv {
|
||
extensions = { enabled, all }:
|
||
enabled
|
||
++ (with all; [ bcmath ctype curl mbstring gd intl zip redis imagick ]);
|
||
};
|
||
configFile =
|
||
pkgs.writeText "pixelfed-env" (lib.generators.toKeyValue { } cfg.settings);
|
||
# Management script
|
||
pixelfed-manage = pkgs.writeShellScriptBin "pixelfed-manage" ''
|
||
cd ${pixelfed}
|
||
sudo=exec
|
||
if [[ "$USER" != ${user} ]]; then
|
||
sudo='exec /run/wrappers/bin/sudo -u ${user}'
|
||
fi
|
||
$sudo ${cfg.phpPackage}/bin/php artisan "$@"
|
||
'';
|
||
dbSocket = {
|
||
"pgsql" = "/run/postgresql";
|
||
"mysql" = "/run/mysqld/mysqld.sock";
|
||
}.${cfg.database.type};
|
||
dbService = {
|
||
"pgsql" = "postgresql.service";
|
||
"mysql" = "mysql.service";
|
||
}.${cfg.database.type};
|
||
redisService = "redis-pixelfed.service";
|
||
in {
|
||
options.services = {
|
||
pixelfed = {
|
||
enable = mkEnableOption (lib.mdDoc "a Pixelfed instance");
|
||
package = mkPackageOptionMD pkgs "pixelfed" { };
|
||
phpPackage = mkPackageOptionMD pkgs "php81" { };
|
||
|
||
user = mkOption {
|
||
type = types.str;
|
||
default = "pixelfed";
|
||
description = lib.mdDoc ''
|
||
User account under which pixelfed runs.
|
||
|
||
::: {.note}
|
||
If left as the default value this user will automatically be created
|
||
on system activation, otherwise you are responsible for
|
||
ensuring the user exists before the pixelfed application starts.
|
||
:::
|
||
'';
|
||
};
|
||
|
||
group = mkOption {
|
||
type = types.str;
|
||
default = "pixelfed";
|
||
description = lib.mdDoc ''
|
||
Group account under which pixelfed runs.
|
||
|
||
::: {.note}
|
||
If left as the default value this group will automatically be created
|
||
on system activation, otherwise you are responsible for
|
||
ensuring the group exists before the pixelfed application starts.
|
||
:::
|
||
'';
|
||
};
|
||
|
||
domain = mkOption {
|
||
type = types.str;
|
||
description = lib.mdDoc ''
|
||
FQDN for the Pixelfed instance.
|
||
'';
|
||
};
|
||
|
||
secretFile = mkOption {
|
||
type = types.path;
|
||
description = lib.mdDoc ''
|
||
A secret file to be sourced for the .env settings.
|
||
Place `APP_KEY` and other settings that should not end up in the Nix store here.
|
||
'';
|
||
};
|
||
|
||
settings = mkOption {
|
||
type = with types; (attrsOf (oneOf [ bool int str ]));
|
||
description = lib.mdDoc ''
|
||
.env settings for Pixelfed.
|
||
Secrets should use `secretFile` option instead.
|
||
'';
|
||
};
|
||
|
||
nginx = mkOption {
|
||
type = types.nullOr (types.submodule
|
||
(import ../web-servers/nginx/vhost-options.nix {
|
||
inherit config lib;
|
||
}));
|
||
default = null;
|
||
example = lib.literalExpression ''
|
||
{
|
||
serverAliases = [
|
||
"pics.''${config.networking.domain}"
|
||
];
|
||
enableACME = true;
|
||
forceHttps = true;
|
||
}
|
||
'';
|
||
description = lib.mdDoc ''
|
||
With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr.
|
||
Set to {} if you do not need any customization to the virtual host.
|
||
If enabled, then by default, the {option}`serverName` is
|
||
`''${domain}`,
|
||
If this is set to null (the default), no nginx virtualHost will be configured.
|
||
'';
|
||
};
|
||
|
||
redis.createLocally = mkEnableOption
|
||
(lib.mdDoc "a local Redis database using UNIX socket authentication")
|
||
// {
|
||
default = true;
|
||
};
|
||
|
||
database = {
|
||
createLocally = mkEnableOption
|
||
(lib.mdDoc "a local database using UNIX socket authentication") // {
|
||
default = true;
|
||
};
|
||
automaticMigrations = mkEnableOption
|
||
(lib.mdDoc "automatic migrations for database schema and data") // {
|
||
default = true;
|
||
};
|
||
|
||
type = mkOption {
|
||
type = types.enum [ "mysql" "pgsql" ];
|
||
example = "pgsql";
|
||
default = "mysql";
|
||
description = lib.mdDoc ''
|
||
Database engine to use.
|
||
Note that PGSQL is not well supported: https://github.com/pixelfed/pixelfed/issues/2727
|
||
'';
|
||
};
|
||
|
||
name = mkOption {
|
||
type = types.str;
|
||
default = "pixelfed";
|
||
description = lib.mdDoc "Database name.";
|
||
};
|
||
};
|
||
|
||
maxUploadSize = mkOption {
|
||
type = types.str;
|
||
default = "8M";
|
||
description = lib.mdDoc ''
|
||
Max upload size with units.
|
||
'';
|
||
};
|
||
|
||
poolConfig = mkOption {
|
||
type = with types; attrsOf (oneOf [ int str bool ]);
|
||
default = { };
|
||
|
||
description = lib.mdDoc ''
|
||
Options for Pixelfed's PHP-FPM pool.
|
||
'';
|
||
};
|
||
|
||
dataDir = mkOption {
|
||
type = types.str;
|
||
default = "/var/lib/pixelfed";
|
||
description = lib.mdDoc ''
|
||
State directory of the `pixelfed` user which holds
|
||
the application's state and data.
|
||
'';
|
||
};
|
||
|
||
runtimeDir = mkOption {
|
||
type = types.str;
|
||
default = "/run/pixelfed";
|
||
description = lib.mdDoc ''
|
||
Ruutime directory of the `pixelfed` user which holds
|
||
the application's caches and temporary files.
|
||
'';
|
||
};
|
||
|
||
schedulerInterval = mkOption {
|
||
type = types.str;
|
||
default = "1d";
|
||
description = lib.mdDoc "How often the Pixelfed cron task should run";
|
||
};
|
||
};
|
||
};
|
||
|
||
config = mkIf cfg.enable {
|
||
users.users.pixelfed = mkIf (cfg.user == "pixelfed") {
|
||
isSystemUser = true;
|
||
group = cfg.group;
|
||
extraGroups = lib.optional cfg.redis.createLocally "redis-pixelfed";
|
||
};
|
||
users.groups.pixelfed = mkIf (cfg.group == "pixelfed") { };
|
||
|
||
services.redis.servers.pixelfed.enable = lib.mkIf cfg.redis.createLocally true;
|
||
services.pixelfed.settings = mkMerge [
|
||
({
|
||
APP_ENV = mkDefault "production";
|
||
APP_DEBUG = mkDefault false;
|
||
# https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L312-L316
|
||
APP_URL = mkDefault "https://${cfg.domain}";
|
||
ADMIN_DOMAIN = mkDefault cfg.domain;
|
||
APP_DOMAIN = mkDefault cfg.domain;
|
||
SESSION_DOMAIN = mkDefault cfg.domain;
|
||
SESSION_SECURE_COOKIE = mkDefault true;
|
||
OPEN_REGISTRATION = mkDefault false;
|
||
# ActivityPub: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L360-L364
|
||
ACTIVITY_PUB = mkDefault true;
|
||
AP_REMOTE_FOLLOW = mkDefault true;
|
||
AP_INBOX = mkDefault true;
|
||
AP_OUTBOX = mkDefault true;
|
||
AP_SHAREDINBOX = mkDefault true;
|
||
# Image optimization: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L367-L404
|
||
PF_OPTIMIZE_IMAGES = mkDefault true;
|
||
IMAGE_DRIVER = mkDefault "imagick";
|
||
# Mobile APIs
|
||
OAUTH_ENABLED = mkDefault true;
|
||
# https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L351
|
||
EXP_EMC = mkDefault true;
|
||
# Defer to systemd
|
||
LOG_CHANNEL = mkDefault "stderr";
|
||
# TODO: find out the correct syntax?
|
||
# TRUST_PROXIES = mkDefault "127.0.0.1/8, ::1/128";
|
||
})
|
||
(mkIf (cfg.redis.createLocally) {
|
||
BROADCAST_DRIVER = mkDefault "redis";
|
||
CACHE_DRIVER = mkDefault "redis";
|
||
QUEUE_DRIVER = mkDefault "redis";
|
||
SESSION_DRIVER = mkDefault "redis";
|
||
WEBSOCKET_REPLICATION_MODE = mkDefault "redis";
|
||
# Support phpredis and predis configuration-style.
|
||
REDIS_SCHEME = "unix";
|
||
REDIS_HOST = config.services.redis.servers.pixelfed.unixSocket;
|
||
REDIS_PATH = config.services.redis.servers.pixelfed.unixSocket;
|
||
})
|
||
(mkIf (cfg.database.createLocally) {
|
||
DB_CONNECTION = cfg.database.type;
|
||
DB_SOCKET = dbSocket;
|
||
DB_DATABASE = cfg.database.name;
|
||
DB_USERNAME = user;
|
||
# No TCP/IP connection.
|
||
DB_PORT = 0;
|
||
})
|
||
];
|
||
|
||
environment.systemPackages = [ pixelfed-manage ];
|
||
|
||
services.mysql =
|
||
mkIf (cfg.database.createLocally && cfg.database.type == "mysql") {
|
||
enable = mkDefault true;
|
||
package = mkDefault pkgs.mariadb;
|
||
ensureDatabases = [ cfg.database.name ];
|
||
ensureUsers = [{
|
||
name = user;
|
||
ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
|
||
}];
|
||
};
|
||
|
||
services.postgresql =
|
||
mkIf (cfg.database.createLocally && cfg.database.type == "pgsql") {
|
||
enable = mkDefault true;
|
||
ensureDatabases = [ cfg.database.name ];
|
||
ensureUsers = [{
|
||
name = user;
|
||
ensurePermissions = { };
|
||
}];
|
||
};
|
||
|
||
# Make each individual option overridable with lib.mkDefault.
|
||
services.pixelfed.poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) {
|
||
"pm" = "dynamic";
|
||
"php_admin_value[error_log]" = "stderr";
|
||
"php_admin_flag[log_errors]" = true;
|
||
"catch_workers_output" = true;
|
||
"pm.max_children" = "32";
|
||
"pm.start_servers" = "2";
|
||
"pm.min_spare_servers" = "2";
|
||
"pm.max_spare_servers" = "4";
|
||
"pm.max_requests" = "500";
|
||
};
|
||
|
||
services.phpfpm.pools.pixelfed = {
|
||
inherit user group;
|
||
inherit phpPackage;
|
||
|
||
phpOptions = ''
|
||
post_max_size = ${toString cfg.maxUploadSize}
|
||
upload_max_filesize = ${toString cfg.maxUploadSize}
|
||
max_execution_time = 600;
|
||
'';
|
||
|
||
settings = {
|
||
"listen.owner" = user;
|
||
"listen.group" = group;
|
||
"listen.mode" = "0660";
|
||
"catch_workers_output" = "yes";
|
||
} // cfg.poolConfig;
|
||
};
|
||
|
||
systemd.services.phpfpm-pixelfed.after = [ "pixelfed-data-setup.service" ];
|
||
systemd.services.phpfpm-pixelfed.requires =
|
||
[ "pixelfed-horizon.service" "pixelfed-data-setup.service" ]
|
||
++ lib.optional cfg.database.createLocally dbService
|
||
++ lib.optional cfg.redis.createLocally redisService;
|
||
# Ensure image optimizations programs are available.
|
||
systemd.services.phpfpm-pixelfed.path = extraPrograms;
|
||
|
||
systemd.services.pixelfed-horizon = {
|
||
description = "Pixelfed task queueing via Laravel Horizon framework";
|
||
after = [ "network.target" "pixelfed-data-setup.service" ];
|
||
requires = [ "pixelfed-data-setup.service" ]
|
||
++ (lib.optional cfg.database.createLocally dbService)
|
||
++ (lib.optional cfg.redis.createLocally redisService);
|
||
wantedBy = [ "multi-user.target" ];
|
||
# Ensure image optimizations programs are available.
|
||
path = extraPrograms;
|
||
|
||
serviceConfig = {
|
||
Type = "simple";
|
||
ExecStart = "${pixelfed-manage}/bin/pixelfed-manage horizon";
|
||
StateDirectory =
|
||
lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
|
||
User = user;
|
||
Group = group;
|
||
Restart = "on-failure";
|
||
};
|
||
};
|
||
|
||
systemd.timers.pixelfed-cron = {
|
||
description = "Pixelfed periodic tasks timer";
|
||
after = [ "pixelfed-data-setup.service" ];
|
||
requires = [ "phpfpm-pixelfed.service" ];
|
||
wantedBy = [ "timers.target" ];
|
||
|
||
timerConfig = {
|
||
OnBootSec = cfg.schedulerInterval;
|
||
OnUnitActiveSec = cfg.schedulerInterval;
|
||
};
|
||
};
|
||
|
||
systemd.services.pixelfed-cron = {
|
||
description = "Pixelfed periodic tasks";
|
||
# Ensure image optimizations programs are available.
|
||
path = extraPrograms;
|
||
|
||
serviceConfig = {
|
||
ExecStart = "${pixelfed-manage}/bin/pixelfed-manage schedule:run";
|
||
User = user;
|
||
Group = group;
|
||
StateDirectory =
|
||
lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
|
||
};
|
||
};
|
||
|
||
systemd.services.pixelfed-data-setup = {
|
||
description =
|
||
"Pixelfed setup: migrations, environment file update, cache reload, data changes";
|
||
wantedBy = [ "multi-user.target" ];
|
||
after = lib.optional cfg.database.createLocally dbService;
|
||
requires = lib.optional cfg.database.createLocally dbService;
|
||
path = with pkgs; [ bash pixelfed-manage rsync ] ++ extraPrograms;
|
||
|
||
serviceConfig = {
|
||
Type = "oneshot";
|
||
User = user;
|
||
Group = group;
|
||
StateDirectory =
|
||
lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
|
||
LoadCredential = "env-secrets:${cfg.secretFile}";
|
||
UMask = "077";
|
||
};
|
||
|
||
script = ''
|
||
# Before running any PHP program, cleanup the code cache.
|
||
# It's necessary if you upgrade the application otherwise you might
|
||
# try to import non-existent modules.
|
||
rm -f ${cfg.runtimeDir}/app.php
|
||
rm -rf ${cfg.runtimeDir}/cache/*
|
||
|
||
# Concatenate non-secret .env and secret .env
|
||
rm -f ${cfg.dataDir}/.env
|
||
cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env
|
||
echo -e '\n' >> ${cfg.dataDir}/.env
|
||
cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env
|
||
|
||
# Link the static storage (package provided) to the runtime storage
|
||
# Necessary for cities.json and static images.
|
||
mkdir -p ${cfg.dataDir}/storage
|
||
rsync -av --no-perms ${pixelfed}/storage-static/ ${cfg.dataDir}/storage
|
||
chmod -R +w ${cfg.dataDir}/storage
|
||
|
||
chmod g+x ${cfg.dataDir}/storage ${cfg.dataDir}/storage/app
|
||
chmod -R g+rX ${cfg.dataDir}/storage/app/public
|
||
|
||
# Link the app.php in the runtime folder.
|
||
# We cannot link the cache folder only because bootstrap folder needs to be writeable.
|
||
ln -sf ${pixelfed}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php
|
||
|
||
# https://laravel.com/docs/10.x/filesystem#the-public-disk
|
||
# Creating the public/storage → storage/app/public link
|
||
# is unnecessary as it's part of the installPhase of pixelfed.
|
||
|
||
# Install Horizon
|
||
# FIXME: require write access to public/ — should be done as part of install — pixelfed-manage horizon:publish
|
||
|
||
# Perform the first migration.
|
||
[[ ! -f ${cfg.dataDir}/.initial-migration ]] && pixelfed-manage migrate --force && touch ${cfg.dataDir}/.initial-migration
|
||
|
||
${lib.optionalString cfg.database.automaticMigrations ''
|
||
# Force migrate the database.
|
||
pixelfed-manage migrate --force
|
||
''}
|
||
|
||
# Import location data
|
||
pixelfed-manage import:cities
|
||
|
||
${lib.optionalString cfg.settings.ACTIVITY_PUB ''
|
||
# ActivityPub federation bookkeeping
|
||
[[ ! -f ${cfg.dataDir}/.instance-actor-created ]] && pixelfed-manage instance:actor && touch ${cfg.dataDir}/.instance-actor-created
|
||
''}
|
||
|
||
${lib.optionalString cfg.settings.OAUTH_ENABLED ''
|
||
# Generate Passport encryption keys
|
||
[[ ! -f ${cfg.dataDir}/.passport-keys-generated ]] && pixelfed-manage passport:keys && touch ${cfg.dataDir}/.passport-keys-generated
|
||
''}
|
||
|
||
pixelfed-manage route:cache
|
||
pixelfed-manage view:cache
|
||
pixelfed-manage config:cache
|
||
'';
|
||
};
|
||
|
||
systemd.tmpfiles.rules = [
|
||
# Cache must live across multiple systemd units runtimes.
|
||
"d ${cfg.runtimeDir}/ 0700 ${user} ${group} - -"
|
||
"d ${cfg.runtimeDir}/cache 0700 ${user} ${group} - -"
|
||
];
|
||
|
||
# Enable NGINX to access our phpfpm-socket.
|
||
users.users."${config.services.nginx.user}".extraGroups = [ cfg.group ];
|
||
services.nginx = mkIf (cfg.nginx != null) {
|
||
enable = true;
|
||
virtualHosts."${cfg.domain}" = mkMerge [
|
||
cfg.nginx
|
||
{
|
||
root = lib.mkForce "${pixelfed}/public/";
|
||
locations."/".tryFiles = "$uri $uri/ /index.php?$query_string";
|
||
locations."/favicon.ico".extraConfig = ''
|
||
access_log off; log_not_found off;
|
||
'';
|
||
locations."/robots.txt".extraConfig = ''
|
||
access_log off; log_not_found off;
|
||
'';
|
||
locations."~ \\.php$".extraConfig = ''
|
||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||
fastcgi_pass unix:${config.services.phpfpm.pools.pixelfed.socket};
|
||
fastcgi_index index.php;
|
||
'';
|
||
locations."~ /\\.(?!well-known).*".extraConfig = ''
|
||
deny all;
|
||
'';
|
||
extraConfig = ''
|
||
add_header X-Frame-Options "SAMEORIGIN";
|
||
add_header X-XSS-Protection "1; mode=block";
|
||
add_header X-Content-Type-Options "nosniff";
|
||
index index.html index.htm index.php;
|
||
error_page 404 /index.php;
|
||
client_max_body_size ${toString cfg.maxUploadSize};
|
||
'';
|
||
}
|
||
];
|
||
};
|
||
};
|
||
}
|