Subject: [bug#28769] [PATCH] gnu: services: Add php-fpm.

From: nee <[email-masked]>
Date: Thu, 23 Nov 2017 21:11:48 +0100

Hello sorry about not replying for such a long time, and thank you for
reviewing my patches again.

Am 02.11.2017 um 20:17 schrieb Christopher Baines:
> 
> I've now attached a system test for php-fpm, that sets it up with
> nginx, creates a basic php file, and checks that it can be requested.

That's great thanks for saving me a bunch of work with this. ;-)

> 
> This seems to work, which I was a little surprised at, as I was
> suspecting problems with the socket permissions.
> 
> I had a look, and while the nginx workers in the test system are not
> root, the nginx master process is, so maybe that allows it to work...
I don't think that's the reason, because I remember it not working at
first when I didn't have the permissions set.

>> I renamed the default workers-log-file to php7-fpm.www.log as it is
>> usually called. The php-fpm log-file is now called php7-fpm.log.
> 
> What I think I meant to say here was, I'm not sure the php _version_ is
> adding much to the log file name (rather than "I'm not sure the php is
> adding").
> 
> The php package version is used in a few places, and while I can
> imagine this being consistent with other distributions, it does add a
> bit of complexity to the default values in the service, and I'm not
> sure what benefit it brings?
> 
If users want to run multiple php versions, they only have to change the
version in the php-package and pass that package along all the services.

My perception of the php landscape was that the major releases aren't
100% reliably backwards compatible and some applications depend on older
stable releases, so that it is not too uncommon to run multiple php
versions the same system.

Here is a quote from: https://wordpress.org/about/requirements/

"""
Why do we support older versions?

We strongly recommend the latest versions of PHP and MySQL, but we
understand that this isn’t right for everyone, and that sometimes hosts
can be slow or hesitant to upgrade their customers since upgrades to PHP
and MySQL have historically broken applications.
"""

An alternative could be to include the php-package hash in the socket
name, but I'm not sure if that would work with nginx and it's currently
missing reload when a system-reconfigure is done. Or services could be
generally more isolated somehow.

WDYT?

>>>> +  (file          php-fpm-configuration-file ;#f | file-like
>>>> +                 (default #f)))
> 
> ...
> 
>>>> +                       (service-extension account-service-type
>>>> +                                          php-fpm-accounts)))
>>>> +                (default-value (php-fpm-configuration))))  
>>>
>>> Filling in the description (a relatively new field on the service type)
>>> would be a great addition here.
>>>   
>> Ah, yes I'm mostly using the web documentation and other services from
>> web.scm as reference. Thanks for the update.
>> What would be a good value for this field? I just used "The php-fpm
>> service-type." for now.
> 
> That is ok, but it could probably be more useful. I think ideally it
> would describe more about what the service offers.
> 
> Users might encounter this when searching for services for example:
> 
> → guix system search php
> name: php-fpm
> location: gnu/services/web.scm:607:2
> extends: shepherd-root activate account
> description: The php-fpm service-type.
> relevance: 5

I ran `guix sysytem search *` and it seems most descriptions start with
'Run' or 'Provide' I changed it to:
 "Run `php-fpm' to provide a fastcgi socket for calling php through a
webserver."

> I've attached a patch containing a couple of copy+paste fixes, and a
> system test.
> 
> It would be good to get your opinion on the system test, does it test
> the right things?
> 
The tests look good to me.
I added another test that adds two numbers and checks for the result in
the response to see if php actually did something with the file.

> If you like the look of it, I'd suggest including those changes in the
> commit that adds the service, and then sending the updated patches.
> I've included a changelog in the commit message.
> 
Here are the updated patches. Thank you for your tests!

From 5347b2880b92e055cb8df1ea2c68ece74f506180 Mon Sep 17 00:00:00 2001
From: nee <[email-masked]>
Date: Mon, 9 Oct 2017 23:02:05 +0200
Subject: [PATCH 1/2] guix: utils: add version-major.

