A service abstraction
golem ships a strongly-typed container library — Quadlet
— so you almost never hand-write a quadlet string. What you do write is a thin
helper that pins the fields your fleet always sets the same way, and calls
workloadGlyphs. That is layer (c): your Emet on top of the shipped library,
which sits on top of the four glyphs. This is exactly
what the real lichess fleet does in Lichess.emet; it
compiles.
A Workload, filled in
A container is a Workload record. image takes a registry, a name, and a tag;
tcp host container is a published port; expose derives the firewall from
those ports. An app listening on 9663, reachable from the internal network:
import Quadlet exposing ( image, tcp, Restart(..), Expose(..), Workload(..), workloadGlyphs )
lila : Workloadlila = Workload { name = "lila" , image = image "docker.io" "lichess-org/lila" "latest" , env = [] , ports = [ tcp 9663 9663 ] , volumes = [] , restart = Always , expose = Internal }workloadGlyphs lila is a List Glyph — nothing golem-special, just data the
agent already understands. Every field is checked: a String where a Port
belongs, or Restart=alwyas, is a compile error.
Wrap the common shape in a helper
Most services on a fleet share a shape: a docker.io image, one port, always restart, internally exposed. Name that shape once and the call sites shrink to the two things that vary:
service : String -> String -> String -> Int -> List Glyphservice name repo tag port = workloadGlyphs (Workload { name = name , image = image "docker.io" repo tag , env = [] , ports = [ tcp port port ] , volumes = [] , restart = Always , expose = Internal })service "lila" "lichess-org/lila" "latest" 9663 is now your fleet’s word for
“an internally-reachable container service.” The library carries the types; your
helper carries the convention.
Use it in a fleet
main : List Scrollmain = [ scroll { name = "app-01" , notifies = [ "golem-nftables.service" ] , glyphs = service "lila" "lichess-org/lila" "latest" 9663 } ]notifies is what loads the firewall rule the helper writes: when any glyph in
the scroll actually changes the host, golem-nftables.service is reloaded once
at the end of the apply.
What it lowers to
service "lila" … 9663 produces ten glyphs — check with
cargo run -q -p emet -- build … --text:
| Glyph | Key | Comes from |
|---|---|---|
aptPackage { name = "podman" } | apt:podman | the runtime |
file /etc/containers/systemd/lila.container | file:/etc/containers/systemd/lila.container | the .container quadlet |
systemdService { unit = "lila.service" } | systemd:lila.service | the generated unit |
six glyphs of Nftables.nftablesBase | apt:nftables, file:/etc/nftables.d, … | expose = Internal, which needs a table to add to |
file /etc/nftables.d/lila-9663.nft | file:/etc/nftables.d/lila-9663.nft | expose = Internal, derived from the port |
The quadlet body and the nftables rule are rendered by the library at compile
time; the agent sees only the final text. The drop-in opens exactly the port the
container publishes — Expose is computed from ports, so the two cannot
drift — and it is a complete file of its own, not a line added to a file
other services share (ADR 0041). Two services on one box repeat the six base
glyphs; identical glyphs share content ids, so the host does that work once.
Add a volume, and a second glyph appears
Give the workload a named volume and the lowering grows a .volume quadlet.
This is the registry (examples/registry/registry.emet):
import Quadlet exposing ( image, tcp, Restart(..), Access(..), Relabel(..) , fromVolume, Expose(..), Workload(..), workloadGlyphs )
registry : Workloadregistry = Workload { name = "registry" , image = image "docker.io/library" "registry" "2" , env = [] , ports = [ tcp 5000 5000 ] , volumes = [ fromVolume "golem-registry-data" "/var/lib/registry" ReadWrite Private ] , restart = Always , expose = Internal }Now eleven glyphs, the extra one being the named volume’s .volume unit:
main : List Scrollplanned scrolls (1): scroll `kaiju` (11 glyphs): * ensure apt package `podman` installed * ensure file `/etc/containers/systemd/golem-registry-data.volume` (mode 0644) * ensure file `/etc/containers/systemd/registry.container` (mode 0600) * enable + start systemd unit `registry.service` * ensure apt package `nftables` installed * ensure directory `/etc/nftables.d` (mode 0755) * ensure file `/etc/golem-nftables.conf` (mode 0755) * ensure file `/etc/nftables.d/00-base.nft` (mode 0644) * ensure file `/etc/systemd/system/golem-nftables.service` (mode 0644) * enable + start systemd unit `golem-nftables.service` * ensure file `/etc/nftables.d/registry-5000.nft` (mode 0644) ↻ kaiju notifies golem-nftables.serviceA fromHost mount would instead emit a directory glyph for its bind-mount
source — see the Quadlet reference for the full mount
story.
Same abstraction, many services
Because service is a function, a whole host is a List.concat of calls:
scaly : Scrollscaly = scroll { name = "scaly" , glyphs = List.concat [ service "lila-http" "lichess-org/lila-http" "latest" 9080 , service "lila-gif" "lichess-org/lila-gif" "latest" 6175 ] }Change the image tag, recompile, re-apply — only the changed glyphs get new content ids and are replaced. See Applying changes.
Where to next
- Every
Quadlettype and its lowering: The Quadlet library - The full multi-host version, with a hand-rolled ingress too: A tour of the lichess fleet
- The lowest level — a pattern built from
fileandlineInFiledirectly: A maintenance page