<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>buildah &amp;mdash; Jerry of the Week</title>
    <link>https://write.in0rdr.ch/tag:buildah</link>
    <description>ˈdʒɛri - Individual who sends life against the grain no matter the consequences</description>
    <pubDate>Thu, 30 Apr 2026 13:28:36 +0000</pubDate>
    <item>
      <title>Parallel container builds with Jenkins scripted pipeline</title>
      <link>https://write.in0rdr.ch/parallel-container-builds-with-jenkins-scripted-pipeline</link>
      <description>&lt;![CDATA[Created a new pipeline for building my container images in the homelab. A dynamic choice populates a &#34;matrix&#34; that runs container build stages in parallel on runners with different architectures.&#xA;&#xA;#jenkins #containers #buildah&#xA;!--more--&#xA;&#xA;I was writing about how and why I use Jenkins last year.&#xA;&#xA;The basic idea behind the pipeline here is to build container images for multiple platforms/architectures (arm64 &amp; amd64 mainly).&#xA;&#xA;An advantage of using the runners native architecture (arm64/amd64) for the builds is that I don&#39;t need to use the QEMU emulation with buildah.&#xA;&#xA;Jenkinsfile with static nodes/stages &#xA;My previous Jenkinsfile (starting point) was very &#34;static&#34;. Static in a way, that the nodes were selected sequentially (first build on an arm64 node, then on a amd64 node) and I created two different tags for the different architectures:&#xA;@Library(&#39;in0rdr-jenkins-lib@master&#39;) &#xA;&#xA;def buildahbud = new BuildahBud(this)&#xA;def buildahpush = new BuildahPush(this)&#xA;def buildahmanifest = new BuildahManifest(this)&#xA;&#xA;node(&#39;podman&amp;&amp;arm64&#39;){&#xA;  checkout scm&#xA;&#xA;  // build with image context and name&#xA;  buildahbud.execute([:], &#34;docker/docker-snac&#34;, &#34;snac&#34;, &#34;2.90-arm64&#34;&#xA;    &#39;Dockerfile&#39;, &#39;arm64/v8&#39;)&#xA;  buildahpush.execute(&#34;snac&#34;, &#34;2.90-arm64&#34;)&#xA;}&#xA;&#xA;node(&#39;podman&amp;&amp;amd64&#39;){&#xA;  checkout scm&#xA;  buildahbud.execute([:], &#34;docker/docker-snac&#34;, &#34;snac&#34;, &#34;2.90-amd64&#34;,&#xA;    &#39;Dockerfile&#39;, &#39;amd64&#39;)&#xA;  buildahmanifest.create(&#39;haproxy.lan:5000/snac:2.90&#39;, [&#xA;    &#39;haproxy.lan:5000/snac:2.90-arm64&#39;,&#xA;    &#39;haproxy.lan:5000/snac:2.90-amd64&#39;&#xA;  ])&#xA;}&#xA;&#xA;You can find my Jenkins libraries here:&#xA;https://code.in0rdr.ch/jenkins-lib/files.html&#xA;&#xA;Pulling a different tag depending on the architecture is cumbersome. By creating a Manifest ([buildah manifest create](https://github.com/containers/buildah/blob/main/docs/buildah-manifest-create.1.md&#xA;)) I can reuse the same tag across multiple machine architectures (e.g., I can use haproxy.lan:5000/snac:2.90 on amd and arm machines):&#xA;buildah manifest inspect haproxy.lan:5000/snac:2.90&#xA;{&#xA;    &#34;schemaVersion&#34;: 2,&#xA;    &#34;mediaType&#34;: &#34;application/vnd.oci.image.index.v1+json&#34;,&#xA;    &#34;manifests&#34;: [&#xA;        {&#xA;            &#34;mediaType&#34;: &#34;application/vnd.oci.image.manifest.v1+json&#34;,&#xA;            &#34;digest&#34;: &#34;sha256:7f67daa2193f2ef9a84c3bcaa14c73d429165631cbbc6c30faf74ef69ac07d4a&#34;,&#xA;            &#34;size&#34;: 1207,&#xA;            &#34;platform&#34;: {&#xA;                &#34;architecture&#34;: &#34;arm64&#34;,&#xA;                &#34;os&#34;: &#34;linux&#34;,&#xA;                &#34;variant&#34;: &#34;v8&#34;&#xA;            }&#xA;        },&#xA;        {&#xA;            &#34;mediaType&#34;: &#34;application/vnd.oci.image.manifest.v1+json&#34;,&#xA;            &#34;digest&#34;: &#34;sha256:85b79a9014eed10f8b12cddd9f6fcf9a0e56cdf792b96cf4cde52c9c8f61c4f7&#34;,&#xA;            &#34;size&#34;: 1204,&#xA;            &#34;platform&#34;: {&#xA;                &#34;architecture&#34;: &#34;amd64&#34;,&#xA;                &#34;os&#34;: &#34;linux&#34;&#xA;            }&#xA;        }&#xA;    ]&#xA;}&#xA;&#xA;Dynamic choice, run stages on nodes in parallel&#xA;&#xA;I decided to change my static Jenkinsfile and spice it up with the dynamic choices based on the &#34;input matrix&#34;. The &#34;platform&#34; axis only holds the &#34;podman&#34; platform right now.&#xA;https://code.in0rdr.ch/jenkins-lib/file/src/BuildahParallelBuild.groovy.html&#xA;&#xA;buildahparallelbuild-choice.jpg&#xA;&#xA;For this, I took some inspiration from an older Jenkins blog post from 2019. The code example for a scripted pipeline with &#34;dynamic choices&#34; (choices based on predefined matrix axes) still works pretty fine.&#xA;&#xA;My inputs can now be kept rather simple, they simply describe which Dockerfile I want to build, from which path in the repo, with certain name &amp; tag (optionally some build arguments):&#xA;&#xA;@Library(&#39;in0rdr-jenkins-lib@master&#39;) &#xA;&#xA;def buildahParallelBuild = new BuildahParallelBuild(this)&#xA;&#xA;buildahParallelBuild.build(&#34;snac&#34;, &#34;2.90&#34;, &#34;docker/docker-snac&#34;)&#xA;&#xA;// Other example inputs:&#xA;//buildahParallelBuild.build(&#34;texlive&#34;, &#34;latest&#34;, &#34;docker/docker-texlive&#34;)&#xA;//buildahParallelBuild.build(&#34;updatecli&#34;, &#34;v0.114.0&#34;, &#34;docker/docker-updatecli&#34;)&#xA;//buildahParallelBuild.build(&#34;jenkins-inbound-agent&#34;, &#34;3355.v388858a47b33&#34;, &#34;docker/docker-jenkins-inbound-agent&#34;, [arg1: &#34;test&#34;, arg2: &#34;test2&#34;])&#xA;&#xA;buildahparallelbuild-pipeline.jpg&#xA;&#xA;It doesn&#39;t matter on which architecture I create the manifest.&#xA;&#xA;I&#39;m still a huge fan of the extensibility of these Jenkins libraries. By simply reusing my existing libraries for Buildah bud (class BuildahBud) / push (class BuildahPush) / manifest creation (class BuildahManifest) from my already working &#34;static&#34; use case, I could extend quickly with the &#34;parallel build&#34; functionality (in a new/separate class which simply imports the existing functionality) 🤗.&#xA;&#xA;Later I might figure out more about @NonCPS and NonCPS best practices.. hit me up if you would like to educate me 🤔&#xA;&#xA;Thanks for reading&#xA;&#xA;div style=&#34;text-align:center; font-size: 0.8em&#34;&#xD;&#xA;a href=&#34;https://write.in0rdr.ch/feed&#34;&amp;#128732; RSS/a | a href=&#34;https://m.in0rdr.ch/in0rdr&#34;&amp;#128024; Fediverse/a | a href=&#34;https://chat.in0rdr.ch/#/guest?join=p0c@conference.in0rdr.ch&#34;&amp;#128172; XMPP/a&#xD;&#xA;/div]]&gt;</description>
      <content:encoded><![CDATA[<p>Created a new pipeline for building my container images in the homelab. A <a href="https://www.jenkins.io/blog/2019/12/02/matrix-building-with-scripted-pipeline/#full-pipeline-example-with-dynamic-choices">dynamic choice</a> populates a “matrix” that runs container build stages in parallel on runners with different architectures.</p>

<p><a href="https://write.in0rdr.ch/tag:jenkins" class="hashtag"><span>#</span><span class="p-category">jenkins</span></a> <a href="https://write.in0rdr.ch/tag:containers" class="hashtag"><span>#</span><span class="p-category">containers</span></a> <a href="https://write.in0rdr.ch/tag:buildah" class="hashtag"><span>#</span><span class="p-category">buildah</span></a>
</p>

<p>I was writing about <a href="https://write.in0rdr.ch/jenkins-works">how and why I use Jenkins last year</a>.</p>

<p>The basic idea behind the pipeline here is to build container images for multiple platforms/architectures (arm64 &amp; amd64 mainly).</p>

<p>An advantage of using the runners native architecture (arm64/amd64) for the builds is that I don&#39;t need to use the <a href="https://github.com/containers/buildah/discussions/4736">QEMU emulation with buildah</a>.</p>

<h2 id="jenkinsfile-with-static-nodes-stages">Jenkinsfile with static nodes/stages</h2>

<p>My previous Jenkinsfile (starting point) was very “static”. Static in a way, that the nodes were selected sequentially (first build on an arm64 node, then on a amd64 node) and I created two different tags for the different architectures:</p>

<pre><code class="language-groovy">@Library(&#39;in0rdr-jenkins-lib@master&#39;) _

def buildahbud = new BuildahBud(this)
def buildahpush = new BuildahPush(this)
def buildahmanifest = new BuildahManifest(this)

node(&#39;podman&amp;&amp;arm64&#39;){
  checkout scm

  // build with image context and name
  buildahbud.execute([:], &#34;docker/docker-snac&#34;, &#34;snac&#34;, &#34;2.90-arm64&#34;
    &#39;Dockerfile&#39;, &#39;arm64/v8&#39;)
  buildahpush.execute(&#34;snac&#34;, &#34;2.90-arm64&#34;)
}

node(&#39;podman&amp;&amp;amd64&#39;){
  checkout scm
  buildahbud.execute([:], &#34;docker/docker-snac&#34;, &#34;snac&#34;, &#34;2.90-amd64&#34;,
    &#39;Dockerfile&#39;, &#39;amd64&#39;)
  buildahmanifest.create(&#39;haproxy.lan:5000/snac:2.90&#39;, [
    &#39;haproxy.lan:5000/snac:2.90-arm64&#39;,
    &#39;haproxy.lan:5000/snac:2.90-amd64&#39;
  ])
}
</code></pre>

<p>You can find my Jenkins libraries here:
* <a href="https://code.in0rdr.ch/jenkins-lib/files.html">https://code.in0rdr.ch/jenkins-lib/files.html</a></p>

<p>Pulling a different tag depending on the architecture is cumbersome. By creating a Manifest (<a href="https://github.com/containers/buildah/blob/main/docs/buildah-manifest-create.1.md"><code>buildah manifest create</code></a>) I can reuse the same tag across multiple machine architectures (e.g., I can use <code>haproxy.lan:5000/snac:2.90</code> on amd and arm machines):</p>

<pre><code class="language-bash">buildah manifest inspect haproxy.lan:5000/snac:2.90
</code></pre>

<pre><code class="language-json">{
    &#34;schemaVersion&#34;: 2,
    &#34;mediaType&#34;: &#34;application/vnd.oci.image.index.v1+json&#34;,
    &#34;manifests&#34;: [
        {
            &#34;mediaType&#34;: &#34;application/vnd.oci.image.manifest.v1+json&#34;,
            &#34;digest&#34;: &#34;sha256:7f67daa2193f2ef9a84c3bcaa14c73d429165631cbbc6c30faf74ef69ac07d4a&#34;,
            &#34;size&#34;: 1207,
            &#34;platform&#34;: {
                &#34;architecture&#34;: &#34;arm64&#34;,
                &#34;os&#34;: &#34;linux&#34;,
                &#34;variant&#34;: &#34;v8&#34;
            }
        },
        {
            &#34;mediaType&#34;: &#34;application/vnd.oci.image.manifest.v1+json&#34;,
            &#34;digest&#34;: &#34;sha256:85b79a9014eed10f8b12cddd9f6fcf9a0e56cdf792b96cf4cde52c9c8f61c4f7&#34;,
            &#34;size&#34;: 1204,
            &#34;platform&#34;: {
                &#34;architecture&#34;: &#34;amd64&#34;,
                &#34;os&#34;: &#34;linux&#34;
            }
        }
    ]
}
</code></pre>

<h2 id="dynamic-choice-run-stages-on-nodes-in-parallel">Dynamic choice, run stages on nodes in parallel</h2>

<p>I decided to change my static Jenkinsfile and spice it up with the dynamic choices based on the “input matrix”. The “platform” axis only holds the “podman” platform right now.
* <a href="https://code.in0rdr.ch/jenkins-lib/file/src/BuildahParallelBuild.groovy.html">https://code.in0rdr.ch/jenkins-lib/file/src/BuildahParallelBuild.groovy.html</a></p>

<p><img src="https://code.in0rdr.ch/pub/blog/buildahparallelbuild-choice.jpg" alt="buildahparallelbuild-choice.jpg"></p>

<p>For this, I took some inspiration from an older <a href="https://www.jenkins.io/blog/2019/12/02/matrix-building-with-scripted-pipeline/#full-pipeline-example-with-dynamic-choices">Jenkins blog post from 2019</a>. The code example for a scripted pipeline with “dynamic choices” (choices based on predefined matrix axes) still works pretty fine.</p>

<p>My inputs can now be kept rather simple, they simply describe which Dockerfile I want to build, from which path in the repo, with certain name &amp; tag (optionally some build arguments):</p>

<pre><code class="language-groovy">@Library(&#39;in0rdr-jenkins-lib@master&#39;) _

def buildahParallelBuild = new BuildahParallelBuild(this)

buildahParallelBuild.build(&#34;snac&#34;, &#34;2.90&#34;, &#34;docker/docker-snac&#34;)

// Other example inputs:
//buildahParallelBuild.build(&#34;texlive&#34;, &#34;latest&#34;, &#34;docker/docker-texlive&#34;)
//buildahParallelBuild.build(&#34;updatecli&#34;, &#34;v0.114.0&#34;, &#34;docker/docker-updatecli&#34;)
//buildahParallelBuild.build(&#34;jenkins-inbound-agent&#34;, &#34;3355.v388858a_47b_33&#34;, &#34;docker/docker-jenkins-inbound-agent&#34;, [arg1: &#34;test&#34;, arg2: &#34;test2&#34;])
</code></pre>

<p><img src="https://code.in0rdr.ch/pub/blog/buildahparallelbuild-pipeline.jpg" alt="buildahparallelbuild-pipeline.jpg"></p>

<p>It doesn&#39;t matter on which architecture I create the manifest.</p>

<p>I&#39;m still a huge fan of the extensibility of these Jenkins libraries. By simply reusing my existing libraries for Buildah bud (class <code>BuildahBud</code>) / push (class <code>BuildahPush</code>) / manifest creation (class <code>BuildahManifest</code>) from my already working “static” use case, I could extend quickly with the “parallel build” functionality (in a new/separate class which simply imports the existing functionality) 🤗.</p>

<p>Later I might figure out more about <a href="https://www.jenkins.io/doc/book/pipeline/cps-method-mismatches/"><code>@NonCPS</code></a> and <a href="https://www.jenkins.io/doc/book/pipeline/pipeline-best-practices/#using-noncps"><code>NonCPS</code> best practices</a>.. hit me up if you would like to educate me 🤔</p>

<p>Thanks for reading</p>

<div style="text-align:center; font-size: 0.8em">
<a href="https://write.in0rdr.ch/feed">🛜 RSS</a> | <a href="https://m.in0rdr.ch/in0rdr">🐘 Fediverse</a> | <a href="https://chat.in0rdr.ch/#/guest?join=p0c@conference.in0rdr.ch">💬 XMPP</a>
</div>
]]></content:encoded>
      <guid>https://write.in0rdr.ch/parallel-container-builds-with-jenkins-scripted-pipeline</guid>
      <pubDate>Wed, 18 Mar 2026 19:48:12 +0000</pubDate>
    </item>
  </channel>
</rss>