Skip to content

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 : Workload
lila =
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 Glyph
service 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 Scroll
main =
[ 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:

GlyphKeyComes from
aptPackage { name = "podman" }apt:podmanthe runtime
file /etc/containers/systemd/lila.containerfile:/etc/containers/systemd/lila.containerthe .container quadlet
systemdService { unit = "lila.service" }systemd:lila.servicethe generated unit
six glyphs of Nftables.nftablesBaseapt:nftables, file:/etc/nftables.d, …expose = Internal, which needs a table to add to
file /etc/nftables.d/lila-9663.nftfile:/etc/nftables.d/lila-9663.nftexpose = 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 : Workload
registry =
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 Scroll
planned 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.service

A 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 : Scroll
scaly =
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