~ruther/guix-local

64bae7237c65b32d97e16ff6802124c26867754a — nee 8 years ago e05cc6b8
gnu: services: Add php-fpm.

* gnu/services/web.scm (<php-fpm-configuration>,
  <php-fpm-process-manager-configuration>): New record types.
  (php-fpm-configuration?,
   php-fpm-process-manager-configuration?,
   php-fpm-service-type,
   nginx-php-location): New procedures.
* doc/guix.texi (Web-Services): Document php-fpm service.
* gnu/tests/web.scm: Add php-fpm system test.

Signed-off-by: Christopher Baines <mail@cbaines.net>
3 files changed, 508 insertions(+), 2 deletions(-)

M doc/guix.texi
M gnu/services/web.scm
M gnu/tests/web.scm
M doc/guix.texi => doc/guix.texi +140 -0
@@ 44,6 44,7 @@ Copyright @copyright{} 2017 Tobias Geerinckx-Rice@*
Copyright @copyright{} 2017 George Clemmer@*
Copyright @copyright{} 2017 Andy Wingo@*
Copyright @copyright{} 2017 Arun Isaac
Copyright @copyright{} 2017 nee

Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3 or


@@ 15081,6 15082,145 @@ capability also has to be configured on the front-end as well.
@end table
@end deftp

@cindex php-fpm
PHP-FPM (FastCGI Process Manager) is an alternative PHP FastCGI implementation
with some additional features useful for sites of any size.

