This is an automated email from the git hooks/post-receive script.
guix_mirror_bot pushed a commit to branch master
in repository guix.
The following commit(s) were added to refs/heads/master by this push:
new 0c506e6f52 gnu: services: Add gunicorn-service-type.
0c506e6f52 is described below
commit 0c506e6f526e51d0dd9c69e4141711bc8ab7659c
Author: Fabio Natali <[email protected]>
AuthorDate: Sun Feb 8 11:16:38 2026 +0000
gnu: services: Add gunicorn-service-type.
* gnu/services/web.scm
(<gunicorn-configuration>, <gunicorn-app>): New records.
(unix-socket?, unix-socket-path, gunicorn-activation,
gunicorn-shepherd-services): New procedures.
(gunicorn-service-type): New variable.
* doc/guix.texi (Web Services): Document the new service.
Co-authored-by: Arun Isaac <[email protected]>
Change-Id: I3aa970422e6a5d31158b798b1061e6928ad2160b
Signed-off-by: jgart <[email protected]>
---
doc/guix.texi | 108 +++++++++++++++++++++++++++
gnu/services/web.scm | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 308 insertions(+), 3 deletions(-)
diff --git a/doc/guix.texi b/doc/guix.texi
index de1dc149ea..70bee4ad0c 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -35165,6 +35165,114 @@ flattened into one line.
@end table
@end deftp
+@subsubheading gunicorn
+@cindex gunicorn
+
+@defvar gunicorn-service-type
+Service type for the @uref{https://gunicorn.org/,gunicorn} Python Web
+Server Gateway Interface (WSGI) HTTP server. The value for this
+service type is a @code{<gunicorn-configuration>} record. A simple
+example follows where gunicorn is used in combination with a web
+server, Nginx, configured as a reverse proxy.
+
+@lisp
+(define %socket "unix:/var/run/gunicorn/werkzeug/socket")
+(define %body (format #f "proxy_pass http://~a;" %socket))
+
+(operating-system
+ ;; ...
+ (services
+ ;; ...
+ (service gunicorn-service-type
+ (gunicorn-configuration
+ (apps
+ (list (gunicorn-app
+ (name "werkzeug")
+ (package python-werkzeug)
+ (wsgi-app-module "werkzeug.testapp:test_app")
+ (sockets `(,%socket))
+ (user "user")
+ (group "users"))))))
+ (service nginx-service-type
+ (nginx-configuration
+ (server-blocks
+ (list (nginx-server-configuration
+ (server-name '("localhost"))
+ (listen '("127.0.0.1:80"))
+ (locations
+ (list (nginx-location-configuration
+ (uri "/")
+ (body `(,%body))))))))))))
+@end lisp
+@end defvar
+
+In practice, it is likely one might want to use
+@code{gunicorn-service-type} by extending it from within a Python
+service that requires gunicorn.
+
+@deftp {Data Type} gunicorn-configuration
+This data type represents the configuration for gunicorn.
+
+@table @asis
+@item @code{gunicorn} (default: @code{gunicorn})
+The gunicorn package to use.
+
+@item @code{apps} (default: @code{'()})
+This is a list of gunicorn apps.
+
+@end table
+@end deftp
+
+@deftp {Data Type} gunicorn-app
+This data type represents the configuration for a gunicorn
+application.
+
+@table @asis
+@item @code{name} (type: string)
+The name of the gunicorn application.
+
+@item @code{package} (type: symbol)
+The Python package associated to the gunicorn app.
+
+@item @code{wsgi-app-module} (type: string)
+The Python module that should be invoked by gunicorn.
+
+@item @code{user} (type: string)
+Launch the app as this user (it must be an existing user).
+
+@item @code{group} (type: string)
+Launch the app as this group (it must be an existing group).
+
+@item @code{sockets} (default:
@code{'("unix:/var/run/gunicorn/APP-NAME/socket")}) (type: list-of-strings)
+A list of sockets (as path strings) which gunicorn will be listening
+on. This list must contain at least one socket.
+
+@item @code{workers} (default: @code{1}) (type: integer)
+The number of workers for the gunicorn app.
+
+@item @code{extra-cli-arguments} (default: @code{'()}) (type: list-of-strings)
+A list of extra arguments to be passed to gunicorn.
+
+@item @code{environment-variables} (default: @code{'()}) (type: list)
+An association list of environment variables that will be visible to
+the gunicorn app, as per this example:
+
+@lisp
+(gunicorn-app (name "example")
+ ...
+ (environment-variables '(("foo" . "bar"))))
+@end lisp
+
+@item @code{timeout} (default: @code{30}) (type: integer)
+Workers silent for more than this many seconds are killed and
+restarted.
+
+@item @code{mappings} (default: @code{'()}) (type: list)
+Volumes that will be visible to the gunicorn app, as a list of
+source-target pairs.
+@end table
+@end deftp
+
@subsubheading Varnish Cache
@cindex Varnish
Varnish is a fast cache server that sits in between web applications
diff --git a/gnu/services/web.scm b/gnu/services/web.scm
index 3354b3295b..7df1c66b9f 100644
--- a/gnu/services/web.scm
+++ b/gnu/services/web.scm
@@ -11,7 +11,7 @@
;;; Copyright © 2019, 2020 Florian Pelz <[email protected]>
;;; Copyright © 2020, 2022 Ricardo Wurmus <[email protected]>
;;; Copyright © 2020 Tobias Geerinckx-Rice <[email protected]>
-;;; Copyright © 2020, 2025 Arun Isaac <[email protected]>
+;;; Copyright © 2020, 2025, 2026 Arun Isaac <[email protected]>
;;; Copyright © 2020 Oleg Pykhalov <[email protected]>
;;; Copyright © 2020, 2021 Alexandru-Sergiu Marton <[email protected]>
;;; Copyright © 2022 Simen Endsjø <[email protected]>
@@ -20,6 +20,7 @@
;;; Copyright © 2024 Leo Nikkilä <[email protected]>
;;; Copyright © 2025 Maxim Cournoyer <[email protected]>
;;; Copyright © 2025 Rodion Goritskov <[email protected]>
+;;; Copyright © 2026 Fabio Natali <[email protected]>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -65,12 +66,14 @@
#:autoload (guix i18n) (G_)
#:autoload (gnu build linux-container) (%namespaces)
#:use-module (guix diagnostics)
+ #:use-module (guix gexp)
#:use-module (guix least-authority)
+ #:use-module (guix modules)
#:use-module (guix packages)
+ #:use-module (guix profiles)
#:use-module (guix records)
- #:use-module (guix modules)
+ #:use-module (guix search-paths)
#:use-module (guix utils)
- #:use-module (guix gexp)
#:use-module ((guix store) #:select (text-file))
#:use-module ((guix utils) #:select (version-major))
#:use-module ((guix packages) #:select (package-version))
@@ -81,6 +84,7 @@
#:use-module (ice-9 match)
#:use-module (ice-9 format)
#:use-module (ice-9 regex)
+ #:use-module (web uri)
#:export (httpd-configuration
httpd-configuration?
httpd-configuration-package
@@ -170,6 +174,25 @@
nginx-service
nginx-service-type
+ gunicorn-app
+ gunicorn-app-environment-variables
+ gunicorn-app-extra-cli-arguments
+ gunicorn-app-group
+ gunicorn-app-mappings
+ gunicorn-app-name
+ gunicorn-app-package
+ gunicorn-app-sockets
+ gunicorn-app-timeout
+ gunicorn-app-user
+ gunicorn-app-workers
+ gunicorn-app-wsgi-app-module
+ gunicorn-app?
+ gunicorn-configuration
+ gunicorn-configuration-apps
+ gunicorn-configuration-package
+ gunicorn-configuration?
+ gunicorn-service-type
+
fcgiwrap-configuration
fcgiwrap-configuration?
fcgiwrap-service-type
@@ -1002,6 +1025,180 @@ renewed TLS certificates, or @code{include}d files.")
(default-value (nginx-configuration))
(description "Run the nginx Web server.")))
+(define-record-type* <gunicorn-configuration>
+ gunicorn-configuration make-gunicorn-configuration
+ gunicorn-configuration?
+ (package gunicorn-configuration-package
+ (default gunicorn))
+ (apps gunicorn-configuration-apps
+ (default '())))
+
+(define (sanitize-gunicorn-app-sockets value)
+ (unless (and (list? value)
+ (not (null? value)))
+ (leave (G_ "gunicorn: '~a' is invalid; must be a non-empty list~%") value))
+ value)
+
+(define-record-type* <gunicorn-app>
+ gunicorn-app make-gunicorn-app
+ gunicorn-app?
+ this-gunicorn-app
+ (name gunicorn-app-name)
+ (package gunicorn-app-package)
+ (wsgi-app-module gunicorn-app-wsgi-app-module)
+ (user gunicorn-app-user)
+ (group gunicorn-app-group)
+ (sockets gunicorn-app-sockets
+ (default (list (string-append "unix:/var/run/gunicorn/"
+ (gunicorn-app-name this-gunicorn-app)
+ "/socket")))
+ (sanitize sanitize-gunicorn-app-sockets)
+ (thunked))
+ (workers gunicorn-app-workers
+ (default 1))
+ (extra-cli-arguments gunicorn-app-extra-cli-arguments
+ (default '()))
+ (environment-variables gunicorn-app-environment-variables
+ (default '()))
+ (timeout gunicorn-app-timeout
+ (default 30))
+ (mappings gunicorn-app-mappings
+ (default '())))
+
+(define (unix-socket? string)
+ "Whether STRING indicates a Unix socket."
+ (eq? 'unix (uri-scheme (string->uri string))))
+
+(define (socket-dir string)
+ "Return the socket directory."
+ (dirname (uri-path (string->uri string))))
+
+(define (gunicorn-activation config)
+ (with-imported-modules '((guix build utils))
+ #~(begin
+ (use-modules (guix build utils)
+ (ice-9 match))
+
+ ;; Create socket directories and set ownership.
+ (for-each (match-lambda
+ ((user group socket-directories ...)
+ (for-each (lambda (socket-directory)
+ (mkdir-p socket-directory)
+ (chown socket-directory
+ (passwd:uid (getpw user))
+ (group:gid (getgrnam group))))
+ socket-directories)))
+ '#$(map (lambda (app)
+ (cons* (gunicorn-app-user app)
+ (gunicorn-app-group app)
+ (filter-map (lambda (socket)
+ (and
+ (unix-socket? socket)
+ (socket-dir socket)))
+ (gunicorn-app-sockets app))))
+ (gunicorn-configuration-apps config))))))
+
+(define (gunicorn-shepherd-services config)
+ (map (lambda (app)
+ (let ((name (string-append "gunicorn-" (gunicorn-app-name app)))
+ (user (gunicorn-app-user app))
+ (group (gunicorn-app-group app)))
+ (shepherd-service
+ (documentation (string-append "Run gunicorn for app "
+ (gunicorn-app-name app)
+ "."))
+ (provision (list (string->symbol name)))
+ (requirement '(networking))
+ (modules '((guix search-paths)
+ (ice-9 match)))
+ (start
+ (let* ((app-manifest (packages->manifest
+ ;; Using python-minimal in the
+ ;; manifest creates collisions with
+ ;; the python in the app package.
+ (list python
+ (gunicorn-app-package app))))
+ (app-profile (profile
+ (content app-manifest)
+ (allow-collisions? #t))))
+ (with-imported-modules
+ (source-module-closure '((guix search-paths)))
+ #~(make-forkexec-constructor
+ (cons*
+ #$(least-authority-wrapper
+ (file-append (gunicorn-configuration-package config)
+ "/bin/gunicorn")
+ #:name (string-append name "-pola-wrapper")
+ #:mappings
+ (cons (file-system-mapping
+ ;; Mapping the app package
+ (source app-profile)
+ (target source))
+ (append
+ ;; Mappings for Unix socket directories
+ (filter-map
+ (lambda (socket)
+ (and (unix-socket? socket)
+ (file-system-mapping
+ (source (socket-dir socket))
+ (target source)
+ (writable? #t))))
+ (gunicorn-app-sockets app))
+ ;; Additional mappings
+ (gunicorn-app-mappings app)))
+ #:preserved-environment-variables
+ (map search-path-specification-variable
+ (manifest-search-paths app-manifest))
+ #:namespaces (delq 'net %namespaces))
+ "--workers" #$(number->string (gunicorn-app-workers app))
+ "--timeout" #$(number->string (gunicorn-app-timeout app))
+ (list
+ #$@(append
+ (append-map (lambda (socket)
+ (list "--bind" socket))
+ (gunicorn-app-sockets app))
+ (append-map
+ (lambda (pair)
+ (list "--env"
+ #~(string-append
+ #$(car pair) "=" #$(cdr pair))))
+ (gunicorn-app-environment-variables app))
+ (gunicorn-app-extra-cli-arguments app)
+ (list (gunicorn-app-wsgi-app-module app)))))
+ #:user #$user
+ #:group #$group
+ #:environment-variables
+ (map (match-lambda
+ ((spec . value)
+ (string-append
+ (search-path-specification-variable spec)
+ "="
+ value)))
+ (evaluate-search-paths
+ (map sexp->search-path-specification
+ '#$(map search-path-specification->sexp
+ (manifest-search-paths app-manifest)))
+ (list #$app-profile)))
+ #:log-file #$(string-append "/var/log/" name ".log")))))
+ (stop #~(make-kill-destructor)))))
+ (gunicorn-configuration-apps config)))
+
+(define gunicorn-service-type
+ (service-type
+ (name 'gunicorn)
+ (description "Run gunicorn.")
+ (extensions (list (service-extension activation-service-type
+ gunicorn-activation)
+ (service-extension shepherd-root-service-type
+ gunicorn-shepherd-services)))
+ (compose concatenate)
+ (extend (lambda (config apps)
+ (gunicorn-configuration
+ (inherit config)
+ (apps (append (gunicorn-configuration-apps config)
+ apps)))))
+ (default-value (gunicorn-configuration))))
+
(define-record-type* <fcgiwrap-configuration> fcgiwrap-configuration
make-fcgiwrap-configuration
fcgiwrap-configuration?