Gurp Server
05 November 2025

As of version 1.3.0 Gurp, as well as applying locally stored configuration, can assert state fetched from a central server. There’s no separate server binary, you just fire up gurp server and point it at the directory storing your config files.

$ uname -n
serv-build
$ gurp server --config-dir=/home/rob/work/my-gurp
2025-11-05T22:38:40.998990Z  INFO commands::server: starting Gurp in server mode
2025-11-05T22:38:41.002976Z  INFO server::http: Listening on 0.0.0.0:1867
2025-11-05T22:38:41.003152Z  INFO server::http: Config dir is /home/rob/work/my-gurp

Then tell your client to apply from a server:

# uname -n
serv-ws
# gurp apply --server=serv-build
2025-11-06T12:41:59.468604Z  INFO doers::host: fetching config from
                             http://serv-build:1867/config/serv-ws?server_name=serv-build
2025-11-06T12:41:59.502863Z  INFO doers::host: Configuring host: serv-ws
2025-11-06T12:42:01.012578Z  INFO commands::apply: Run time: 1.544s
2025-11-06T12:42:01.012607Z  INFO commands::apply: resources: 32  changes: 0

Let’s get the caveats out of the way first.

  1. The Janet config is compiled on the server. This is a problem if your config references inspects the local host in any way. There are a couple of little tricks to help though, which we’ll talk about later.
  2. Yes, it’s plain, unauthenticated HTTP. So far as I know, in the whole entire world, Gurp only runs on my internal home-lab network. I don’t need encrypted connections or certificates to prove identities, and I really don’t want to deal with the fuss of implementing it, or even of managing it. I don’t know… run it through Wireguard or something.

Let’s use Gurp to build a Gurp zone. I’ve shown how to create zones lots of times in the past, so we’ll assume the zone exists. Here’s a Gurp server role. All we have to do is wrap the gurp binary in an SMF service, and make a low-privileged user for it to run as.

(user/ensure "gurp"
             :gecos "gurp server user"
             :primary-group "daemon"
             :shell "/bin/false"
             :home-dir "/var/tmp"
             :uid 1867)

(smf/ensure "gurp-server"
            :fmri "sysdef/application/gurp-server"
            :description "config management server"
            :duration "child"
            (smf-method "start"
                        :exec (argcat "/opt/site/bin/gurp"
                                      "server"
                                      "--metrics-to=http://metrics:8428"
                                      "--config-dir=/data/gurp")
                        :timeout 20
                        :user "gurp"
                        :group (this :user :gurp :primary-group)
                        :privileges ["basic" "!proc_session" "!proc_info" "!file_link_any"]))

And let’s check it’s up. (It’s in my DNS.)

$ curl gurp:1867/status
ok

We can bootstrap zones from the server (network config removed for brevity)

(zone/ensure "example-zone"
             :brand "lipkg"
             :clone-from gold-zone
             (zone-bootstrap :server "gurp.lan.id264.net"
                             :hostname "kate-ws"))

And we can set up a cron job to keep the state we defined:

(cron/ensure "run gurp"
             :minute (cron-minutes-from-name (hostname) 10)
             :command (argcat "gurp" "apply" "--server=gurp"))

You might have noticed the server command, like apply, accepts --metrics-to. It emits OpenTelemetry metrics, which my VictoriaMetrics server collects. Here’s the time spent compiling and sending config for clients. It’s not the clearest at this size with no hover legend, but I promise it’s much more meaningful in real life:

Gurp client config request time

Those times are also bucketed into histograms, so we can get a heatmap of compilation times. No interesting outliers I’m afraid:

Gurp config request heatmap

And here’s the average time spent sending plain files back to clients. Gurp average file request time

tags