These features include:
@itemize @bullet
@item Adaptive process spawning
@item Basic statistics (similar to Apache's mod_status)
@item Advanced process management with graceful stop/start
@item Ability to start workers with different uid/gid/chroot/environment
and different php.ini (replaces safe_mode)
@item Stdout & stderr logging
@item Emergency restart in case of accidental opcode cache destruction
@item Accelerated upload support
@item Support for a "slowlog"
@item Enhancements to FastCGI, such as fastcgi_finish_request() -
a special function to finish request & flush all data while continuing to do
something time-consuming (video converting, stats processing, etc.)
@end itemize
... and much more.

@defvr {Scheme Variable} php-fpm-service-type
A Service type for @code{php-fpm}.
@end defvr

@deftp {Data Type} php-fpm-configuration
Data Type for php-fpm service configuration.
@table @asis
@item @code{php} (default: @code{php})
The php package to use.
@item @code{socket} (default: @code{(string-append "/var/run/php" (version-major (package-version php)) "-fpm.sock")})
The address on which to accept FastCGI requests.  Valid syntaxes are:
@table @asis
@item @code{"ip.add.re.ss:port"}
Listen on a TCP socket to a specific address on a specific port.
@item @code{"port"}
Listen on a TCP socket to all addresses on a specific port.
@item @code{"/path/to/unix/socket"}
Listen on a unix socket.
@end table

@item @code{user} (default: @code{php-fpm})
User who will own the php worker processes.
@item @code{group} (default: @code{php-fpm})
Group of the worker processes.
@item @code{socket-user} (default: @code{php-fpm})
User who can speak to the php-fpm socket.
@item @code{socket-group} (default: @code{php-fpm})
Group that can speak to the php-fpm socket.
@item @code{pid-file} (default: @code{(string-append "/var/run/php" (version-major (package-version php)) "-fpm.pid")})
The process id of the php-fpm process is written to this file
once the service has started.
@item @code{log-file} (default: @code{(string-append "/var/log/php" (version-major (package-version php)) "-fpm.log")})
Log for the php-fpm master process.
@item @code{process-manager} (default: @code{(php-fpm-dynamic-process-manager-configuration)})
Detailed settings for the php-fpm process manager.
Must be either:
@table @asis
@item @code{<php-fpm-dynamic-process-manager-configuration>}
@item @code{<php-fpm-static-process-manager-configuration>}
@item @code{<php-fpm-on-demand-process-manager-configuration>}
@end table
@item @code{display-errors} (default @code{#f})
Determines wether php errors and warning should be sent to clients
and displayed in their browsers.
This is useful for local php development, but a security risk for public sites,
as error messages can reveal passwords and personal data.
@item @code{workers-logfile} (default @code{(string-append "/var/log/php" (version-major (package-version php)) "-fpm.www.log")})
This file will log the @code{stderr} outputs of php worker processes.
Can be set to @code{#f} to disable logging.
@item @code{file} (default @code{#f})
An optional override of the whole configuration.
You can use the @code{mixed-text-file} function or an absolute filepath for it.
@end table
@end deftp

@deftp {Data type} php-fpm-dynamic-process-manager-configuration
Data Type for the @code{dynamic} php-fpm process manager.  With the
@code{dynamic} process manager, spare worker processes are kept around
based on it's configured limits.
@table @asis
@item @code{max-children} (default: @code{5})
Maximum of worker processes.
@item @code{start-servers} (default: @code{2})
How many worker processes should be started on start-up.
@item @code{min-spare-servers} (default: @code{1})
How many spare worker processes should be kept around at minimum.
@item @code{max-spare-servers} (default: @code{3})
How many spare worker processes should be kept around at maximum.
@end table
@end deftp

@deftp {Data type} php-fpm-static-process-manager-configuration
Data Type for the @code{static} php-fpm process manager.  With the
@code{static} process manager, an unchanging number of worker processes
are created.
@table @asis
@item @code{max-children} (default: @code{5})
Maximum of worker processes.
@end table
@end deftp

@deftp {Data type} php-fpm-on-demand-process-manager-configuration
Data Type for the @code{on-demand} php-fpm process manager.  With the
@code{on-demand} process manager, worker processes are only created as
requests arrive.
@table @asis
@item @code{max-children} (default: @code{5})
Maximum of worker processes.
@item @code{process-idle-timeout} (default: @code{10})
The time in seconds after which a process with no requests is killed.
@end table
@end deftp


@deffn {Scheme Procedure} nginx-php-fpm-location @
       [#:nginx-package nginx] @
       [socket (string-append "/var/run/php" @
                              (version-major (package-version php)) @
                              "-fpm.sock")]
A helper function to quickly add php to an @code{nginx-server-configuration}.
@end deffn

A simple services setup for nginx with php can look like this:
@example
(services (cons* (dhcp-client-service)
                 (service php-fpm-service-type)
                 (service nginx-service-type
                          (nginx-server-configuration
                           (server-name '("example.com"))
                           (root "/srv/http/")
                           (locations
                            (list (nginx-php-location)))
                           (https-port #f)
                           (ssl-certificate #f)
                           (ssl-certificate-key #f)))
                 %base-services))
@end example

@node Certificate Services
@subsubsection Certificate Services


M gnu/services/web.scm => gnu/services/web.scm +246 -1
@@ 4,6 4,7 @@
;;; Copyright © 2016 ng0 <ng0@we.make.ritual.n0.is>
;;; Copyright © 2016, 2017 Julien Lepiller <julien@lepiller.eu>
;;; Copyright © 2017 Christopher Baines <mail@cbaines.net>
;;; Copyright © 2017 nee <nee-git@hidamari.blue>
;;;
;;; This file is part of GNU Guix.
;;;


@@ 26,8 27,11 @@
  #:use-module (gnu system shadow)
  #:use-module (gnu packages admin)
  #:use-module (gnu packages web)
  #:use-module (gnu packages php)
  #:use-module (guix records)
  #:use-module (guix gexp)
  #:use-module ((guix utils) #:select (version-major))
  #:use-module ((guix packages) #:select (package-version))
  #:use-module (srfi srfi-1)
  #:use-module (ice-9 match)
  #:export (<nginx-configuration>


@@ 78,7 82,49 @@

            fcgiwrap-configuration
            fcgiwrap-configuration?
            fcgiwrap-service-type))
            fcgiwrap-service-type

            <php-fpm-configuration>
            php-fpm-configuration
            make-php-fpm-configuration
            php-fpm-configuration?
            php-fpm-configuration-php
            php-fpm-configuration-socket
            php-fpm-configuration-user
            php-fpm-configuration-group
            php-fpm-configuration-socket-user
            php-fpm-configuration-socket-group
            php-fpm-configuration-pid-file
            php-fpm-configuration-log-file
            php-fpm-configuration-process-manager
            php-fpm-configuration-display-errors
            php-fpm-configuration-workers-log-file
            php-fpm-configuration-file

            <php-fpm-dynamic-process-manager-configuration>
            php-fpm-dynamic-process-manager-configuration
            make-php-fpm-dynamic-process-manager-configuration
            php-fpm-dynamic-process-manager-configuration?
            php-fpm-dynamic-process-manager-configuration-max-children
            php-fpm-dynamic-process-manager-configuration-start-servers
            php-fpm-dynamic-process-manager-configuration-min-spare-servers
            php-fpm-dynamic-process-manager-configuration-max-spare-servers

            <php-fpm-static-process-manager-configuration>
            php-fpm-static-process-manager-configuration
            make-php-fpm-static-process-manager-configuration
            php-fpm-static-process-manager-configuration?
            php-fpm-static-process-manager-configuration-max-children

            <php-fpm-on-demand-process-manager-configuration>
            php-fpm-on-demand-process-manager-configuration
            make-php-fpm-on-demand-process-manager-configuration
            php-fpm-on-demand-process-manager-configuration?
            php-fpm-on-demand-process-manager-configuration-max-children
            php-fpm-on-demand-process-manager-configuration-process-idle-timeout

            php-fpm-service-type
            nginx-php-location))

;;; Commentary:
;;;


@@ 397,3 443,202 @@ of index files."
		       (service-extension account-service-type
                                          fcgiwrap-accounts)))
                (default-value (fcgiwrap-configuration))))

(define-record-type* <php-fpm-configuration> php-fpm-configuration
  make-php-fpm-configuration
  php-fpm-configuration?
  (php              php-fpm-configuration-php ;<package>
                    (default php))
  (socket           php-fpm-configuration-socket
                    (default (string-append "/var/run/php"
                                            (version-major (package-version php))
                                            "-fpm.sock")))
  (user             php-fpm-configuration-user
                    (default "php-fpm"))
  (group            php-fpm-configuration-group
                    (default "php-fpm"))
  (socket-user      php-fpm-configuration-socket-user
                    (default "php-fpm"))
  (socket-group     php-fpm-configuration-socket-group
                    (default "nginx"))
  (pid-file         php-fpm-configuration-pid-file
                    (default (string-append "/var/run/php"
                                            (version-major (package-version php))
                                            "-fpm.pid")))
  (log-file         php-fpm-configuration-log-file
                    (default (string-append "/var/log/php"
                                            (version-major (package-version php))
                                            "-fpm.log")))
  (process-manager  php-fpm-configuration-process-manager
                    (default (php-fpm-dynamic-process-manager-configuration)))
  (display-errors   php-fpm-configuration-display-errors
                    (default #f))
  (workers-log-file php-fpm-configuration-workers-log-file
                    (default (string-append "/var/log/php"
                                            (version-major (package-version php))
                                            "-fpm.www.log")))
  (file             php-fpm-configuration-file ;#f | file-like
                    (default #f)))

(define-record-type* <php-fpm-dynamic-process-manager-configuration>
  php-fpm-dynamic-process-manager-configuration
  make-php-fpm-dynamic-process-manager-configuration
  php-fpm-dynamic-process-manager-configuration?
  (max-children         php-fpm-dynamic-process-manager-configuration-max-children
                        (default 5))
  (start-servers        php-fpm-dynamic-process-manager-configuration-start-servers
                        (default 2))
  (min-spare-servers    php-fpm-dynamic-process-manager-configuration-min-spare-servers
                        (default 1))
  (max-spare-servers    php-fpm-dynamic-process-manager-configuration-max-spare-servers
                        (default 3)))

(define-record-type* <php-fpm-static-process-manager-configuration>
  php-fpm-static-process-manager-configuration
  make-php-fpm-static-process-manager-configuration
  php-fpm-static-process-manager-configuration?
  (max-children         php-fpm-static-process-manager-configuration-max-children
                        (default 5)))

(define-record-type* <php-fpm-on-demand-process-manager-configuration>
  php-fpm-on-demand-process-manager-configuration
  make-php-fpm-on-demand-process-manager-configuration
  php-fpm-on-demand-process-manager-configuration?
  (max-children         php-fpm-on-demand-process-manager-configuration-max-children
                        (default 5))
  (process-idle-timeout php-fpm-on-demand-process-manager-configuration-process-idle-timeout
                        (default 10)))

(define php-fpm-accounts
  (match-lambda
    (($ <php-fpm-configuration> php socket user group socket-user socket-group _ _ _ _ _ _)
     (list
      (user-group (name "php-fpm") (system? #t))
      (user-group
       (name group)
       (system? #t))
      (user-account
       (name user)
       (group group)
       (supplementary-groups '("php-fpm"))
       (system? #t)
       (comment "php-fpm daemon user")
       (home-directory "/var/empty")
       (shell (file-append shadow "/sbin/nologin")))))))

(define (default-php-fpm-config socket user group socket-user socket-group
          pid-file log-file pm display-errors workers-log-file)
  (apply mixed-text-file "php-fpm.conf"
         (flatten
          "[global]\n"
          "pid =" pid-file "\n"
          "error_log =" log-file "\n"
          "[www]\n"
          "user =" user "\n"
          "group =" group "\n"
          "listen =" socket "\n"
          "listen.owner =" socket-user "\n"
          "listen.group =" socket-group "\n"

          (match pm
            (($ <php-fpm-dynamic-process-manager-configuration>
                pm.max-children
                pm.start-servers
                pm.min-spare-servers
                pm.max-spare-servers)
             (list
              "pm = dynamic\n"
              "pm.max_children =" (number->string pm.max-children) "\n"
              "pm.start_servers =" (number->string pm.start-servers) "\n"
              "pm.min_spare_servers =" (number->string pm.min-spare-servers) "\n"
              "pm.max_spare_servers =" (number->string pm.max-spare-servers) "\n"))

            (($ <php-fpm-static-process-manager-configuration>
                pm.max-children)
             (list
              "pm = static\n"
              "pm.max_children =" (number->string pm.max-children) "\n"))

            (($ <php-fpm-on-demand-process-manager-configuration>
                pm.max-children
                pm.process-idle-timeout)
             (list
              "pm = ondemand\n"
              "pm.max_children =" (number->string pm.max-children) "\n"
              "pm.process_idle_timeout =" (number->string pm.process-idle-timeout) "s\n")))


          "php_flag[display_errors] = " (if display-errors "on" "off") "\n"

          (if workers-log-file
              (list "catch_workers_output = yes\n"
                    "php_admin_value[error_log] =" workers-log-file "\n"
                    "php_admin_flag[log_errors] = on\n")
              (list "catch_workers_output = no\n")))))

(define php-fpm-shepherd-service
  (match-lambda
    (($ <php-fpm-configuration> php socket user group socket-user socket-group
                                pid-file log-file pm display-errors workers-log-file file)
     (list (shepherd-service
            (provision '(php-fpm))
            (documentation "Run the php-fpm daemon.")
            (requirement '(networking))
            (start #~(make-forkexec-constructor
                      '(#$(file-append php "/sbin/php-fpm")
                        "--fpm-config"
                        #$(or file
                              (default-php-fpm-config socket user group
                                socket-user socket-group pid-file log-file
                                pm display-errors workers-log-file)))
                      #:pid-file #$pid-file))
            (stop #~(make-kill-destructor)))))))

(define php-fpm-activation
  (match-lambda
    (($ <php-fpm-configuration> _ _ user _ _ _ _ log-file _ _ workers-log-file _)
     #~(begin
         (use-modules (guix build utils))
         (let* ((user (getpwnam #$user))
                (touch (lambda (file-name)
                         (call-with-output-file file-name (const #t))))
                (init-log-file
                 (lambda (file-name)
                   (when #$workers-log-file
                     (when (not (file-exists? file-name))
                       (touch file-name))
                     (chown file-name (passwd:uid user) (passwd:gid user))
                     (chmod file-name #o660)))))
           (init-log-file #$log-file)
           (init-log-file #$workers-log-file))))))


(define php-fpm-service-type
  (service-type
   (name 'php-fpm)
   (description
    "Run @command{php-fpm} to provide a fastcgi socket for calling php through
a webserver.")
   (extensions
    (list (service-extension shepherd-root-service-type
                             php-fpm-shepherd-service)
          (service-extension activation-service-type
                             php-fpm-activation)
          (service-extension account-service-type
                             php-fpm-accounts)))
   (default-value (php-fpm-configuration))))

(define* (nginx-php-location
          #:key
          (nginx-package nginx)
          (socket (string-append "/var/run/php"
                                 (version-major (package-version php))
                                 "-fpm.sock")))
  "Return a nginx-location-configuration that makes nginx run .php files."
  (nginx-location-configuration
   (uri "~ \\.php$")
   (body (list
          "fastcgi_split_path_info ^(.+\\.php)(/.+)$;"
          (string-append "fastcgi_pass unix:" socket ";")
          "fastcgi_index index.php;"
          (list "include " nginx-package "/share/nginx/conf/fastcgi.conf;")))))

M gnu/tests/web.scm => gnu/tests/web.scm +122 -1
@@ 1,5 1,6 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2017 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2017 Christopher Baines <mail@cbaines.net>
;;;
;;; This file is part of GNU Guix.
;;;


@@ 27,7 28,8 @@
  #:use-module (gnu services networking)
  #:use-module (guix gexp)
  #:use-module (guix store)
  #:export (%test-nginx))
  #:export (%test-nginx
            %test-php-fpm))

(define %index.html-contents
  ;; Contents of the /index.html file served by nginx.


@@ 129,3 131,122 @@ HTTP-PORT."
   (name "nginx")
   (description "Connect to a running NGINX server.")
   (value (run-nginx-test))))


;;;
;;; PHP-FPM
;;;

(define %make-php-fpm-http-root
  ;; Create our server root in /srv.
  #~(begin
      (mkdir "/srv")
      (call-with-output-file "/srv/index.php"
        (lambda (port)
          (display "<?php
phpinfo();
echo(\"Computed by php:\".((string)(2+3)));
?>\n" port)))))

(define %php-fpm-nginx-server-blocks
  (list (nginx-server-configuration
         (root "/srv")
         (locations
          (list (nginx-php-location)))
         (http-port 8042)
         (https-port #f)
         (ssl-certificate #f)
         (ssl-certificate-key #f))))

(define %php-fpm-os
  ;; Operating system under test.
  (simple-operating-system
   (dhcp-client-service)
   (service php-fpm-service-type)
   (service nginx-service-type
            (nginx-configuration
             (server-blocks %php-fpm-nginx-server-blocks)))
   (simple-service 'make-http-root activation-service-type
                   %make-php-fpm-http-root)))

(define* (run-php-fpm-test #:optional (http-port 8042))
  "Run tests in %PHP-FPM-OS, which has nginx running and listening on
HTTP-PORT, along with php-fpm."
  (define os
    (marionette-operating-system
     %php-fpm-os
     #:imported-modules '((gnu services herd)
                          (guix combinators))))

  (define vm
    (virtual-machine
     (operating-system os)
     (port-forwardings `((8080 . ,http-port)))))

  (define test
    (with-imported-modules '((gnu build marionette)
                             (guix build utils))
      #~(begin
          (use-modules (srfi srfi-11) (srfi srfi-64)
                       (gnu build marionette)
                       (web uri)
                       (web client)
                       (web response))

          (define marionette
            (make-marionette (list #$vm)))

          (mkdir #$output)
          (chdir #$output)

          (test-begin "php-fpm")

          (test-assert "php-fpm running"
            (marionette-eval
             '(begin
                (use-modules (gnu services herd))
                (match (start-service 'php-fpm)
                  (#f #f)
                  (('service response-parts ...)
                   (match (assq-ref response-parts 'running)
                     ((pid) (number? pid))))))
             marionette))

          (test-eq "nginx running"
            'running!
            (marionette-eval
             '(begin
                (use-modules (gnu services herd))
                (start-service 'nginx)
                'running!)
             marionette))

          (test-equal "http-get"
            200
            (let-values (((response text)
                          (http-get "http://localhost:8080/index.php"
                                    #:decode-body? #t)))
              (response-code response)))

          (test-equal "php computed result is sent"
            "Computed by php:5"
            (let-values (((response text)
                          (http-get "http://localhost:8080/index.php"
                                    #:decode-body? #t)))
              (begin
                (use-modules (ice-9 regex))
                (let ((matches (string-match "Computed by php:5" text)))
                  (and matches
                       (match:substring matches 0))))))

          (test-end)

          (exit (= (test-runner-fail-count (test-runner-current)) 0)))))

  (gexp->derivation "php-fpm-test" test))

(define %test-php-fpm
  (system-test
   (name "php-fpm")
   (description "Test PHP-FPM through nginx.")
   (value (run-php-fpm-test))))