<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>Materia</title>
<link>https://primamateria.systems/blog/</link>
<atom:link href="https://primamateria.systems/blog/index.xml" rel="self" type="application/rss+xml"/>
<description></description>
<generator>quarto-1.11.9</generator>
<lastBuildDate>Wed, 21 Oct 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>0.8.0 Release</title>
  <dc:creator>Steve Ryan</dc:creator>
  <link>https://primamateria.systems/blog/2026-10-21-0.8.0-release.html</link>
  <description><![CDATA[ 






<p><a href="https://github.com/stryan/materia/releases/tag/v0.8.0">Materia v0.8</a> has been released! This was a very busy summer so not as many user-facing features were added with this release, but the internal core is always improving.</p>
<p>Still, that doesn’t mean you won’t see anything new! Read on for the highlights, or skip directly to the changelog</p>
<section id="more-useful-facts-generation" class="level1">
<h1>More useful facts generation</h1>
<p>Materia now gathers the facts it needs to generate plans in a much more standardized manner: Facts are gathered by <code>collectors</code> and organized into <code>namespaces</code>. The number of built in facts have also been expanded to cover information like “what components are installed” and “what was the last source revision applied and was it successful”?</p>
<p>Existing facts, such as the current <code>hostname</code> and network <code>interface</code>(s) available have been moved into the <code>host</code> namespace.</p>
<p>The following facts are now available:</p>
<p><code>host.hostname</code>: The hosts reported (not configured in Materia) hostname</p>
<p><code>host.interfaces</code>: All network interfaces on the host and their IPv4/IPv6 addresses</p>
<p><code>materia.hostname</code>: The configured hostname, if any</p>
<p><code>materia.roles</code>: What roles the host has</p>
<p><code>materia.assigned_components</code>: What components are assigned to the host</p>
<p><code>materia.installed_components</code>: What components are already installed</p>
<p><code>materia.last_revision</code>: The last source revision used in an update operation</p>
<p><code>materia.last_run</code>: When the last update opperation occured</p>
<p><code>materia.last_run_successful</code>: Whether the last update operation was successful.</p>
<p>As always, these facts are accessible via the <code>materia facts</code> commands or the <code>facts</code> Varlink call for the server. The next step for fact generation is to allow custom external fact collectors.</p>
<p>As an internal note, anything gathered as a fact is now gathered only at the start of a Materia plan. For most users this shouldn’t affect anything, but it does make plan generation much more deterministic, so if you had issues with, say, network interfaces coming and going during plan generation that should happen no more!</p>
</section>
<section id="consistent-output-data" class="level1">
<h1>Consistent Output Data</h1>
<p>Those new source revision facts didn’t come from nowhere: Materia now saves output data on each plan and run to <code>$MATERIA_DIR/output</code> for your future perusal and consumption by external tools.</p>
<p>Each <code>materia plan</code> and <code>materia update</code> operation, whether via command line or server mode, will now generate a <code>lastplan.toml</code> and <code>lastrun.toml</code> file, respectively. Up to five instances (customizable in the future) of each will be saved; older files will be rotated to <code>filename-x.toml</code>. For example, if <code>lastrun.toml</code> already exists in the output directory, the next run will be saved as <code>lastrun.toml</code> and the existing run will become <code>lastrun-1.toml</code>.</p>
<p>These files are parsed by materia to generate the <code>last_revision/last_run/</code>/etc facts, so be aware that if you delete them you won’t have those facts until the next run.</p>
</section>
<section id="server-mode-improvements" class="level1">
<h1>Server mode improvements</h1>
<p>Server mode continues to get many improvements to bring it in-line with the existing CLI <code>materia plan-materia upate</code> loop.</p>
<ul>
<li>Server mode now uses the exact same internal calls as the CLI; no more strange mismatches between the two!</li>
<li>Server mode webhook management has been expanded:
<ul>
<li>You can now pass the security token as a <code>Bearer: token</code> instead of in the <code>/update</code> payload</li>
<li>A new <code>/facts</code> endpoint that you can <code>GET</code>. This is useful for validating current deployment status</li>
<li>There’s now a <code>/health</code> endpoint that gets automatically activated when any other endpoint is activated</li>
</ul></li>
<li>Rollback now works the same for server mode as it does for the CLI. Note: this is still dependent on individual source support.</li>
</ul>
<p>The Varlink API has also been updated to match the new <code>facts</code> command line arguments.</p>
</section>
<section id="file-management-improvement" class="level1">
<h1>File management improvement</h1>
<p>Materia can now manage the permissions of files it manages, such as its Quadlet and data files. This is included along with many other changes<sup>1</sup> that should help prevent any path escapes from happening during Materia execution.</p>
<p>If you haven’t been running latest, this will be visible as a big wave of <code>Update</code> actions being taken. This will only happen once as Materia fixes all the file permissions that lazily got installed incorrectly.</p>
</section>
<section id="a-new-release-pipeline" class="level1">
<h1>A new release pipeline</h1>
<p>Materia releases have been re-organized into binary tarballs instead of raw binaries and are now signed via Cosign upon CI release. Release images are also signed via Cosign; please note that as <code>:stable</code> just points to the latest tagged release, it does not have its own signed image digest.</p>
<p>The public key for verifying the signed releases can be found in the repository root and on this website’s <a href="https://primamateria.systems/cosign.pub">root</a>.</p>
<p>The new binary tarballs also include the README and generated man pages for your convenience.</p>
<section id="other-changes-you-should-know" class="level2">
<h2 class="anchored" data-anchor-id="other-changes-you-should-know">Other Changes You Should Know:</h2>
</section>
<section id="public-packages" class="level2">
<h2 class="anchored" data-anchor-id="public-packages">Public Packages</h2>
<p>For a while now most of the Go packages used within Materia were public; this was to make it easier to make tools like <a href="https://github.com/stryan/athanor">athanor</a> that re-use the Materia component manifest format.</p>
<p>At this point, all of the internal packages used in Materia are now available for use by other Go projects. This includes how it defines resources and components, plan generation and execution, source definitions, etc. The plan is for these packages API to stabilize along with CLI version 1.0<sup>2</sup>.</p>
</section>
<section id="git-source-improvements" class="level2">
<h2 class="anchored" data-anchor-id="git-source-improvements">Git source improvements</h2>
<p>The <code>git</code> source now supports using <code>Bearer</code> authentication tokens for login via HTTP clones via <code>git.auth_token</code>. It can also detect if your local source cache has gotten corrupted and will redownload it.</p>
</section>
<section id="materia-image-changes" class="level2">
<h2 class="anchored" data-anchor-id="materia-image-changes"><code>materia</code> image changes</h2>
<p>The <code>materia</code> image is now built from <code>podman/stable</code> instead of <code>tumbleweed:bci</code>. There should be no user-facing differences from this change (except for a small image size) but please let us know if there’s any problems.</p>
</section>
</section>
<section id="changelog" class="level1">
<h1>Changelog</h1>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode default code-with-copy"><code class="sourceCode default"><span id="cb1-1">- feat: most materia packages are now public. This will be stabilized and documented better in 0.9.</span>
<span id="cb1-2">    - This may also fix some bugs in server mode that (hopefully) no-one has noticed</span>
<span id="cb1-3">- feat: git repos can reset on corrupted HEAD</span>
<span id="cb1-4">- feat: git source supports `git.auth_token` for `Bearer: &lt;token&gt;` HTTP auth</span>
<span id="cb1-5">- refactor: rewrote file management backend</span>
<span id="cb1-6">    - Files are now installed with consistent permissions and using `os.Root` to prevent symlink/path escaping</span>
<span id="cb1-7">    - You may notice an initial wave of `Update` actions as materia fixes installed file permissions. Note that changing permissions does count as a normal update action, so all triggered effects (container restarts,etc) will still occur</span>
<span id="cb1-8">- feat/refactor: The `materia` container image is now built from `podman/stable` instead of `tumbleweed/bci:latest`</span>
<span id="cb1-9">- ci: release tarballs and images are now signed via cosign. Public key for validation is in the repo and the main primamateria.systems website</span>
<span id="cb1-10">- feat/rework: Redid `facts` generation. Facts are now denoted in dotted-path format.</span>
<span id="cb1-11">    - Existing facts and their new versions:</span>
<span id="cb1-12">        - `hostname` -&gt; `materia.hostname`</span>
<span id="cb1-13">        - `interface` -&gt; `host.interfaces`</span>
<span id="cb1-14">    - New facts</span>
<span id="cb1-15">        - `host.hostname`: hostname as the OS sees its</span>
<span id="cb1-16">        - `materia.roles`: Assigned roles</span>
<span id="cb1-17">        - `materia.assigned_components`: Assigned components</span>
<span id="cb1-18">        - `materia.installed_components`: Installed components</span>
<span id="cb1-19">- feat: generate a $OUTPUTDIR/lastrun.toml file on `materia update` or `materia server update`. Up to five last-plan and last-run files are stored.</span>
<span id="cb1-20">    - If you have existing `lastrun.toml` files, you need to remove them before running `materia facts`.</span>
<span id="cb1-21">- feat: facts `materia.output` now shows last applied revision and timestamp and whether it was succesful:</span>
<span id="cb1-22">    - New facts:</span>
<span id="cb1-23">        - `materia.last_revision`</span>
<span id="cb1-24">        - `materia.last_run`</span>
<span id="cb1-25">        - `materia.last_run_succesful`</span>
<span id="cb1-26">- feat: server mode supports new `server.token` for `Bearer: token` style authentication:</span>
<span id="cb1-27">    - this replaces the now deprecated `server.update_secret`.</span>
<span id="cb1-28">- feat: other server config changes:</span>
<span id="cb1-29">    - `server.url`: replaces `server.update_url`</span>
<span id="cb1-30">    - `server.update_webhook` requires `server.update_secret` or `server.token`</span>
<span id="cb1-31">    - `update_secret` or `token` checks are disabled by setting `server.url` to `localhost*`</span>
<span id="cb1-32">- feat: new `/facts` endpoint for server mode. Enable with `server.facts_endpoint`  Requires `server.token` to be enabled.</span>
<span id="cb1-33">- feat: new `/health` endpoint, automatically enabled when any other webhook is enabled or if `server.health` is `true`.</span>
<span id="cb1-34">- bugfix: `lastplan`/`lastrun` files can no longer include un-redacted podman secrets. Thanks shirome on Matrix!</span>
<span id="cb1-35">    - The planner no longer contains Podman secrets contents at all, even in memory, so this should not happen again</span>
<span id="cb1-36">- feat: server and cli use same internal commands. Server now supports rollback</span>
<span id="cb1-37">- chore: upgrade to Go 1.27.3</span>
<span id="cb1-38">- feat/chore: rework `materia validate` to use new facts system</span></code></pre></div></div>
</section>
<section id="deprecations" class="level1">
<h1>Deprecations</h1>
<ol type="1">
<li>The <code>file</code> source has been renamed to <code>local</code> to prevent settings collisions with the <code>file</code> attributes engine. Setting the <code>SOURCE_KIND</code> to <code>file</code> will work until after <code>0.8</code> is released. This is a straight rename; no other settings to be adjusted besides replacing <code>file</code> with <code>local</code>.</li>
<li>The following server settings are deprecated:
<ul>
<li><code>server.update_url</code>: replaced with <code>server.url</code></li>
<li><code>server.update_secret</code>: replaced with <code>Bearer token</code> auth with <code>server.token</code></li>
</ul></li>
<li>The following facts are deprecated:
<ul>
<li><code>hostname</code>: replaced with <code>host.hostname</code></li>
<li><code>interface</code>: replaced with <code>host.interfaces</code></li>
</ul></li>
</ol>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Fun fact for the day: the file management code, along with the aforementioned facts generation code, was one of the oldest bits of the project! It’s basically sat almost untouched (outside of shuffling the file its in around) since the project started two years ago.↩︎</p></li>
<li id="fn2"><p>This isn’t a hard commitment; I’m not willing to let the API hold back the much-more-likely-to-be-used CLI if it would be a big deal.↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>release</category>
  <guid>https://primamateria.systems/blog/2026-10-21-0.8.0-release.html</guid>
  <pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>0.7.0 Release</title>
  <dc:creator>Steve Ryan</dc:creator>
  <link>https://primamateria.systems/blog/2026-06-16-0.7.0-release.html</link>
  <description><![CDATA[ 






<p><a href="https://github.com/stryan/materia/releases/tag/v0.7.0">Materia v0.7</a> has been released! This release contains many new features and QoL improvements, plus lays the ground work for more improvements in the future.</p>
<p>As always, here’s a bit of a more detailed over view of the bigger changes. If you just want the changelog, check it out here.</p>
<section id="rollback-on-failed-updates" class="level1">
<h1>Rollback on failed updates</h1>
<p>0.7.0 adds the first steps for fullly self healing quadlets. By setting the new <code>rollback.kind</code> setting to <code>service</code> in your config, materia will now detect when an update results in one or more services in the <code>failed</code> state and will “rollback” its local source cache to the state before the update, running a new plan-execute cycle along the way. This will also send a “Rollback initated” message to a notification webhook, if configured.</p>
<p>This service healthcheck occurs at two points during an update: when the service’s state is changed (i.e.&nbsp;when materia runs <code>systemctl start foo.service</code>) and at the end of the execute portion, where Materia will verify all adjusted services are in the state they should be.</p>
<p>Currently this only works with the <code>git</code> source type, but I plan for <code>oci</code> support to be added in a point release or two once any initial bugs are ironed out.</p>
<p>As some of you may have guessed from this getting its own <code>[rollback]</code> configuration block, I plan on expandeding this feature in the future with multiple types of rollback healthchecks, support for using this in server mode, and different rollback types, such as migrating volume state.</p>
<p>This has been one of the most asked about features on the <code>roadmap.md</code> for a while now. In fact, I think this is the only feature asked for by multiple independent people; this shows both how much people want it and the variety of ways people use Materia :)</p>
</section>
<section id="server-mode-improvements" class="level1">
<h1>Server mode improvements</h1>
<p>The <code>materia server</code> command has been given a little refresh this update.</p>
<p>The biggest change is server mode now starts a <a href="https://varlink.org/">Varlink</a>-compatible server on its listening socket. This replaces the existing bespoke command setup, and was done for three reasons:</p>
<ol type="1">
<li>systemd is moving to Varlink as its IPC system and Materia strives to be compatible with it</li>
<li>Varlink makes it easy to expose the same API both locally and over HTTP with <a href="https://github.com/systemd/varlink-http-bridge">varlink-http-bridge</a></li>
<li>Having a standardized protocol should hopefully make it easier for you to automate Materia in more expansive ways.</li>
</ol>
<p>You can use the <code>materia agent</code> command to try it out, or introspect the socket directly with <code>varlinkctl</code>.</p>
<p>Additionally, server mode now supports starting a <code>/webhook</code> endpoint for triggering <code>materia update</code> on command with a JSON payload. Server mode also now uses the new <code>notify</code> substem for sending alerts, keeping it in line with the rest of Materia</p>
</section>
<section id="instanced-components" class="level1">
<h1>Instanced Components</h1>
<p>Systemd supports templating service units by naming them with the format <code>servicename@</code> and running them with <code>systemctl start servicename@foo</code>. This is useful for running multiple instances of the same server with different configurations. For example <code>web-server@site1</code> and <code>web-server@site2</code> use the same <code>web-server@</code> unit file.</p>
<p>You can now use Materia components in the same way; simply assign the component to the host/role with the same <code>component@instance</code> format as systemd expects. Materia will then detect any <code>name@</code> style resources and install them with the proper instance variable like <code>name@instance</code> instead, allowing them to be used as systemd instanced services.</p>
<p>This also applies to attribute expansion: Materia will search for both <code>component</code> and <code>component@instance</code> attributes at run time, with the latter overriding the former.</p>
</section>
<section id="other-changes-you-should-know" class="level1">
<h1>Other Changes You Should Know</h1>
<section id="drop-in-files" class="level2">
<h2 class="anchored" data-anchor-id="drop-in-files">Drop-in files</h2>
<p>A little know Quadlet fact: much like with systemd services, you can override Quadlet settings by placing a <code>quadlet.container.d/00-override.conf</code> drop-in file next to the Quadlet file. Systemd will combine the two files together, allowing you to modify Quadlet services without editing the actual file. Materia now correctly ignores these drop-in files, allowing you to edit Quadlets in components locally without worrying about Materia overriding your changes next update. This is now officially the supported way of doing so.</p>
</section>
<section id="podman-bindings" class="level2">
<h2 class="anchored" data-anchor-id="podman-bindings">Podman bindings</h2>
<p>Materia now directly uses the <a href="https://github.com/podman-container-tools/podman/tree/main/pkg/bindings">Podman bindings</a> package for its interactions with Podman. This replaces the previous method of wrapping the Podman command (done to make it eaiser to build Materia on multiple platforms) and should provide better integration in the future.</p>
<p>The old Podman command wrapper will stick around for a point release or two but will be removed before 0.8.</p>
</section>
<section id="service-strictness" class="level2">
<h2 class="anchored" data-anchor-id="service-strictness">Service strictness</h2>
<p>As part of the Rollback changes, Materia is now much stricter about checking service status at the end of an update run. You may notice more failures reported for e.g.&nbsp;services that start up correctly but fail soon after start.</p>
<p>If you see more failures or other improper states when services are starting correctly, please open a discussion or leave a message on the <a href="https://matrix.to/#/#materia:saintnet.tech">Matrix channel</a>.</p>
</section>
</section>
<section id="changelog" class="level1">
<h1>Changelog</h1>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode default code-with-copy"><code class="sourceCode default"><span id="cb1-1">- feat: Components with instanced systemd units (i.e. `unit@.service`) can now be instanced at the component level</span>
<span id="cb1-2">- refactor: removed `autoUpdate` snippet, added `onBoot` and `harden` snippets.</span>
<span id="cb1-3">- feat: Added `m_quadletDir`, `m_outputDir`, `m_scriptsDir`, `m_serviceDir` macros</span>
<span id="cb1-4">- feat: simplified volume compression. `containers.compression_command` and `containers.compression_suffix` have been simplified into one `containers.compression` setting that can be blank, `gzip`, or `zstd`</span>
<span id="cb1-5">- refactor/note: volume dump file are now in the format `volumename-volume.tar(.gz/zst)`.</span>
<span id="cb1-6">- feat: `PreCommand` and `PostCommand` component manifest settings for running scripts before or after an update. Experimental.</span>
<span id="cb1-7">- feat: Remote components can specify a `subpath` to use.</span>
<span id="cb1-8">- tests: new testcontainers based setup to replace virter tests</span>
<span id="cb1-9">- feat: `clean` command now attempts to gracefully remove components. Skippable with `--force`.</span>
<span id="cb1-10">- feat: server mode can now listen on a webhook for sync/update requests.</span>
<span id="cb1-11">    - `server.webhook` is now `server.notify_webhook`, though see also the new `notify` config block</span>
<span id="cb1-12">    - `server.update_webhook` = true|false</span>
<span id="cb1-13">    - `server.update_secret` = "shared secret"</span>
<span id="cb1-14">    - `server.update_url` = URL to listen on. Default `:6284/webhook`</span>
<span id="cb1-15">- feat: `lock` setting causes materia to create a dbus or file lock, preventing multiple instances from running destructive actions.</span>
<span id="cb1-16">- feat: `materia server` now exposes a varlink api over its socket</span>
<span id="cb1-17">- feat: `materia agent` now uses new varlink api</span>
<span id="cb1-18">- refactor: materia now uses native podman bindings instead of wrapping the `podman` command. If you experience issues with this, use the `MATERIA_PODMAN_COMMAND` flag to restore the old behaviour.</span>
<span id="cb1-19">- feat: quadlet drop-ins are now left-unchanged by `materia update`.</span>
<span id="cb1-20">- feat: new service resource config setting: `Oneshot`.</span>
<span id="cb1-21">- feat: experimental rollback on failure support. Enable with `rollback.kind = "service"`</span>
<span id="cb1-22">    - service state health checks have been rewritten</span>
<span id="cb1-23">    - Currently supports git source, OCI coming post 0.7.0</span>
<span id="cb1-24">    - If a materia update leads to a failed service or a service is in the wrong state at the end of the update, checkout the last known git commit and run the update again</span>
<span id="cb1-25">- feat: `notify` config block for configuring all webhook notifications in one place</span>
<span id="cb1-26">    - Set `notify.triggers.update` to replace `server.notify_webhook`</span>
<span id="cb1-27">    - Get notifications when rollback occurs with `notify.triggers.rollback`</span>
<span id="cb1-28">    - `server.notify_webhook` is now deprecated and will be removed in a future release, server mode uses same notification code as everywhere else.</span>
<span id="cb1-29">- refactor: removed `source.URL` guessing. `source.URL` is now passed directly to the sources config.</span></code></pre></div></div>
</section>
<section id="deprecations" class="level1">
<h1>Deprecations</h1>
<ol type="1">
<li><p><code>{ snippet "autoUpdate" }</code> has been removed; there was little purpose for it with <code>AutoUpdate=</code> already existing.</p></li>
<li><p>Materia will no longer attempt to guess the source type from <code>source.URL</code>. You now need to specify the source type with <code>source.kind</code>.</p></li>
</ol>


</section>

 ]]></description>
  <category>release</category>
  <guid>https://primamateria.systems/blog/2026-06-16-0.7.0-release.html</guid>
  <pubDate>Tue, 16 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The Many Uses of Quadlets: Static Site Automation</title>
  <dc:creator>Steve Ryan</dc:creator>
  <link>https://primamateria.systems/blog/2026-06-03-many-uses-of-quadlets.html</link>
  <description><![CDATA[ 






<p>Today we’re going to look at a slightly unorthodox usage of Quadlets. Often Quadlets are just used as a way to run long running server processes in containers, but there’s nothing stopping you from treating them like individual units of work.</p>
<p>For this post, I’ll be showing off a little static site deployment pipeline I wrote recently that’s installed with Materia and uses Quadlets and Systemd to orchestrate the individual steps.</p>
<p>The basic premise is this: I have a static website stored in a private Github repository. The repo uses <code>git-lfs</code> to store some particularly large blobs, and thus I need that on whatever host I want to deploy on the site on. However, most of my servers run <a href="https://microos.opensuse.org/">OpenSUSE MicroOS</a> and I don’t want to have to add <code>git-lfs</code> to the base image of every single one just for this. So instead, I’m going to automatically build an image on the host that has the Git dependencies and secrets I need and use that to download the site automatically.</p>
<section id="the-premise" class="level1">
<h1>The Premise</h1>
<p>The quick bullet list:</p>
<ul>
<li>I have a static website in a private repo on Github</li>
<li>I need git-lfs to clone it</li>
<li>I don’t want to install anything on my MicroOS server since that would require a transactional upgrade</li>
<li>BONUS I already have caddy running on the server and it has <code>/srv/www</code> <sup>1</sup> already bind mounted.</li>
</ul>
<p>That last part about Caddy is optional; I could very easily deploy it as part of this pipeline as well but I happened to have it already running for my other static sites.</p>
</section>
<section id="building-on-the-host" class="level1">
<h1>Building on the host</h1>
<p>To download the website onto the host I need two things: a script that runs the git commands and an image with <code>git</code> and <code>git-lfs</code> in it.</p>
<p>Fortunately, there’s a Quadlet type that fits my exact need: <a href="https://docs.podman.io/en/latest/markdown/podman-build.unit.5.html"><code>.build</code> units</a> are Quadlets that build a Podman image on the host. You can think of them as declarative <code>podman build</code> commands in a way.</p>
<p>We need an environment to grab the site in. We need two tools: <code>git</code> and <code>git-lfs</code>, plus an SSH key to clone it with.</p>
<p>First off, here’s the script that I’m going to use to clone the website:</p>
<p>Sync script <code>sync_sh</code><sup>2</sup>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/bin/sh</span></span>
<span id="cb1-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">set</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-eu</span></span>
<span id="cb1-3"></span>
<span id="cb1-4"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${REPO_URL</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:?</span>REPO_URL environment variable is required<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb1-5"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">${SYNC_DEST</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:?</span>SYNC_DEST environment variable is requires<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb1-6"></span>
<span id="cb1-7"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">[</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">-d</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$SYNC_DEST</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/.git"</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">]</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">then</span></span>
<span id="cb1-8">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Updating"</span></span>
<span id="cb1-9">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-C</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$SYNC_DEST</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> fetch <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--all</span></span>
<span id="cb1-10">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-C</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$SYNC_DEST</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> reset <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--hard</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"@{u}"</span></span>
<span id="cb1-11">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-C</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$SYNC_DEST</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> lfs pull</span>
<span id="cb1-12"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span></span>
<span id="cb1-13">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Cloning"</span></span>
<span id="cb1-14">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> clone <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$REPO_URL</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$SYNC_DEST</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb1-15">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-C</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$SYNC_DEST</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> lfs pull</span>
<span id="cb1-16"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fi</span></span>
<span id="cb1-17"></span>
<span id="cb1-18"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Sync complete: </span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">$(</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">date</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-u</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">)</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span></code></pre></div></div>
<p>Since this is running fully automated I’m using <code>git fetch</code> and <code>git reset</code> to ignore any local changes that might get made by accident. This script gets added to the final image below.</p>
<p>Next we have the <code>Containerfile</code>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode dockerfile code-with-copy"><code class="sourceCode dockerfile"><span id="cb2-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span> alpine:latest</span>
<span id="cb2-2"></span>
<span id="cb2-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">apk</span> add <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--no-cache</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-4">    git <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-5">    git-lfs <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-6">    openssh-client</span>
<span id="cb2-7"></span>
<span id="cb2-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> lfs install</span>
<span id="cb2-9"></span>
<span id="cb2-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> /srv/www</span>
<span id="cb2-11"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mkdir</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-p</span> ~/.ssh <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ssh-keyscan</span> github.com <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> ~/.ssh/known_hosts</span>
<span id="cb2-12"></span>
<span id="cb2-13"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">COPY</span> sync_sh /usr/local/bin/sync.sh</span>
<span id="cb2-14"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">RUN</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">chmod</span> +x /usr/local/bin/sync.sh</span>
<span id="cb2-15"></span>
<span id="cb2-16"></span>
<span id="cb2-17"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">ENTRYPOINT</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"/usr/local/bin/sync.sh"</span>]</span></code></pre></div></div>
<p>A very simple image! We could even do everything in the container and not embed a separate script, but this is easier to maintain. The one adjustment I’d like to make later is to not set up <code>known_hosts</code> in the image since that could get out of date quickly, but this seemed like an easy duck-tape for now.</p>
<p>Finally, we need a <code>.build</code> Quadlet to automate the image building process.</p>
<p><code>site-builder.build.gotmpl</code></p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode ini code-with-copy"><code class="sourceCode ini"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Build]</span></span>
<span id="cb3-2"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">File</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">Containerfile</span></span>
<span id="cb3-3"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">ImageTag</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">localhost/site-builder:latest</span></span>
<span id="cb3-4"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">SetWorkingDirectory</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">{{ m_dataDir "staticsite" }} # evaluates to /var/lib/materia/components/staticsite</span></span></code></pre></div></div>
<p>Since this is being deployed with Materia I’m using the <code>m_dataDir</code> macro to keep things dynamic, but there’s nothing stopping you from hard-coding the value.</p>
<p>That takes care of actually downloading the static site and building the image. Now I need to make it run on a regular basis.</p>
</section>
<section id="automating-the-build" class="level1">
<h1>Automating the build</h1>
<p>I need two things for this: a container quadlet and a timer</p>
<p>The <code>site-updater.container.gotmpl</code> quadlet:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode ini code-with-copy"><code class="sourceCode ini"><span id="cb4-1"></span>
<span id="cb4-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Unit]</span></span>
<span id="cb4-3"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Description</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">static site updater</span></span>
<span id="cb4-4"></span>
<span id="cb4-5"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Service]</span></span>
<span id="cb4-6"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Type</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">oneshot</span></span>
<span id="cb4-7"></span>
<span id="cb4-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Container]</span></span>
<span id="cb4-9"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Image</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">site-builder.build</span></span>
<span id="cb4-10"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Volume</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/srv/www:/srv/www:z</span></span>
<span id="cb4-11"></span>
<span id="cb4-12"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Secret</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">materia-github-ssh,type=mount,target=/root/.ssh/id_rsa,mode=0600</span></span>
<span id="cb4-13"></span>
<span id="cb4-14"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Environment</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">REPO_URL={{ .repoURL }}</span></span>
<span id="cb4-15"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Environment</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SYNC_DEST={{ .dest }}</span></span></code></pre></div></div>
<p>The key here is having the <code>Image=</code> setting be the build unit directly instead of the resulting image. This tells the Quadlet systemd-generator that the resulting service file for the container needs the image built first, automatically adding <code>Wants=site-builder</code> and <code>Requires=site-builder</code> as service dependencies.</p>
<p>I’m mounting a Podman secret into the container with the <code>Secret=</code> option<sup>3</sup>. This will mount whatever data is stored in the <code>materia-github-ssh</code> secret as a file in the container. I’m also once again we’re using Materia’s templating support to dynamically insert the <code>REPO_URL</code> and <code>SYNC_DEST</code> values, but there’s nothing stopping you from hard coding them.</p>
<p>I manually specified it as a one-shot service so we don’t intend for it to be running all the time, only once to run the embedded script.</p>
<p>Finally, I need a timer to automatically run the container once a day.</p>
<p><code>site-updater.timer</code>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode ini code-with-copy"><code class="sourceCode ini"><span id="cb5-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Unit]</span></span>
<span id="cb5-2"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Description</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">Static Site daily update</span></span>
<span id="cb5-3"></span>
<span id="cb5-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Timer]</span></span>
<span id="cb5-5"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">RandomizedDelaySec</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1800</span></span>
<span id="cb5-6"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">OnCalendar</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">*-*-* 16:00:00</span></span>
<span id="cb5-7"></span>
<span id="cb5-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Install]</span></span>
<span id="cb5-9"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">WantedBy</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">timers.target</span></span></code></pre></div></div>
<p>This is pretty standard timer. Since <code>.container</code> Quadlets get converted into <code>.service</code> units I don’t need to specify it with <code>Unit=</code> here.</p>
</section>
<section id="tying-it-all-together" class="level1">
<h1>Tying it all together</h1>
<p>Since I’m deploying this as a Materia <a href="../documentation/latest/components.html">component</a> we naturally have a <code>MANIFEST.toml</code> file to describe how its deployed:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode toml code-with-copy"><code class="sourceCode toml"><span id="cb6-1"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Secrets</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">[</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"github-ssh"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb6-2"></span>
<span id="cb6-3"></span>
<span id="cb6-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[[Services]]</span></span>
<span id="cb6-5"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Service</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"site-updater.timer"</span></span>
<span id="cb6-6"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Static</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">true</span></span></code></pre></div></div>
<p>This manifest does two things:</p>
<ol type="1">
<li>It declares that the <code>github-ssh</code> attribute should be stored as a Podman secret on the host (and thus made available in the Container with the above <code>Secret=</code> line)</li>
<li>The <code>site-updater.timer</code> should be enabled and started.</li>
</ol>
<p>Both of these steps can easily be done manually if desired, but keeping it as a Materia component lets me move it to another host easily by just changing my repositories manifest file.</p>
</section>
<section id="the-final-result" class="level1">
<h1>The Final Result</h1>
<p>The final Materia component looks like this:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode default code-with-copy"><code class="sourceCode default"><span id="cb7-1">materia_repo/components/staticsite master ❯ ls -la</span>
<span id="cb7-2">total 32K</span>
<span id="cb7-3">drwxr-xr-x.  2 stryan stryan 4.0K Jun  2 21:00 .</span>
<span id="cb7-4">drwxr-xr-x. 49 stryan stryan 4.0K Jun  2 20:38 ..</span>
<span id="cb7-5">-rw-r--r--.  1 stryan stryan  119 Jun  2 20:40 site-builder.build.gotmpl</span>
<span id="cb7-6">-rw-r--r--.  1 stryan stryan  320 Jun  2 21:17 site-updater.container.gotmpl</span>
<span id="cb7-7">-rw-r--r--.  1 stryan stryan  136 Jun  2 20:43 site-updater.timer</span>
<span id="cb7-8">-rw-r--r--.  1 stryan stryan  368 Jun  2 20:45 Containerfile</span>
<span id="cb7-9">-rw-r--r--.  1 stryan stryan   97 Jun  2 20:49 MANIFEST.toml</span>
<span id="cb7-10">-rw-r--r--.  1 stryan stryan  465 Jun  2 20:43 sync_sh</span></code></pre></div></div>
<p>On the server itself, running the actual <code>site-updater.service</code> does this:</p>
<ol type="1">
<li>Systemd sees <code>site-updater.service</code> depends on <code>site-builder.build</code></li>
<li><code>site-builder.build</code> is started</li>
<li>The <code>localhost/staticsite:latest</code> image is build with the embedded shell script. This will be quick since all the layers are cached.</li>
<li><code>site-updater.service</code> runs, starting a <code>localhost/staticsite:latest</code> container with the <code>github-ssh</code> secret mounted inside as an SSH key and <code>/srv/www</code> bind-mounted.</li>
<li>The script runs, downloading the static site git repo to <code>/srv/www/&lt;site-destination&gt;</code>.</li>
</ol>
</section>
<section id="was-this-worth-it" class="level1">
<h1>Was this worth it?</h1>
<p>Maybe! If you’re not running an atomic distro like MicroOS or Fedora CoreOS, it’s probably a little more effort then its worth. The way I see it though, this workflow has two big benefits:</p>
<ol type="1">
<li>It keeps all the work done on the host itself; I used to deploy my sites with a CI pipeline, but that meant Yet Another Server to manage deploy keys and workflows on.</li>
<li>Writing it as Quadlets keeps the whole thing essentially OS agnostic. I already know all my machines have Podman and Systemd on them, so I can move this setup to any other server I want without having to worry about having the <code>git-lfs</code> dependency.</li>
</ol>
<p>So I’m pretty happy that I went with this instead of a bespoke cron + shell script setup.</p>
<p>There’s a few other improvements I could make to this workflow as well, like container health checks to make sure the site actually syncs properly and moving to image backed Volumes instead of a bind mount. The <code>site-builder.build</code>, <code>site-updater.container</code> pattern is also an obvious candidate for being moved into an instanced component so I can have multiple copies of it like <code>static-site@quadlets-are-cool</code>, <code>static-site@nerf-seven</code>, etc.</p>
<p>Finally, you may have noticed that, if you squint at this hard enough, this is basically a small build/CICD system powered by systemd. I’m not going to say this is a <em>good</em> usage of systemd job scheduling abilities, but if you ever needed a job system that’s already on your host, you could do worse then systemd <sup>4</sup>.</p>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>That’s right, <code>/srv/www</code> not <code>/var/www</code>. In this house we respect the FHS to the letter.↩︎</p></li>
<li id="fn2"><p>I named it <code>sync_sh</code> instead of <code>sync.sh</code> to prevent Materia from installing it as a script to <code>/usr/local/bin</code>. This just keeps things a littler cleaner.↩︎</p></li>
<li id="fn3"><p>That <code>Secret=</code> line would use the <code>secretMount</code> macro in reality, but I left it manually inserted for illustrative purposes. And because at the time of writing the macro doesn’t support the mode argument. Whoops.↩︎</p></li>
<li id="fn4"><p>Looking at you Jenkins and GHA.↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>guides</category>
  <guid>https://primamateria.systems/blog/2026-06-03-many-uses-of-quadlets.html</guid>
  <pubDate>Wed, 03 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>0.6.0 Release</title>
  <dc:creator>Steve Ryan</dc:creator>
  <link>https://primamateria.systems/blog/2026-02-23-0.6.0-release.html</link>
  <description><![CDATA[ 






<p><a href="https://github.com/stryan/materia/releases/tag/v0.6.0">Materia v0.6</a> has been released! Much of this release also ended up being more backend refactoring, but we still had a few new features added that I didn’t want to keep from <code>:stable</code> any longer.</p>
<p>As I did for v0.5, here’s a bit of a more detailed over view for the bigger changes. If you just want the changelog, check it out here.</p>
<section id="oci-images-as-a-repository-source" class="level1">
<h1>OCI images as a repository source</h1>
<p>You can now specify a remote OCI image to use as your Materia repository! Simply create a bare image with the normal Materia repository file layout at <code>/</code> and pass it as the source URL like any other image: <code>MATERIA_SOURCE__URL=oci://git.saintnet.tech/stryan/materia-example-repo:latest</code>. The <a href="https://github.com/stryan/materia_example_repo">example repository</a> has been updated with a <code>mise</code> task that demonstrates how to do so. This is useful if you’re building your repository with some automated process like Nix, or you simply don’t want to expose your Git forge over the internet.</p>
<p>This feature was contributed by <a href="https://github.com/TECHNOFAB11">TECHNOFAB</a> on Github.</p>
</section>
<section id="quadlets-and-appmode-compatibility" class="level1">
<h1>Quadlets and AppMode compatibility</h1>
<p>Podman 5.8 introduced a new Quadlet type: <code>.quadlets</code>. These files contain multiple quadlets concatenated together, an example of which can be seen on the <a href="https://docs.podman.io/en/v5.8.0/markdown/podman-quadlet-install.1.html">official Podman documentation</a>. Materia now officially supports this quadlet type with its own implementation, meaning you don’t need Podman 5.8 to use it. Quadlets files can be included in your components and repositories like any other Quadlet type and Materia will automatically expand and install the file as its “component” Quadlet files.</p>
<p>On a related note, Materia now has a <code>MATERIA_APPMODE</code> (default false) configuration option that causes it to generate <code>.component_name.app</code> files for installed components. This is too improve compatibility with the <code>podman quadlet</code> commands, as now you can see which quadlets are a part of what component. Please note that since Materia stores data files in its own folder the app file does <strong>not</strong> contain the data files associated with the component, only the Quadlet files.</p>
</section>
<section id="component-script-improvements" class="level1">
<h1>Component Script improvements</h1>
<p>Did you know? Components can be scripted with post-install and post-removal steps, for all those tricky installation steps that need to be done imperatively or are too tedious to implement as systemd units.</p>
<p>Originally this feature was implemented using special <code>setup.sh</code> and <code>cleanup.sh</code> scripts included with the component, but now is done entirely through the Component manifest file and systemd one-shot jobs. Normal script resources can be specified in the new <code>Settings.SetupScript</code> and <code>Settings.CleanupScript</code> Component manifest options. When the component is installed Materia will run the specified setup script as a systemd transitive job with the name <code>componentname-materia-setup.service</code>. Conversely, when the component is removed Materia will run the specified cleanup script as <code>componentname-materia-cleanup.service</code>. These jobs are set to remain after exit so you can examine logs if needed, but running the startup job will attempt to remove any stale cleanup jobs and vice-versa.</p>
</section>
<section id="changelog" class="level1">
<h1>Changelog</h1>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode default code-with-copy"><code class="sourceCode default"><span id="cb1-1">- refactor: planner,plan,containers,services, executor, and component loading modules have been publicized and refactored.</span>
<span id="cb1-2">- feat: Component scripts are no longer a separate resource type. They are instead normal script resources that, when specified in the component manifest, are run in a transient systemd unit on installation/removal.</span>
<span id="cb1-3">- bugfix: component removal ignores non-existent services</span>
<span id="cb1-4">- bugfix/feat: `Overrides` now properly overrides manifests, new `Extensions` manifest option</span>
<span id="cb1-5">- feat: add support for OCI images as repository sources.</span>
<span id="cb1-6">- feat: experimental support for `.quadlets` files</span>
<span id="cb1-7">- feat: LoadAllAttributes config setting for attributes engine to ignore host/role filtering</span>
<span id="cb1-8">- feat: new `services` config block</span>
<span id="cb1-9">- feat/refactor: remote components use new source format</span>
<span id="cb1-10">- feat: `appMode` config option to generate `podman quadlet` compatible app files</span></code></pre></div></div>
</section>
<section id="deprecations" class="level1">
<h1>Deprecations</h1>
<p>As mentioned in the previous release, the <code>cleanup</code>, <code>cleanup_volumes</code>, <code>backup_volumes</code>, <code>migrate_volumes</code> settings are now in the <code>planner</code> config section.</p>


</section>

 ]]></description>
  <category>release</category>
  <guid>https://primamateria.systems/blog/2026-02-23-0.6.0-release.html</guid>
  <pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>0.5.0 Release</title>
  <dc:creator>Steve Ryan</dc:creator>
  <link>https://primamateria.systems/blog/2026-01-12-0.5.0-release.html</link>
  <description><![CDATA[ 






<p><a href="https://github.com/stryan/materia/releases/tag/v0.5.0">Materia v0.5</a> has been released! This release was mostly focused on back-end improvements and other infrastructure changes that should help make 0.6.0 be a bit more featureful.</p>
<p>Nevertheless, here’s an overview of some of the more important changes. You can also skip directly to the changelog.</p>
<p>This post also has an associated little walk-through I wrote: <a href="../blog/2026-01-11-writing-materia-components.html">How to write a Materia Component</a>.</p>
<section id="planner-re-work-and-new-settings" class="level1">
<h1>Planner re-work and new settings</h1>
<p>Much of this release was focusing on improving the <strong>planner</strong> which, as you may have guessed, is the big block of code that takes the current state of the host and repository as inputs and figures out the plan it needs to take to get the host reconciled with the desired state. Naturally, the planner is probably the oldest and most complicated chunk of code in the codebase. Unfortunately it was so complex it was making it complicated to add fun new features like dynamic timeouts so I refactored and separated out a bunch of the functions into less complex units. The user-facing changes from this should be minimal.</p>
<p>On my continued journey to clean up Materia’s configuration, I also separated out config options that directly affect plan generation into their own table. The following settings were migrated to the new <code>planner</code> section, with the old locations to be deprecated in 0.6:</p>
<ul>
<li><em>cleanup</em> -&gt; <em>planner.cleanup_quadlets</em></li>
<li><em>cleanup_volumes</em> -&gt; <em>planner.cleanup_volumes</em></li>
<li><em>migrate_volumes</em> -&gt; <em>planner.migrate_volumes</em></li>
<li><em>backup_volumes</em> -&gt; <em>planner.backup_volumes</em></li>
</ul>
<p>On a related note, the old <code>cleanup</code> function used to also purge components after they failed to install. I don’t think anyone ever used this feature but if you did it’s now under <code>executor.cleanup_components</code>.</p>
</section>
<section id="service-definitions-can-use-quadlets" class="level1">
<h1>Service definitions can use Quadlets</h1>
<p>It felt silly to make users have to figure out what service name a Quadlet will translate to, so now Materia can use direct Quadlet resource names in the <code>[[Services]]</code> array.</p>
</section>
<section id="dynamic-timeouts" class="level1">
<h1>Dynamic timeouts</h1>
<p>Materia now automatically adjusts the timeout it uses for <code>systemctl</code> actions to account for whether the service is dependant on another object. This was specifically added to account for situations like <code>.container</code> Quadlets using <code>Image=otherimage.build</code>: since building a Container can take a while, the default 30s timeout was far too little but adjusting the overall timeout felt off since you’d still want other services to fail in a normal amount of time. Now Materia will A) automatically add some extra time to the timeout free of charge when it detects this situation, B) allow you to manually specify timeouts for individual Services and C) combine Service timeouts when required.</p>
<p>For example: if <code>hello.container</code> relies on <code>hello.build</code>, you can define the following Services for them in the manifest: <sub></sub>~ini [[Services]] Service = “hello.container”</p>
<p>[[Services]] Service = “hello.build” Stopped = true Timeout = 100 <sub></sub>~</p>
<p>Materia will add <code>hello.build</code>’s 100s timeout to any service actions applied to <code>hello.container</code>.</p>
<p>As part of this, you can also specify <code>Stopped</code> in your <code>[[Services]]</code> entries to tell Materia <strong>not</strong> to start a service automatically, as used in the <code>.build</code> unit above.</p>
</section>
<section id="ensuring-volumes-and-networks" class="level1">
<h1>Ensuring Volumes and Networks</h1>
<p>While testing dynamic timeouts I noticed something annoying about how <code>.volume</code> Quadlets worked: Since Podman creates those as <code>one-shot</code> units, running them again won’t actually do anything if they’re been run once. This is especially annoying when you do something like remove a Volume quadlet in one update, delete the Podman volume, and re-add it in another since systemd will just see the still active <code>volumename-volume.service</code> and not do anything.</p>
<p>This annoyed me so much that Materia now checks for this scenario and plans an <code>Ensure</code> action to account for it; if it detects a <code>.volume</code>/<code>.network</code>/<code>.image</code> resource being updated and doesn’t see an equivalent Podman resource for it, it will ensure it exists.</p>
</section>
<section id="changelog" class="level1">
<h1>Changelog</h1>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode default code-with-copy"><code class="sourceCode default"><span id="cb1-1">- refactor: planner has been heavily refactored</span>
<span id="cb1-2">- feat: Quadlet resources (.container,.pod,etc) can be used as services in Component services configurations</span>
<span id="cb1-3">- feat: Containers that rely on Build or Image quadlets will use dynamic timeouts:</span>
<span id="cb1-4">- feat: New `planner` and `executor` config sections, deprecating the old `cleanup`,`cleanup_volumes`,`migrate_volumes`,`backup_volumes` config option locations</span>
<span id="cb1-5">- feat: ensure volumes and networks exist when unit already started; this fixes situations like installing,removing, and reinstalling a volume.</span>
<span id="cb1-6">- feat/bugfix: materia will no longer attempt to cleanup quadlets that are still in use by other containers</span></code></pre></div></div>


</section>

 ]]></description>
  <category>release</category>
  <guid>https://primamateria.systems/blog/2026-01-12-0.5.0-release.html</guid>
  <pubDate>Mon, 12 Jan 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Writing Materia Components</title>
  <dc:creator>Steve Ryan</dc:creator>
  <link>https://primamateria.systems/blog/2026-01-11-writing-materia-components.html</link>
  <description><![CDATA[ 






<section id="intro" class="level1">
<h1>Intro</h1>
<p>I recently found myself in need of a <code>dnsmasq</code> component and while writing it up decided to document my process as sort of a tutorial.</p>
<p>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 .</p>
<p>We’re going to use <a href="https://hub.docker.com/r/dockurr/dnsmasq">dockurr’s container</a> for this, but really <a href="https://thekelleys.org.uk/dnsmasq/doc.html">dnsmasq</a> is simple enough to package up and deploy that probably any container would work with it.</p>
<p>You can see the resulting component <a href="https://github.com/stryan/dnsmasq_component">here</a> and even use it in your own repositories, though at your own risk of course.</p>
</section>
<section id="creating-the-directory-layout-and-manifest" class="level1">
<h1>Creating the directory layout and manifest</h1>
<p>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.</p>
<p>We’ll run all these commands from the root of my repository.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> cd <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">code/materia_repo</span></span>
<span id="cb1-2"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> ls</span>
<span id="cb1-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">components</span>  secrets  MANIFEST.toml  mise.local.toml  mise.toml</span></code></pre></div></div>
<p>(The mise files are mostly just tasks for easy <code>sops</code> encryption)</p>
<p>Let’s create the folder and manifest file for the dnsmasq component:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> mkdir <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">components/dnsmasq</span></span>
<span id="cb2-2"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> touch <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">components/dnsmasq/MANIFEST.toml</span></span>
<span id="cb2-3"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> cd <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">components/dnsmasq</span></span>
<span id="cb2-4"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> ls</span>
<span id="cb2-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">MANIFEST.toml</span></span></code></pre></div></div>
<p>I like to fill in the manifest last when writing components, so we’ll write up the container file first.</p>
</section>
<section id="writing-the-.container-file" class="level1">
<h1>Writing the .container file</h1>
<p>We’ll create a <code>dnsmasq.container</code> file first:</p>
<p><code>components/dnsmasq/dnsmasq.container:</code></p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode ini code-with-copy"><code class="sourceCode ini"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Container]</span></span>
<span id="cb3-2"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Image</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">docker.io/dockurr/dnsmasq:latest</span></span>
<span id="cb3-3"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">ContainerName</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">dnsmasq</span></span>
<span id="cb3-4"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">PublishPort</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">53:53/udp</span></span>
<span id="cb3-5"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">ReloadSignal</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SIGHUP</span></span>
<span id="cb3-6"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Volume</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">/etc/dnsmasq:/etc/dnsmasq</span></span>
<span id="cb3-7"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">AddCapability</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">NET_ADMIN</span></span>
<span id="cb3-8"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">AutoUpdate</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">registry</span></span>
<span id="cb3-9"></span>
<span id="cb3-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">[Install]</span></span>
<span id="cb3-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Needed to make container start at boot</span></span>
<span id="cb3-12"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">WantedBy</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">multi-user.target default.target</span></span></code></pre></div></div>
<p>This is essentially the simplest quadlet we could write: it will create the dnsmasq container and mount the host’s <code>/etc/dnasmasq</code> 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 <code>/etc/containers/systemd</code>.</p>
<p>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.</p>
<p>We probably also want to have a nice description and restart automatically if it fails.</p>
<p>Let’s convert it into a template and make it more flexible. Replace the <code>dnsmasq.container</code> file with this <code>dnsmasq.container.gotmpl</code> file:</p>
<p><code>components/dnsmasq/dnsmasq.container.gotmpl:</code> <sub></sub>~ini [Unit] Description=dnsmasq container StartLimitIntervalSec=400 StartLimitBurst=3</p>
<p>[Service] Restart=on-failure RestartSec=5s</p>
<p>[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</p>
<p>[Install] WantedBy=multi-user.target default.target <sub></sub>~</p>
<p>See the differences? Here’s a brief summary of what changed:</p>
<ol type="1">
<li><p>We added <code>[Unit]</code> and <code>[Service]</code> sections. Like the <code>[Install]</code> section these are normal systemd unit properties that will apply to the generated unit.</p></li>
<li><p>We added two simple variables: <code>{ .containerTag }</code> and <code>{ .hostPort }</code>. These will be templated in with values from your configured attributes vaults, or from the <code>defaults</code> section of the component manifest. Spoilers: We’re going to set them in the component manifest.</p></li>
<li><p>We added an if-statement with <code>{- if .useSeperateFiles }</code>; since these are normal <a href="https://pkg.go.dev/text/template">Go Templates</a> we can use any feature available there. I’m old fashioned and like having dnsmasq use <code>/etc/hosts</code> and the oft-forgotten <code>/etc/ethers</code> files, but some situations or users might want just the one <code>dnsmasq.conf</code> file.</p></li>
<li><p>We used the <code>m_dataDir</code> macro; these are built-in functions for materia that provide useful features. In this case the <code>m_dataDir "component_name"</code> macro resolves to data directory for the <code>component_name</code>, letting us refer to data files in a component without hardcoding references. In this case, it would be <code>/var/lib/materia/components/dnsmasq/</code>.</p></li>
<li><p>Finally, we used <code>:Z</code> to get around <code>selinux</code> 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.</p></li>
</ol>
<p>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.&nbsp;we’re not creating any podman volumes or networks) we’ll move to the data files.</p>
</section>
<section id="adding-data-files" class="level1">
<h1>Adding data files</h1>
<p>We need to create three data files: <code>dnsmasq.conf</code>, <code>hosts</code>, and <code>ethers</code>. 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:</p>
<p><code>components/dnsmasq/dnsmasq.conf.gotmpl</code>: <sub></sub>~gotmpl {{ .confContents }} <sub></sub>~</p>
<p><code>components/dnsmasq/hosts.gotmpl</code>: <sub></sub>~gotmpl {{ .hosts }} <sub></sub>~</p>
<p><code>components/dnsmasq/ethers.gotmpl</code>: <sub></sub>~gotmpl {{ .ethers }} <sub></sub>~</p>
<p>These files will be filled with the contents of their respective attributes.</p>
<p>This setup gives us maximum flexibility with minimum blog post length :)</p>
</section>
<section id="writing-the-component-manifest" class="level1">
<h1>Writing the component manifest</h1>
<p>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.</p>
<p><code>components/dnsmasq/MANIFEST.toml</code>: <sub></sub>~toml [Defaults] containerTag = “latest” hostPort = “53” useSeperateFiles = true confContents = “” hosts = “” ethers = “”</p>
<p>[[Services]] Service = “dnsmasq.container” ReloadedBy = [“hosts”,“ethers”,“dnsmasq.conf”] <sub></sub>~</p>
<p>We’ve created two sections here: <code>[Defaults]</code> and <code>[[Services]]</code>.</p>
<ol type="1">
<li><p><code>[Defaults]</code> is a table that defines the default attribute values for this component. For example, the <code>containerTag</code> 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 <code>confContents</code> to force the user to provide a configuration.</p></li>
<li><p><code>[[Services]]</code> 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 <code>dnsmasq</code> service by telling it to start the <code>dnsmasq.container</code> quadlet (which it will translate to the proper systemd unit). We also defined the <code>dnsmasq.container</code> service to be reloaded by the <code>hosts</code>, <code>ethers</code>, and <code>dnsmasq.conf</code> resources. That means whenever any of those files are updated, materia will trigger a <code>systemctl reload</code> for the dnsmasq service.</p></li>
</ol>
<p>The resulting file layout for the component should look like this: <sub></sub>~bash &gt; ls components/dnsmasq dnsmasq.conf.gotmpl dnsmasq.container.gotmpl ethers.gotmpl hosts.gotmpl MANIFEST.toml <sub></sub>~</p>
<p>That’s it! The <code>dnsmasq</code> component is now complete and ready for use.</p>
</section>
<section id="adding-it-to-a-host" class="level1">
<h1>Adding it to a host</h1>
<p>Just to demonstrate, we’re going to assign this component to my server named <code>viscous</code>. We’ll do this by editing the main <code>MANIFEST.toml</code> file at the root of the repository and adding it to the host config for <code>viscous</code>.</p>
<p><code>MANIFEST.toml</code>: <sub></sub>~toml [hosts.viscous] components = [“dnsmasq”] <sub></sub>~</p>
<p>And that’s all there is to it. The next time materia runs on <code>viscous</code> it will automatically install and template the <code>dnsmasq.container</code>,<code>dnsmasq.conf</code>, <code>hosts</code>, and <code>ethers</code> files onto the host and start the <code>dnsmasq</code> container.</p>


</section>

 ]]></description>
  <category>guides</category>
  <guid>https://primamateria.systems/blog/2026-01-11-writing-materia-components.html</guid>
  <pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
