mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-11-14 11:44:19 +01:00
This avoids restarting the postgresql server, when only ensureDatabases or ensureUsers have been changed. It will also allow to properly wait for recovery to finish later. To wait for "postgresql is ready" in other services, we now provide a postgresql.target. Resolves #400018 Co-authored-by: Marcel <me@m4rc3l.de>
537 lines
17 KiB
Nix
537 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 ${phpPackage}/bin/php artisan "$@"
|
|
'';
|
|
dbSocket =
|
|
{
|
|
"pgsql" = "/run/postgresql";
|
|
"mysql" = "/run/mysqld/mysqld.sock";
|
|
}
|
|
.${cfg.database.type};
|
|
dbUnit =
|
|
{
|
|
"pgsql" = "postgresql.target";
|
|
"mysql" = "mysql.service";
|
|
}
|
|
.${cfg.database.type};
|
|
redisService = "redis-pixelfed.service";
|
|
in
|
|
{
|
|
options.services = {
|
|
pixelfed = {
|
|
enable = mkEnableOption "a Pixelfed instance";
|
|
package = mkPackageOption pkgs "pixelfed" { };
|
|
phpPackage = mkPackageOption pkgs "php83" { };
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "pixelfed";
|
|
description = ''
|
|
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 = ''
|
|
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 = ''
|
|
FQDN for the Pixelfed instance.
|
|
'';
|
|
};
|
|
|
|
secretFile = mkOption {
|
|
type = types.path;
|
|
description = ''
|
|
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 = ''
|
|
.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 = ''
|
|
With this option, you can customize an nginx virtual host which already has sensible defaults for Pixelfed.
|
|
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 "a local Redis database using UNIX socket authentication" // {
|
|
default = true;
|
|
};
|
|
|
|
database = {
|
|
createLocally = mkEnableOption "a local database using UNIX socket authentication" // {
|
|
default = true;
|
|
};
|
|
automaticMigrations = mkEnableOption "automatic migrations for database schema and data" // {
|
|
default = true;
|
|
};
|
|
|
|
type = mkOption {
|
|
type = types.enum [
|
|
"mysql"
|
|
"pgsql"
|
|
];
|
|
example = "pgsql";
|
|
default = "mysql";
|
|
description = ''
|
|
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 = "Database name.";
|
|
};
|
|
};
|
|
|
|
maxUploadSize = mkOption {
|
|
type = types.str;
|
|
default = "8M";
|
|
description = ''
|
|
Max upload size with units.
|
|
'';
|
|
};
|
|
|
|
poolConfig = mkOption {
|
|
type =
|
|
with types;
|
|
attrsOf (oneOf [
|
|
int
|
|
str
|
|
bool
|
|
]);
|
|
default = { };
|
|
|
|
description = ''
|
|
Options for Pixelfed's PHP-FPM pool.
|
|
'';
|
|
};
|
|
|
|
dataDir = mkOption {
|
|
type = types.str;
|
|
default = "/var/lib/pixelfed";
|
|
description = ''
|
|
State directory of the `pixelfed` user which holds
|
|
the application's state and data.
|
|
'';
|
|
};
|
|
|
|
runtimeDir = mkOption {
|
|
type = types.str;
|
|
default = "/run/pixelfed";
|
|
description = ''
|
|
Ruutime directory of the `pixelfed` user which holds
|
|
the application's caches and temporary files.
|
|
'';
|
|
};
|
|
|
|
schedulerInterval = mkOption {
|
|
type = types.str;
|
|
default = "1d";
|
|
description = "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;
|
|
}
|
|
];
|
|
};
|
|
|
|
# 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 dbUnit
|
|
++ 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 dbUnit)
|
|
++ (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 dbUnit;
|
|
requires = lib.optional cfg.database.createLocally dbUnit;
|
|
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};
|
|
'';
|
|
}
|
|
];
|
|
};
|
|
};
|
|
}
|