From 0c506e6f526e51d0dd9c69e4141711bc8ab7659c Mon Sep 17 00:00:00 2001 From: Fabio Natali Date: Sun, 8 Feb 2026 11:16:38 +0000 Subject: [PATCH] gnu: services: Add gunicorn-service-type. * gnu/services/web.scm (, ): 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 Change-Id: I3aa970422e6a5d31158b798b1061e6928ad2160b Signed-off-by: jgart --- doc/guix.texi | 108 ++++++++++++++++++++++ gnu/services/web.scm | 209 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 311 insertions(+), 6 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{} 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 ;;; Copyright © 2020, 2022 Ricardo Wurmus ;;; Copyright © 2020 Tobias Geerinckx-Rice -;;; Copyright © 2020, 2025 Arun Isaac +;;; Copyright © 2020, 2025, 2026 Arun Isaac ;;; Copyright © 2020 Oleg Pykhalov ;;; Copyright © 2020, 2021 Alexandru-Sergiu Marton ;;; Copyright © 2022 Simen Endsjø @@ -20,6 +20,7 @@ ;;; Copyright © 2024 Leo Nikkilä ;;; Copyright © 2025 Maxim Cournoyer ;;; Copyright © 2025 Rodion Goritskov +;;; Copyright © 2026 Fabio Natali ;;; ;;; 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 least-authority) - #:use-module (guix packages) - #:use-module (guix records) - #:use-module (guix modules) - #:use-module (guix utils) #: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 search-paths) + #:use-module (guix utils) #: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 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 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 make-fcgiwrap-configuration fcgiwrap-configuration?