(define-module (tw services web) #:use-module (gnu services) #:use-module (gnu services configuration) #:use-module (gnu services certbot) #:use-module (gnu services web) #:use-module (guix gexp) #:use-module (guix records) #:use-module ((srfi srfi-1) #:select (every concatenate)) #:export (%nginx-cert-deploy-hook https-reverse-proxy-service-type https-reverse-proxy-configuration)) (define %nginx-cert-deploy-hook (program-file "nginx-cert-deploy-hook" #~(kill (call-with-input-file "/var/run/nginx/pid" read) SIGHUP))) (define-maybe/no-serialization string) (define (list-of-nginx-location-configurations? thing) (and (list? thing) (every nginx-location-configuration? thing))) (define-configuration/no-serialization https-reverse-proxy-configuration (domains list-of-strings "List of domain names that nginx should proxy requests for.") (destination-port integer "The port number of the service that should be proxied to.") (destination-ip (string "127.0.0.1") "The IP address of the server that should be proxied to. Usually, this should be localhost.") (websocket-uri maybe-string "An nginx URI prefix to which any WebSocket connections should be passed. WebSocket requests to other URIs are not handled.") (extra-locations (list-of-nginx-location-configurations '()) "A list of @code{nginx-location-configuration} records to apply in addition to the default one.")) (define (reverse-proxy-certificate config) (match-record config (domains) (certificate-configuration (domains domains) (deploy-hook %nginx-cert-deploy-hook)))) (define (reverse-proxy-nginx-server config) (match-record config (domains destination-port destination-ip websocket-uri extra-locations) (nginx-server-configuration (listen '("443 ssl http2")) (server-name domains) (ssl-certificate (string-append "/etc/letsencrypt/live/" (car domains) "/fullchain.pem")) (ssl-certificate-key (string-append "/etc/letsencrypt/live/" (car domains) "/privkey.pem")) (server-tokens? #f) (locations `(,(nginx-location-configuration (uri "/") (body `(("proxy_pass http://" ,destination-ip ":" ,(number->string destination-port) ";") ;; For Grafana: https://grafana.com/tutorials/run-grafana-behind-a-proxy/#configure-nginx "proxy_set_header Host $http_host;" ;; Allow large file uploads (for Paperless). "client_max_body_size 100M;"))) ,@(if (maybe-value-set? websocket-uri) (list (nginx-location-configuration (uri websocket-uri) ;; https://nginx.org/en/docs/http/websocket.html (body `(("proxy_pass http://" ,destination-ip ":" ,(number->string destination-port) ";") "proxy_http_version 1.1;" "proxy_set_header Upgrade $http_upgrade;" "proxy_set_header Connection \"upgrade\";")))) '()) ,@extra-locations))))) (define (reverse-proxy-certificates configs) (map reverse-proxy-certificate configs)) (define (reverse-proxy-nginx-servers configs) (map reverse-proxy-nginx-server configs)) (define https-reverse-proxy-service-type (service-type (name 'reverse-proxy) (extensions (list (service-extension nginx-service-type reverse-proxy-nginx-servers) (service-extension certbot-service-type reverse-proxy-certificates))) (default-value '()) (compose concatenate) (extend append) (description "Configure nginx as a reverse proxy proxying external HTTPS requests to another host or a local port over plain HTTP.")))