diff --git a/modules/module-list.nix b/modules/module-list.nix index 40f1d1e867..ac82f62616 100644 --- a/modules/module-list.nix +++ b/modules/module-list.nix @@ -103,6 +103,7 @@ ./services/misc/nix-daemon.nix ./services/misc/nix-gc.nix ./services/misc/nixos-manual.nix + ./services/misc/phpfpm.nix ./services/misc/rogue.nix ./services/misc/svnserve.nix ./services/misc/synergy.nix diff --git a/modules/services/misc/phpfpm.nix b/modules/services/misc/phpfpm.nix new file mode 100644 index 0000000000..e1c9b6edf2 --- /dev/null +++ b/modules/services/misc/phpfpm.nix @@ -0,0 +1,372 @@ +{ config, pkgs, ... }: + +/* + +What does this php-fpm module provide? + +Short intro: php-fpm means running PHP outside of apache, with proper linux +user id/group. The php-fdpm daemon will spawn/kill processes as needed. +Comparison chart: http://php-fpm.org/about/#why + +How does it work? One php-fpm daemon supervises multiple pools. +However some options like xdebug cannot be configured for an individual pool, +thus if you want one project to be debugcgable you have to create a new +config/daemon pair. + +This is what this module is about: +You feed in a list of PHP pool configurations - and the module will group +groupable pools so that they can be supervisd by the same daemon. +Eg if you enable xdebug for one user you'll get two php-fpm services +automatically and everything should just work. + +Now that systemd exists each daemon has +- a name ( .service file) +- many pools (each has a socket clients like apache connect to) + +And that's what the functions daemonIdFun, socketPathFun are about: given a +configuration they derive a name. If you know that you're using one PHP-5.3 +configuration you can return "5.3", however you must ensure that different +daemon configs don't get the same name ! Thus the default implementation is +using a short hash based on the configuration. Downside is that log file names +change if you configure PHP differently. + +Obviously the socket paths are used by the systemd configuration and by the +apache/lighthttpd/nginx web server configurations. + +simple usage example illustrating how you can access the same local web applications +using two different domains php53 and php54 to test both versions, but +debugging is only enable for php 5.3 + + let + documentRoot = "/var/www"; + slowLogDir = "/var/www"; + + # You can have a different user/group for each pool - that's what + # php-fpm is about. For this sample using apache user/group. + user = config.services.httpd.user; + group = config.services.httpd.group; + + phpfpmPools = + let environment = { + # LOCALE_ARCHIVE="${pkgs.glibcLocales}/lib/locale/locale-archive"; + }; + in { + "php54" = { + daemonCfg.php = pkgs.php5_4fpm; + # daemonCfg.id = "5.4"; # optional + poolItemCfg = { + inherit environment; + inherit user group; + listen = { owner = config.services.httpd.user; group = config.services.httpd.group; mode = "0700"; }; + slowlog = "${slowLogDir}/slow-log-5.4"; + }; + }; + + "php53" = rec { + daemonCfg.php = pkgs.php5_3fpm; + daemonCfg.xdebug = { enable = true; remote_port = "9000"; }; + daemonCfg.phpIniLines = '' + additional php ini lines + ''; + # daemonCfg.id = "5.3"; # optional + poolItemCfg = { + # inherit environment; + inherit user group; + listen = { owner = config.services.httpd.user; group = config.services.httpd.group; mode = "0700"; }; + slowlog = "${slowLogDir}/slow-log-5.3"; + extraLines = '' + php_admin_value[sendmail_path] = /var/setuid-wrappers/sendmail -t -i + php_admin_value[date.timezone] = "${config.time.timeZone}" + php_admin_value[max_input_vars] = 10000 + # php_flag[name]=on/off + ''; + }; + }; + }; + + + in + + {pkgs , config, ...} : { + + networking.extraHosts = '' + 127.0.0.1 php53 php54 + ''; + + services.phpfpm.pools = lib.attrValues phpfpmPools; + + services.httpd.enable = true; + services.httpd.extraModules = [ { name = "fastcgi"; path = "${pkgs.mod_fastcgi}/modules/mod_fastcgi.so"; } }; + + services.httpd.extraConfig = '' + + Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch + Order allow,deny + Allow from all + AllowOverride All + + + FastCGIExternalServer /dev/shm/php5.3.fcgi -socket ${config.services.phpfpm.socketPathFun phpfpmPools.php53} -flush -idle-timeout 300 + FastCGIExternalServer /dev/shm/php5.4.fcgi -socket ${config.services.phpfpm.socketPathFun phpfpmPools.php54} -flush -idle-timeout 300 + ''; + services.httpd.virtualHosts = + ( + map (php: { + enable = true; + documentRoot = documentRoot; + hostName = php.domain; + extraConfig = '' + RewriteEngine On + + AddType application/x-httpd-php .php + AddType application/x-httpd-php .php5 + Action application/x-httpd-php /x/php.fcgi + Alias /x/php.fcgi /dev/shm/php${php.version}.fcgi + + + DirectoryIndex index.php index.html + Options +ExecCGI + Order allow,deny + Allow from all + AllowOverride All + + + ''; + }) [ + { domain = "php53"; version = "5.3"; } + { domain = "php54"; version = "5.4"; } + ] + ); + } + +*/ + +with pkgs.lib; + +let + inherit (builtins) listToAttrs head baseNameOf unsafeDiscardStringContext toString; + + cfg = config.services.phpfpm; + + phpIniFile = + {item, phpIniLines ? "", name}: + pkgs.runCommand name { inherit phpIniLines; } + "cat ${item.daemonCfg.php}/etc/php-recommended.ini > $out; echo \"$phpIniLines\" >> $out" + ; + + preparePool = item: # item = item of phpfpm.pools + let + enableXdebug = item.daemonCfg.xdebug.enable or false; + profileDir = item.daemonCfg.xdebug.profileDir or (id: "/tmp/xdebug-profiler-dir-${id}"); + xd = if enableXdebug + then + let remote_host = item.xdebug.remote_host or "127.0.0.1"; + remote_port = builtins.toString item.xdebug.remote_port or 9000; + in { + idAppend = "-xdebug"; + phpIniLines = '' + zend_extension="${item.daemonCfg.php.xdebug}/lib/xdebug.so" + zend_extension_ts="${item.daemonCfg.php.xdebug}/lib/xdebug.so" + zend_extension_debug="${item.daemonCfg.php.xdebug}/lib/xdebug.so" + xdebug.remote_enable=true + xdebug.remote_host=${remote_host} + xdebug.remote_port=${remote_port} + xdebug.remote_handler=dbgp + xdebug.profiler_enable=0 + xdebug.remote_mode=req + ''; + } + else { + idAppend = ""; + phpIniLines = ""; + }; + phpIniLines = + xd.phpIniLines + + (item.phpIniLines or ""); + + + # using phpIniLines create a cfg-id + iniId = builtins.substring 0 5 (builtins.hashString "sha256" (unsafeDiscardStringContext phpIniLines)) + +xd.idAppend; + + phpIni = (item.daemonCfg.phpIniFile or phpIniFile) { + inherit item; + name = "php-${iniId}.ini"; + phpIniLines = + phpIniLines + + optionalString enableXdebug "\nprofiler_output_dir = \"${item.daemonCfg.xdebug.profiler_output_dir or (profileDir iniId)}\;"; + }; + + # [ID] see daemonIdFun + id = item.daemonCfg.id or "${item.daemonCfg.php.id}-${iniId}"; + phpIniName = baseNameOf (unsafeDiscardStringContext item.phpIni); + in item // { + daemonCfg = item.daemonCfg // { + inherit phpIniName phpIni id; + }; + }; + + + phpFpmDaemons = + + let nv = name: value: listToAttrs [{ inherit name value; }]; + poolsWithIni = map preparePool cfg.pools; + # group pools by common php and php ini config + poolsByPHP = foldAttrs (n: a: [n] ++ a) [] (map (p: nv "${p.daemonCfg.id}" p) poolsWithIni); + toDaemon = name: pools: + let h = head pools; + in h.daemonCfg.php.system_fpm_config + { # daemon config + # TODO make option or such by letting user set these by php.id attr or such + log_level = "notice"; + emergency_restart_threshold = "10"; + emergency_restart_interval = "1m"; + process_control_timeout = "5s"; + inherit (h.daemonCfg) id phpIni phpIniLines; + } + # pools + (map (p: + let socketPath = cfg.socketPathFun p; + in p.poolItemCfg + // { + listen_address = socketPath; + name = builtins.baseNameOf socketPath; + }) + pools); + in attrValues (mapAttrs toDaemon poolsByPHP); + +in { + + options = { + services.phpfpm = { + + enable = mkOption { + default = true; + description = "Whether to enable the PHP FastCGI Process Manager."; + }; + + stateDir = mkOption { + default = "/var/run/phpfpm"; + description = "State directory with PID and socket files."; + }; + + logDir = mkOption { + default = "/var/log/phpfpm"; + description = "Directory where to put in log files."; + }; + + daemonIdFun = mkOption { + description = "Function returning service name based on php compilation options, php ini file."; + default = pool: (preparePool pool).daemonCfg.id; + }; + + socketPathFun = mkOption { + description = "Function returning socket path by pool to which web servers connect to."; + default = pool: + let pool_h = builtins.substring 0 8 (builtins.hashString "sha256" (builtins.toXML pool.poolItemCfg)); + in "/dev/shm/php-fpm-${cfg.daemonIdFun pool}-${pool_h}"; + }; + + pools = mkOption { + default = []; + example = []; + /* a typical list item looks like this - I don't think it makes sense to show + this example in generated docs - its too big + + rec { + + ### php-fpm daemon options: If contents differ multiple daemons will be started + daemonCfg = { + + ### id + # optional: + # An ID based on the PHP configuration is generated automatically, see [ID] and daemonIdFun + # id = "php-5.3" + # this id is used to make the systemd units and the socket paths unique + # see daemonIdFun etc. + + # php version, must support fpm, thus must have a system_fpm_config attr + php = pkgs.php5_2fpm.override {}; + + # optional: append addditional php.ini lines. + + # Please note that most options can be set by using etxraLines in + # the pool configuration like this: + # php_flag[name]=on/off + # php_admin_value[str_option]="127.0.0.1" + # which should be preferred so that less php-fpm daemons have to be started + # These lines are appended to the default configuartion shipping with PHP + # unless phpIniFile is given + # + # phpIniLines appends lines to the default php.ini file shipping with PHP. + # You can override everything by either setting + # - phpIniFile (function returning file, see sample in this file) + # - phpIni (must be a file), eg phpIniFile = pkgs.writeFile ... + phpIniLines = '' + ''; + + # optional: enable xdebug, if set additional phpIniLines will be created + # xdebug can't be configured per pool, see: https://bugs.php.net/bug.php?id=54824 + xdebug = { + enable = true; + # optional names: + # remote_host = "127.0.0.1"; + # remote_port = 9000; + # profileDir = id: "/tmp/xdebug-profiler-dir-${id}"; # setting profiler_output_dir + }; + + }; + + ### php-fpm per pool options + poolItemCfg = { + + # pool config, see system_fpm_config implementation in nixpkgs + slowlog = ""; # must be writeable by the user/group of the php process? + + user = "user"; + group = "group"; + # listen_adress will be set automatically by socketPathFun + listen = { owner = config.services.httpd.user; group = config.services.httpd.group; mode = "0700"; }; + + extraLines = '' + php_admin_value[sendmail_path] = /var/setuid-wrappers/sendmail -t -i + php_admin_value[date.timezone] = "${config.time.timeZone}" + php_admin_value[max_input_vars] = 10000 + php_flag[name]=on/off + ''; + + pm = { + value = "dynamic"; + max_children = 400; + min_spare_servers = 10; + max_spare_servers = 30; + }; + + }; + } + */ + + description = '' + Examples and more detailed explanation about the + option can be found in the + nixos module's source code. + + The module will group pools by uniq (php, php-ini) tuples + and start a new php-fpm daemon for each group which will manage its + pools. + + The php-fpm daemon must run as root, because it must switch user for + worker threads. + ''; + }; + + }; + }; + + # config = mkIf cfg.enable (mkMerge phpFpmDaemons); + # is too strict, need to evaluate "config", so pick attrs which are used only + config = { + environment = mkMerge (catAttrs "environment" phpFpmDaemons); + systemd = mkMerge (catAttrs "systemd" phpFpmDaemons); + }; +}