* guix/utils.scm (version-major): New procedure.
---
 guix/utils.scm | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/guix/utils.scm b/guix/utils.scm
index c0ffed172..bfa5e3a69 100644
--- a/guix/utils.scm
+++ b/guix/utils.scm
@@ -80,6 +80,7 @@
             version>=?
             version-prefix
             version-major+minor
+            version-major
             guile-version>?
             string-replace-substring
             arguments-from-environment-variable
@@ -492,6 +493,10 @@ For example, (version-prefix \"2.1.47.4.23\" 3) returns \"2.1.47\""
 minor version numbers from version-string."
   (version-prefix version-string 2))
 
+(define (version-major version-string)
+  "Return the major version number as string from the version-string."
+  (version-prefix version-string 1))
+
 (define (version>? a b)
   "Return #t when A denotes a version strictly newer than B."
   (eq? '> (version-compare a b)))
-- 
2.14.1

From a9061ce9dafc1454e5b964e6c9e16c9269356ca5 Mon Sep 17 00:00:00 2001
From: nee <[email-masked]>
Date: Mon, 9 Oct 2017 23:06:05 +0200
Subject: [PATCH 2/2] 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.
---
 doc/guix.texi        | 137 +++++++++++++++++++++++++++-
 gnu/services/web.scm | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 gnu/tests/web.scm    | 123 ++++++++++++++++++++++++-
 3 files changed, 504 insertions(+), 4 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index d4a2a696a..9058c9a23 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -40,7 +40,8 @@ Copyright @copyright{} 2017 Christopher Allan Webber@*
 Copyright @copyright{} 2017 Marius Bakke@*
 Copyright @copyright{} 2017 Hartmut Goebel@*
 Copyright @copyright{} 2017 Maxim Cournoyer@*
-Copyright @copyright{} 2017 Tobias Geerinckx-Rice
+Copyright @copyright{} 2017 Tobias Geerinckx-Rice@*
+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
@@ -14942,6 +14943,140 @@ capability also has to be configured on the front-end as well.
 @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 (ala 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 {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/log/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 is 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
+
+
+@defvr {Scheme Variable} nginx-php-fpm-location
+A helper function to quickly add php to an @code{nginx-server-configuration}.
+@end defvr
+
+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 DNS Services
 @subsubsection DNS Services
 @cindex DNS (domain name system)
diff --git a/gnu/services/web.scm b/gnu/services/web.scm
index 9d713003c..3b3be215a 100644
--- a/gnu/services/web.scm
+++ b/gnu/services/web.scm
@@ -4,6 +4,7 @@
 ;;; Copyright © 2016 ng0 <[email-masked]>
 ;;; Copyright © 2016, 2017 Julien Lepiller <[email-masked]>
 ;;; Copyright © 2017 Christopher Baines <[email-masked]>
+;;; Copyright © 2017 nee <[email-masked]>
 ;;;
 ;;; 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>
@@ -76,7 +80,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:
 ;;;
@@ -256,10 +302,12 @@ of index files."
           "events {}\n")))
 
 (define %nginx-accounts
-  (list (user-group (name "nginx") (system? #t))
+  (list (user-group (name "php-fpm") (system? #t))
+        (user-group (name "nginx") (system? #t))
         (user-account
          (name "nginx")
          (group "nginx")
+         (supplementary-groups '("php-fpm"))
          (system? #t)
          (comment "nginx server user")
          (home-directory "/var/empty")
@@ -385,3 +433,199 @@ 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 "php-fpm"))
+  (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 `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;")))))
diff --git a/gnu/tests/web.scm b/gnu/tests/web.scm
index 3fa272c67..68233af9f 100644
--- a/gnu/tests/web.scm
+++ b/gnu/tests/web.scm
@@ -1,5 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2017 Ludovic Courtès <[email-masked]>
+;;; Copyright © 2017 Christopher Baines <[email-masked]>
 ;;;
 ;;; 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.
@@ -132,3 +134,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))))
-- 
2.14.1