This is the third in a series of articles on working with Elm in Guix. See the first article for an introduction. The outline is:
- packaging the Elm compiler
- developing Guix infrastructure for building Elm apps, and using this to package the Elm reactor, and the full Elm tool
- deploying an Elm web application on Guix System
In part 2 of this series, we ended up with a Guix package which outputs the compiled Javascript of our sample Elm applications:
$ cat puzzle-draw.scm
(define-module (puzzle-draw)
...)
(define-public puzzle-draw-elm
(package
(name "puzzle-draw-elm")
(version "current")
(source "/home/rob/puzzle-draw/web")
(build-system elm-application-build-system)
(arguments
`(#:elm-modules ((("Main.elm") . "web.js")))))
...))
$ guix build -L . puzzle-draw-elm
[...]
/gnu/store/dd3i77nimvp17vgrak14yshd10mfm66c-puzzle-draw-elm-current
$ ls /gnu/store/dd3i77nimvp17vgrak14yshd10mfm66c-puzzle-draw-elm-current
web.js
Below, we’ll figure out how to serve this up on Guix System via nginx:
- Wrap
puzzle-draw-elm
in a packagepuzzle-draw-frontend
that bundles a web server and static assets. - Define a Shepherd service to run that web server.
- Configure the system to hook this server up to nginx, and redeploy it.
A full package for the puzzle-draw web application
puzzle-draw comes with a web server executable that serves a backend API next to static content: src/serve/Main.hs, so we’ll go ahead and package the two together. Alternatively, we might package the static files separately from the backend API, and hook both up to the public-facing web server independently below.
Let’s assume we’ve packaged puzzle-draw
already as a Haskell project. Part 1 goes into details of how to do that with the example of the elm
tool.
(define-public puzzle-draw
(package
(name "puzzle-draw")
(version "current")
(source "/home/rob/puzzle-draw")
(build-system haskell-build-system)
...
(synopsis
"Creating graphics for pencil puzzles")
(description
"puzzle-draw is a library and tool for drawing pencil puzzles using Diagrams.
It aims to provide a utility layer on top of Diagrams to help with drawing arbitrary
puzzles, and to support several specific puzzles types In addition, it includes
functionality for parsing puzzle data from a YAML file format.")
(license license:expat)))
Then we can combine puzzle-draw-elm
and puzzle-draw
by providing a builder script to the trivial build system:
(define-public puzzle-draw-frontend
(package
(name "puzzle-draw-frontend")
(version "current")
(source "/home/rob/puzzle-draw")
(build-system trivial-build-system)
(inputs
`(("puzzle-draw" ,puzzle-draw)
("bash" ,bash)))
(native-inputs
`(("puzzle-draw-elm" ,puzzle-draw-elm)))
(arguments
`(#:modules ((guix build utils))
#:builder
(begin
(use-modules (guix build utils))
(let* ((elm (assoc-ref %build-inputs "puzzle-draw-elm"))
(draw (assoc-ref %build-inputs "puzzle-draw"))
(source (assoc-ref %build-inputs "source"))
(bash (assoc-ref %build-inputs "bash"))
(out (assoc-ref %outputs "out")))
(copy-recursively (string-append source "/static") (string-append out "/static"))
(copy-recursively (string-append source "/tests/examples") (string-append out "/tests/examples"))
(copy-recursively elm (string-append out "/static"))
(install-file
(string-append draw "/bin/servepuzzle")
(string-append out "/bin"))
(setenv "PATH" (string-append (getenv "PATH") ":" bash "/bin"))
(wrap-program (string-append out "/bin/servepuzzle")
`("PUZZLE_DRAW_ROOT" ":" = (,out)))))))
(description #f)
(synopsis #f)
(license #f)
(home-page #f)))
The trivial build system does nothing but run its builder
argument. That’s a Guile script that has access to source, inputs and output directory, as well as any explicitly provide Guile modules.
We provide both puzzle-draw
and puzzle-draw-elm
as inputs, as well as bash
since we’ll generate a wrapper shell script via wrap-program
that will reference it.
Guix always provides us with a Guile interpreter; via the modules
argument, we instruct it to make the (guix build utils
module available to our script.
The script itself then merely copies our various ingredients to the output path, before wrapping servepuzzle
in a shell-script that sets an environment variable to let the server know where to find its static files.
A puzzle-draw Shepherd service
The GNU Shepherd is Guix System’s init
. It’s a Guile daemon originating as dmd
with GNU Hurd.
To define this service within Guix, we define a new service type:
(define-public puzzle-draw-service-type
(service-type
(name 'puzzle-draw)
(extensions
(list (service-extension shepherd-root-service-type
puzzle-draw-shepherd-service)
(service-extension account-service-type
(const %puzzle-draw-accounts))))
(default-value (puzzle-draw-configuration))))
This references a couple of things. The puzzle-draw-configuration
in this case is pure boilerplate, that’s used to pass in our base Guix package puzzle-draw-frontend
while allowing it to be overridden:
(define-record-type* <puzzle-draw-configuration>
puzzle-draw-configuration make-puzzle-draw-configuration
puzzle-draw-configuration?
(puzzle-draw puzzle-draw-configuration-puzzle-draw (default puzzle-draw-frontend)))
Other than that, there are two “service extensions”; these are defined as functions that take the service configuration and return a suitable “extension” of the given service type. In the case of account-service-type
, that’s just a list of groups and accounts that doesn’t care about the configuration (whence const
):
(define %puzzle-draw-accounts
(list (user-group (name "pzldraw") (system? #t))
(user-account
(name "pzldraw")
(group "pzldraw")
(system? #t)
(comment "puzzle-draw server user")
(home-directory "/var/empty")
(shell (file-append shadow "/sbin/nologin")))))
This sets up a dedicated for the puzzle-draw daemon; we could easily have skipped that part and run things as an existing user below.
The other extension is to shepherd-service-type
:
(define puzzle-draw-shepherd-service
(match-lambda
(($ <puzzle-draw-configuration> puzzle-draw-frontend)
(list (shepherd-service
(provision '(puzzle-draw))
(documentation "Run the puzzle-draw daemon.")
(requirement '(user-processes))
(start #~(make-forkexec-constructor
'(#$(logger-wrapper "puzzle-draw" (file-append puzzle-draw-frontend "/bin/servepuzzle")
"-b" "127.0.0.1" "-p" "8765"))
#:user "pzldraw"
#:group "pzldraw"))
(stop #~(make-kill-destructor)))))))
This is mostly just us telling shepherd to run the servepuzzle
executable, whose path we obtain from the puzzle-draw-frontend
field in the configuration.
There’s one extra change we made here, namely wrapping the executable with logger:
(define* (logger-wrapper name exec . args)
"Return a derivation that builds a script to start a process with
standard output and error redirected to syslog via logger."
(define exp
#~(begin
(use-modules (ice-9 popen))
(let* ((pid (number->string (getpid)))
(logger #$(file-append inetutils "/bin/logger"))
(args (list "-t" #$name (string-append "--id=" pid)))
(pipe (apply open-pipe* OPEN_WRITE logger args)))
(dup pipe 1)
(dup pipe 2)
(execl #$exec #$exec #$@args))))
(program-file (string-append name "-logger") exp))
This is required because, by default, Shepherd is happy to let its child processes dump their output to Shepherd’s stdout
, i.e. the console, where it will never be seen. Note that the #~(begin
, #$
, #$@
magic here is the G-expressions version of Scheme quasi-quoting, allowing us to mix host and target code.
Configuring the system
Now to bring our new service live. Our Guix system is defined by an operating-system
record which comes with a list of services:
(operating-system
(host-name "garp")
(timezone "Europe/Berlin")
(locale "en_US.utf8")
...
(services
(append
(list
(static-networking-service "eth0" ...)
(service openssh-service-type ...)
...)
%base-services)))
To set up nginx and puzzle-draw, we add two services:
(service openssh-service-type ...)
(service nginx-service-type
(nginx-configuration
(extra-content
#~(string-append
(string-append " include " #$(local-file "nginx/garp") ";\n")))))
(service puzzle-draw-service-type)
...
The nginx include directive allows us to write a plain nginx fragment, rather than do everything in Guile. Here’s nginx/garp
:
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name garp.vllmrt.net;
ssl_certificate /etc/letsencrypt/live/garp.vllmrt.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/garp.vllmrt.net/privkey.pem;
...
location /puzzles/draw/ {
proxy_pass http://127.0.0.1:8765/;
}
}
Ideally, we’d template this fragment somehow so we don’t have to hard-code the port number in two places, but this works for now.
Finally, we can reconfigure the system:
$ sudo guix system reconfigure config.scm
$ sudo herd restart nginx
Done! Draw some puzzles!