Writing Materia Components

Writing components for fun and little profit
guides
Author

Steve Ryan

Published

January 11, 2026

Intro

I recently found myself in need of a dnsmasq component and while writing it up decided to document my process as sort of a tutorial.

This post was written for the soon-to-be-release 0.5.0 version of Materia, though most if not all of it would also apply to v0.4.x .

We’re going to use dockurr’s container for this, but really dnsmasq is simple enough to package up and deploy that probably any container would work with it.

You can see the resulting component here and even use it in your own repositories, though at your own risk of course.

Creating the directory layout and manifest

First off, let’s pop into my materia repository to create the main component directory and manifest. I like to create the manifest first since a component is never valid without one.

We’ll run all these commands from the root of my repository.

> cd code/materia_repo
> ls
components  secrets  MANIFEST.toml  mise.local.toml  mise.toml

(The mise files are mostly just tasks for easy sops encryption)

Let’s create the folder and manifest file for the dnsmasq component:

> mkdir components/dnsmasq
> touch components/dnsmasq/MANIFEST.toml
> cd components/dnsmasq
> ls
MANIFEST.toml

I like to fill in the manifest last when writing components, so we’ll write up the container file first.

Writing the .container file

We’ll create a dnsmasq.container file first:

components/dnsmasq/dnsmasq.container:

[Container]
Image=docker.io/dockurr/dnsmasq:latest
ContainerName=dnsmasq
PublishPort=53:53/udp
ReloadSignal=SIGHUP
Volume=/etc/dnsmasq:/etc/dnsmasq
AddCapability=NET_ADMIN
AutoUpdate=registry

[Install]
# Needed to make container start at boot
WantedBy=multi-user.target default.target

This is essentially the simplest quadlet we could write: it will create the dnsmasq container and mount the host’s /etc/dnasmasq directory in it. If materia was run with this component right now, the only thing it would do is install this container file as-is to /etc/containers/systemd.

Realistically though we’ll want more than this. We want to supply configuration files as well as the container Quadlet and allow customization in case we need to run it on hosts with differing needs.

We probably also want to have a nice description and restart automatically if it fails.

Let’s convert it into a template and make it more flexible. Replace the dnsmasq.container file with this dnsmasq.container.gotmpl file:

components/dnsmasq/dnsmasq.container.gotmpl:

[Unit]
Description=dnsmasq container
StartLimitIntervalSec=400
StartLimitBurst=3

[Service]
Restart=on-failure
RestartSec=5s

[Container]
Image=docker.io/dockurr/dnsmasq:{{ .containerTag }}
ContainerName=dnsmasq
PublishPort={{ .hostPort }}:53/udp
ReloadSignal=SIGHUP
Volume={{ m_dataDir "dnsmasq" }}/dnsmasq.conf:/etc/dnsmasq.conf:Z
{{- if .useSeperateFiles }}
Volume={{ m_dataDir "dnsmasq" }}/hosts:/etc/hosts:Z
Volume={{ m_dataDir "dnsmasq" }}/ethers:/etc/ethers:Z
{{- end }}
AddCapability=NET_ADMIN
AutoUpdate=registry

[Install]
WantedBy=multi-user.target default.target

See the differences? Here’s a brief summary of what changed:

  1. We added [Unit] and [Service] sections. Like the [Install] section these are normal systemd unit properties that will apply to the generated unit.

  2. We added two simple variables: { .containerTag } and { .hostPort }. These will be templated in with values from your configured attributes vaults, or from the defaults section of the component manifest. Spoilers: We’re going to set them in the component manifest.

  3. We added an if-statement with {- if .useSeperateFiles }; since these are normal Go Templates we can use any feature available there. I’m old fashioned and like having dnsmasq use /etc/hosts and the oft-forgotten /etc/ethers files, but some situations or users might want just the one dnsmasq.conf file.

  4. We used the m_dataDir macro; these are built-in functions for materia that provide useful features. In this case the m_dataDir "component_name" macro resolves to data directory for the component_name, letting us refer to data files in a component without hardcoding references. In this case, it would be /var/lib/materia/components/dnsmasq/.

  5. Finally, we used :Z to get around selinux permissions issues; feel free to skip this step if you don’t use selinux. If you don’t include it you may see errors in the logs about not being able to access the data files.

Now we have a single container file that’s more robust and flexible thanks to systemd and templating. Since there’s no other quadlet’s needed here (i.e. we’re not creating any podman volumes or networks) we’ll move to the data files.

Adding data files

We need to create three data files: dnsmasq.conf, hosts, and ethers. If this component was being designed as a remote component we would make this part with more robust templating, but since that would be an article it’s own right we’re going to be a little lazy and create the following three files as such:

components/dnsmasq/dnsmasq.conf.gotmpl:

{{ .confContents }}

components/dnsmasq/hosts.gotmpl:

{{ .hosts }}

components/dnsmasq/ethers.gotmpl:

{{ .ethers }}

These files will be filled with the contents of their respective attributes.

This setup gives us maximum flexibility with minimum blog post length :)

Writing the component manifest

This brings us to the final file, the component manifest. We need to do two things here: set the default values for all the variables we used and configure what services should be running.

components/dnsmasq/MANIFEST.toml:

[Defaults]
containerTag = "latest"
hostPort = "53"
useSeperateFiles = true
confContents = ""
hosts = ""
ethers = ""

[[Services]]
Service = "dnsmasq.container"
ReloadedBy = ["hosts","ethers","dnsmasq.conf"]

We’ve created two sections here: [Defaults] and [[Services]].

  1. [Defaults] is a table that defines the default attribute values for this component. For example, the containerTag attribute will be templated as “latest” by default, unless overridden somewhere else in your repository (usually as a host attribute). You usually always want to set a default value for attributes, unless you specifically do not want the component to be useable without the user setting one. We’re going to set everything for dnsmasq this time, but an argument can be made for not setting confContents to force the user to provide a configuration.

  2. [[Services]] is an array of service definitions. Materia (unless configured otherwise) starts any systemd units or quadlets defined here as part of the component installation and checks to make sure they’re running whenever it updates. Here we defined the main dnsmasq service by telling it to start the dnsmasq.container quadlet (which it will translate to the proper systemd unit). We also defined the dnsmasq.container service to be reloaded by the hosts, ethers, and dnsmasq.conf resources. That means whenever any of those files are updated, materia will trigger a systemctl reload for the dnsmasq service.

The resulting file layout for the component should look like this:

> ls components/dnsmasq
dnsmasq.conf.gotmpl  dnsmasq.container.gotmpl  ethers.gotmpl  hosts.gotmpl  MANIFEST.toml

That’s it! The dnsmasq component is now complete and ready for use.

Adding it to a host

Just to demonstrate, we’re going to assign this component to my server named viscous. We’ll do this by editing the main MANIFEST.toml file at the root of the repository and adding it to the host config for viscous.

MANIFEST.toml:

[hosts.viscous]
components = ["dnsmasq"]

And that’s all there is to it. The next time materia runs on viscous it will automatically install and template the dnsmasq.container,dnsmasq.conf, hosts, and ethers files onto the host and start the dnsmasq container.