Running a website is not always easy, especially with the complexity of setting up the web server itself.
The Goal
- Run Nginx on Guix System
- Run a website using Nginx on Guix System
- Run a CGit instance on the same server, making git repositories browsable
- Enable HTTPS for our sites
Running Nginx on Guix System
Running an instance of Nginx on Guix System is straightforward, just add it as a service-type.
(use-modules (gnu)
(gnu system))
(use-service-modules web)
(operating-system
...
(services (list (service nginx-service-type))))
Adding this one service starts Nginx with a default configuration that is provided by Guix that does nothing.
The generated nginx.conf
file only contains information about log files, PID files, the user and group to run the server as, information about MIME types, and some redirection paths.
In its current state, Nginx will not listen on any ports and will not respond with anything.
The default Nginx configuration can be found with:
(use-modules (gnu)
(gnu system)
(gnu tests) ;; Provide simple-operating-system macro
(gnu services web))
(simple-operating-system
(service nginx-service-type))
Throughout this post, I use the simple-operating-system
macro to reduce the amount of code you have to read.
An example system is available at the end of this post that has a complete description of our web server.
Running a Website on Guix System
To run a website on Guix system with Nginx, you need to specify an nginx-server-configuration
that adds a server block to the http
block in the generated Nginx configuration.
(use-modules (gnu)
(gnu tests) ;; For simple-operating-system macro
(gnu services web))
(simple-operating-system
(service nginx-service-type
(nginx-configuration
(server-blocks
(list
(nginx-server-configuration))))))
This simple configuration makes Nginx listen on both ports 80 and 443 (HTTP and HTTPS respectively), and serves content from /srv/http
.
You can find the default values in the manual.
Now you just need to create an index.html
file in /srv/http/
, browse to your webserver, and you should see your new webpage!
This post does not attempt to cover every possible configuration option, their values, or their meanings. If you want to further customize your Nginx configuration with all the possible configuration options and their values, you should see the manual.
Make your Whole Site Reproducible
Reproducibility is one of Guix's main tenents, and this is a task that is particularly well-suited to the task, as serving a website amounts to just setting the webserver's webroot
path in the configuration.
For the rest of this post, assume we have a website we want to serve, called sample-website
, which has already been packaged for Guix and builds properly.
(use-modules (gnu)
(guix packages)
(guix build-system copy)
(gnu packages web))
(define sample-website
(package
(name "sample-website")
(version "post")
(source (package-source nginx)) ;; Use Nginx's built-in index.html as "website"
(build-system copy-build-system)
(arguments
;; NOTE: The double ' is to prevent evaluation and is required!
'(#:install-plan
'(("html/" "/"))))
(home-page #f)
(synopsis "Sample \"website\"")
(description "Sample \"website\" for a blog post.")
(license (package-license nginx))))
This simple website copies the contents of the html/
directory from Nginx's source tarball and copies it to our output directory.
If you copy this package recipe to a file, you can build this sample website with:
guix build -f <path/to/sample-website.scm>
The most obvious way to get Nginx to use the build output for our sample-website is to pass it as the root
for that server-configuration
.
(use-modules (gnu)
(gnu system)
(gnu tests) ;; For simple-operating-system macro
(gnu services web))
(simple-operating-system
(service nginx-service-type
(nginx-configuration
(server-blocks
(list
(nginx-server-configuration
(listen '("8080"))
(server-name '("website.com"))
(root sample-website)))))))
In fact, this will do exactly what you want! You can navigate to the server's IP address and port 8080, and you should see Nginx's default webpage. While this is all fine-and-dandy, though there are a few issues we will encounter later with this approach (skip to "Enabling HTTPS" to see the problem and its solution). For now, let's move onto doing something slightly more complicated.
Running CGit on the Same Server
CGit is a web server for Git that allows for browsing code online like one would with GitHub, GitLab, SourceHut, or any other online Git-browsing system. It is one of the Git servers used by the GNU project on Savannah, and by Guix. I personally think it looks nice and it complicates our Nginx setup a little bit more, so let's try setting it up.
Like before, Guix makes it trivial to add and set up cgit:
(use-modules (gnu)
(gnu tests) ;; For simple-operating-system macro
(gnu services networking)
(gnu services cgit))
(simple-operating-system
(service cgit-service-type
(cgit-configuration
(repository-directory "/srv/git/")))
;; We just need some kind of networking for fcgiwrap
(service dhcp-client-service-type))
By default, cgit will create a new HTTP server block that listens on port 80. If you load your machine's website now, it should be a cgit page stating you have no repositories.
Our Website & CGit
Notice how the default ports for our website's nginx-server-configuration
and cgit interfere with each other (they both listen on port 80).
To solve this, we need to use redirection and named servers.
(use-modules (gnu)
(gnu tests) ;; For simple-operating-system macro
(gnu services networking)
(gnu packages version-control) ;; For cgit package
(gnu services cgit))
(simple-operating-system
(service nginx-service-type
(nginx-configuration
(server-blocks
(list
(nginx-server-configuration
(listen '("80"))
(server-name '("website.com"))
(root sample-website)
(locations
(list
;; Redirect website.com/cgit -> cgit.website.com
(nginx-location-configuration
(uri "= /cgit")
(body '("return 308 $scheme://cgit.website.com ;")))
;; Redirect website.com/cgit/repo.git -> cgit.website.com/repo.git
(nginx-location-configuration
(uri "~ /cgit(/.*)")
(body '("return 308 $scheme://cgit.website.com$1 ;"))))))))))
(service cgit-service-type
(cgit-configuration
(repository-directory "/srv/git/")
(nginx
(list
(nginx-server-configuration
(listen '("80"))
(server-name '("cgit.website.com"))
(locations
(list
(nginx-location-configuration ;; So CSS & co. are found
(uri "~ ^/share/cgit/")
(body `(("root " ,cgit ";"))))
(nginx-named-location-configuration
(name "cgit")
(body `(("fastcgi_param SCRIPT_FILENAME " ,cgit "/lib/cgit/cgit.cgi;")
"fastcgi_param PATH_INFO $uri;"
"fastcgi_param QUERY_STRING $args;"
"fastcgi_param HTTP_HOST $server_name;"
"fastcgi_pass 127.0.0.1:9000;"))))))))))
;; We just need some kind of networking for fcgiwrap
(service dhcp-client-service-type))
This is a big block, but all of cgit-configuration
's nginx
field is a reproduction of what cgit's default Nginx configuration is set to.
We must manually provide this entire body because we have no way to "inherit" the old configuration.
The important parts are the locations
in the nginx-configuration
.
These two nginx-location-configuration
s redirect requests made to your default website to your cgit site when website.com
contains /cgit
immediately afterwards.
Like the previous section, The
cgit-service-type
has too many configuration options to cover in this post. For further reading and customization, you should see the manual.
Now that we have configured Nginx to serve both our website and cgit, we should be good system/web admins and enable HTTPS for our sites.
Enabling HTTPS
HTTPS is a requirement for websites today, with any reasonable browser warning users that they are visiting an "Unprotected site" and the lock symbol missing. In the past, it was difficult to get an SSL/TLS certificate to authenticate the ownership of your servers. Today we are fortunate that the EFF created Certbot, a tool for easily creating and managing certificates. Certbot makes it easy to create new SSL/TLS certificates that are trusted by all the major browsers and keep the one you already have renewed and up-to-date.
On traditional Linux systems, enabling HTTPS is fairly straightforward with Certbot.
In our case of using Nginx, you would just use the following command to get a new certificate, install it, and edit the nginx.conf
file to automatically redirect all HTTP requests to your sites to HTTPS requests:
sudo certbot --nginx
But, Guix is not a traditional Linux system, so what does it do?
Certbot on Guix
As documented in Guix's manual, setting up Certbot to run for a website hosted at the URI website.com
is as simple as adding the following lines.
(use-modules (gnu)
(gnu tests) ;; For simple-operating-system macro
(gnu services certbot))
(simple-operating-system
(service certbot-service-type
(certbot-configuration
(certificates
(list
(certificate-configuration
(name "website")
(domains '("website.com"))))))))
The certbot-service-type
does two things:
- It adds an mcron task that runs at a random time within the next twelve hours.
This mcron task will call the
/gnu/store/...-certbot-command
Guile script which, unsurprisingly, runs Certbot. Once the command is runs correctly,/etc/certbot/live/<name>
should exist as a directory, where<name>
is the name you provided in thecertificate-configuration
record. - Adds another HTTP server block to
nginx.conf
that redirects/upgrades all HTTP traffic towebsite.com
to HTTPS.
Now that you have certificates, enabling HTTPS is quite simple.
In the block below, I show a minimized version of the configuration.
In particular, note the ssl-certificate
, ssl-certificate-key
and listen
fields.
(use-modules (gnu)
(gnu tests) ;; For simple-operating-system macro
(gnu services networking)
(gnu services cgit)
(gnu services certbot))
(simple-operating-system
(service certbot-service-type
(certbot-configuration
(certificates
(list
(certificate-configuration
(name "website")
(domains '("website.com")))
(certificate-configuration
(name "cgit")
(domains '("cgit.website.com")))))))
(service nginx-service-type
(nginx-configuration
(server-blocks
(list
(nginx-server-configuration
(listen '("443 ssl"))
(server-name '("website.com"))
(ssl-certificate "/etc/letsencrypt/live/website/fullchain.pem")
(ssl-certificate-key "/etc/letsencrypt/live/website/privkey.pem")
(root sample-website))))))
(service cgit-service-type
(cgit-configuration
(repository-directory "/srv/git/")
(nginx
(list
(nginx-server-configuration
(listen '("443 ssl"))
(server-name '("cgit.website.com"))
(ssl-certificate "/etc/letsencrypt/live/cgit/fullchain.pem")
(ssl-certificate-key "/etc/letsencrypt/live/cgit/privkey.pem"))))))
;; We just need some kind of networking for fcgiwrap
(service dhcp-client-service-type))
Now we only listen on port 443 for SSL/TLS traffic, as certbot-service-type
handles redirecting/upgrading HTTP to HTTPS requests.
We also specify the SSL/TLS certificates we generated with Certbot so that our website can be verified to be ours.
Guix-specific Certbot "Oddities"
On Guix:
- The
certbot-service-type
extends Nginx, which means Nginx gets pulled in. - The
certbot-command
uses the--webroot
flag.
Certbot's documentation about the --webroot
flag means that some other webserver is the one that will response on port 80, in Guix's case, Nginx.
Something to note, which is not stated anywhere, is that if the domain
s you pass in the certificate-configuration
match the server-name
of an nginx-server-configuration
, the default-location
of Certbot (which defaults to <server-webroot>/.well-known/
) is relative to that server's webroot
.
Normally, this is not a problem if the webroot
set to /var/www/<site>
, as this is usually a readable and writable directory for the website, which is required for Certbot to handle the necessary challenge and responses.
Certbot, Nginx, and Guix Package Outputs
If you are like me, you want to have Guix build everything for you, including the website that your new Guix system will be serving.
The quickest and most obvious way is to set the webroot
of the server-configuration
to the output of a Guix package that describes your website.
(use-modules (gnu)
(gnu tests) ;; For simple-operating-system macro
(gnu services web)
(gnu services certbot))
(simple-operating-system
(service nginx-service-type
(nginx-configuration
(server-blocks
(list
(nginx-server-configuration
(listen '("443 ssl"))
(server-name '("website.com"))
;; Set webroot to the output of sample-website
(root sample-website)
(ssl-certificate "/etc/letsencrypt/live/website/fullchain.pem")
(ssl-certificate-key "/etc/letsencrypt/live/website/privkey.pem"))))))
(service certbot-service-type
(certbot-configuration
(certificates
(list
(certificate-configuration
(name "website")
(domains '("website.com"))))))))
This sets webroot
to a path in the store (/gnu/store/...-sample-website/
), which contains your website.
This works fine when serving the website, but is problematic when working with Certbot, which requires its .well-known
directory inside of the webroot
be writable.
To fix this, you will need to set webroot
to any readable and writable directory when generating the certificate, then change it back to the sample-website
later.
This problem presents itself the same way for all Nginx server blocks that match the Certbot
certificate-configuration
, so we omit the cgit configuration for now.
Alternatively, we can create a new Guix service-type that provides a Shepherd service to install the website somewhere readable and writable. Such a Shepherd service can create the website's directory and copy the website's contents from the store to the output directory when started and remove the website when stopped. Combining Guix and Shepherd in this way lets us reproducibly put the built website in a world readable/writable environment. It also allows us to
- Automatically start and copy the website during the system's boot.
- Issue a
herd restart
orherd start
command to refresh the contents of the site. - Update the site upon a
guix system reconfigure
.
(use-modules (gnu)
(gnu tests) ;; For simple-operating-system macro
(gnu services web)
(gnu services certbot)
(gnu services shepherd))
(define website-deploy-service
(let ((website-dir "/srv/http/website.com"))
(simple-service
'website-deploy
shepherd-root-service-type
(list
(shepherd-service
(requirement '(file-systems))
(provision '(website-deploy))
(documentation "Copy website out of store to @file{/srv/http/website.com/}")
(start #~(let ((website-in-store #$sample-website))
(lambda _
(mkdir-p #$website-dir)
;; (guix build utils) already in scope for start by
;; #:modules. See (guix) Shepherd Services
(copy-recursively website-in-store #$website-dir))))
(stop #~(lambda _
(with-exception-handler (lambda (e) (pk 'caught e))
(lambda () (delete-file-recursively #$website-dir))
#:unwind? #t)
#f)))))))
(simple-operating-system
website-deploy-service
(service nginx-service-type
(nginx-configuration
(server-blocks
(list
(nginx-server-configuration
(listen '("443 ssl"))
(server-name '("website.com"))
;; Set webroot to the output of sample-website
(root "/srv/http/website.com")
(ssl-certificate "/etc/letsencrypt/live/website/fullchain.pem")
(ssl-certificate-key "/etc/letsencrypt/live/website/privkey.pem"))))))
(service certbot-service-type
(certbot-configuration
(certificates
(list
(certificate-configuration
(name "website")
(domains '("website.com"))))))))
Conclusion
Guix is an excellent tool for package management and system management! With less than 150 lines of code, we have created a definition for a computer, running Nginx and cgit over HTTPS thanks to Certbot. In addition, we are building our website with Guix, so not only is our system reproducible, but the packages the system's servers depend on are also reproducible!
A Buildable System
(use-modules (gnu)
(guix packages)
(guix build-system copy)
(gnu packages web)
;; For nginx
(gnu tests) ;; For %simple-os
(gnu services web)
;; cgit
(gnu packages version-control) ;; For cgit package
(gnu services networking)
(gnu services cgit)
;; certbot
(gnu services certbot)
;; custom service-type
(gnu services shepherd))
(define sample-website
(package
(name "sample-website")
(version "post")
(source (package-source nginx)) ;; Use Nginx's built-in index.html as "website"
(build-system copy-build-system)
(arguments
;; NOTE: The double ' is to prevent evaluation and is required!
'(#:install-plan
'(("html/" "/"))))
(home-page #f)
(synopsis "Sample \"website\"")
(description "Sample \"website\" for a blog post.")
(license (package-license nginx))))
(define website-deploy-service
(let ((website-dir "/srv/http/website.com"))
(simple-service
'website-deploy
shepherd-root-service-type
(list
(shepherd-service
(requirement '(file-systems))
(provision '(website-deploy))
(documentation "Copy website out of store to @file{/srv/http/website.com/}")
(start #~(let ((website-in-store #$sample-website))
(lambda _
(mkdir-p #$website-dir)
;; (guix build utils) already in scope for start by
;; #:modules. See (guix) Shepherd Services
(copy-recursively website-in-store #$website-dir))))
(stop #~(lambda _
(with-exception-handler (lambda (e) (pk 'caught e))
(lambda () (delete-file-recursively #$website-dir))
#:unwind? #t)
#f)))))))
(operating-system
(inherit %simple-os) ;; Get necessities
(services
(append
(list
website-deploy-service
(service nginx-service-type
(nginx-configuration
(server-blocks
(list
(nginx-server-configuration
(listen '("443 ssl"))
(server-name '("website.com"))
(root "/srv/http/website.com")
(ssl-certificate "/etc/letsencrypt/live/website/fullchain.pem")
(ssl-certificate-key "/etc/letsencrypt/live/website/privkey.pem")
(locations
(list
;; Redirect website.com/cgit -> cgit.website.com
(nginx-location-configuration
(uri "= /cgit")
(body '("return 308 $scheme://cgit.website.com ;")))
;; Redirect website.com/cgit/repo.git -> cgit.website.com/repo.git
(nginx-location-configuration
(uri "~ /cgit(/.*)")
(body '("return 308 $scheme://cgit.website.com$1 ;"))))))))))
(service cgit-service-type
(cgit-configuration
(repository-directory "/srv/git/")
(nginx
(list
(nginx-server-configuration
(listen '("443 ssl"))
(server-name '("cgit.website.com"))
(ssl-certificate "/etc/letsencrypt/live/website/fullchain.pem")
(ssl-certificate-key "/etc/letsencrypt/live/website/privkey.pem")
(locations
(list
(nginx-location-configuration ;; So CSS & co. are found
(uri "~ ^/share/cgit/")
(body `(("root " ,cgit ";"))))
(nginx-named-location-configuration
(name "cgit")
(body `(("fastcgi_param SCRIPT_FILENAME " ,cgit "/lib/cgit/cgit.cgi;")
"fastcgi_param PATH_INFO $uri;"
"fastcgi_param QUERY_STRING $args;"
"fastcgi_param HTTP_HOST $server_name;"
"fastcgi_pass 127.0.0.1:9000;"))))))))))
(service certbot-service-type
(certbot-configuration
(certificates
(list
(certificate-configuration
(name "website")
(domains '("website.com")))
(certificate-configuration
(name "cgit")
(domains '("cgit.website.com")))))))
;; We just need some kind of networking for fcgiwrap
(service dhcp-client-service-type))
%base-services)))