<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Marcin Niemira</title>
    <description>The latest articles on Forem by Marcin Niemira (@n0npax).</description>
    <link>https://forem.com/n0npax</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F350184%2F9776dd96-e5a8-48d8-95ff-54462b74916d.jpeg</url>
      <title>Forem: Marcin Niemira</title>
      <link>https://forem.com/n0npax</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/n0npax"/>
    <language>en</language>
    <item>
      <title>Simple and cheap RAG - genai-toolbox and pgvector</title>
      <dc:creator>Marcin Niemira</dc:creator>
      <pubDate>Wed, 18 Mar 2026 10:55:00 +0000</pubDate>
      <link>https://forem.com/n0npax/simple-and-cheap-rag-genai-toolbox-and-pgvector-2822</link>
      <guid>https://forem.com/n0npax/simple-and-cheap-rag-genai-toolbox-and-pgvector-2822</guid>
      <description>&lt;p&gt;I recently hit a common architectural fork in the road while building my &lt;strong&gt;ADK (Agent Development Kit)&lt;/strong&gt; application. &lt;/p&gt;

&lt;p&gt;Initially, I was using &lt;a href="https://www.trychroma.com/" rel="noopener noreferrer"&gt;Chroma&lt;/a&gt; as my RAG (Retrieval-Augmented Generation) backend. It works perfectly for local development, but things got complicated when moving to the cloud. I needed a production-ready, resilient solution that didn't involve managing stateful assets on a Mac Mini or paying for a separate managed vector database.&lt;/p&gt;

&lt;p&gt;The solution was already right in front of me: &lt;strong&gt;Postgres&lt;/strong&gt; 🐘.&lt;/p&gt;

&lt;p&gt;By using &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt;, you can turn your relational database into a powerful vector store. This is especially seamless if you use &lt;a href="https://supabase.com/docs/guides/database/extensions/pgvector" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;, which can host small databases for free or provide a "Pro" tier for a reasonable price.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Swapped MCP for genai-toolbox
&lt;/h3&gt;

&lt;p&gt;In my previous setup, I used an &lt;a href="[mcp](https://google.github.io/adk-docs/integrations/chroma/)"&gt;chroma mcp&lt;/a&gt; server. However, for my relational data, I was already relying heavily on &lt;a href="https://github.com/googleapis/genai-toolbox" rel="noopener noreferrer"&gt;genai-toolbox&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;I realized I could simplify my architecture significantly by dropping the separate MCP server and Chroma dependency in favor of extending my &lt;code&gt;tools.yaml&lt;/code&gt;. In engineering, if I can remove a dependency without losing functionality, I don't hesitate.&lt;/p&gt;

&lt;p&gt;Here is the configuration change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;embeddingModels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gemini-emb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text-embedding-004&lt;/span&gt;
    &lt;span class="na"&gt;dimension&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;768&lt;/span&gt;

&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;add-document&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-sql&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;naati-db&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;Adds a document to the RAG database. The 'content' will be automatically embedded.&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;content&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The raw text content to be stored in the database.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vector_string&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="c1"&gt;# This parameter is hidden from the LLM.&lt;/span&gt;
        &lt;span class="c1"&gt;# It automatically copies the value from 'content' and embeds it.&lt;/span&gt;
        &lt;span class="na"&gt;valueFromParam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;content&lt;/span&gt;
        &lt;span class="na"&gt;embeddedBy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-emb&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This parameter is hidden from the LLM.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metadata&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
          &lt;span class="s"&gt;JSON string representing metadata (e.g., '{"file_name": "tos.docx"}').&lt;/span&gt;

  &lt;span class="na"&gt;query-documents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-sql&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;naati-db&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;Performs a semantic similarity search on the RAG database using the provided query text.&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
          &lt;span class="s"&gt;The search query text to embed and search for.&lt;/span&gt;
        &lt;span class="na"&gt;embeddedBy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-emb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;limit&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
          &lt;span class="s"&gt;Maximum number of results to return.&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;SELECT content, metadata&lt;/span&gt;
      &lt;span class="s"&gt;FROM documents&lt;/span&gt;
      &lt;span class="s"&gt;ORDER BY embedding &amp;lt;=&amp;gt; $1::vector&lt;/span&gt;
      &lt;span class="s"&gt;LIMIT $2::int;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it works like a dream. Access to data and knowledge is now unified and goes via &lt;code&gt;genai-toolbox&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>postgres</category>
      <category>rag</category>
    </item>
    <item>
      <title>Optimising Docker Builds for Go</title>
      <dc:creator>Marcin Niemira</dc:creator>
      <pubDate>Sun, 15 Mar 2026 00:30:00 +0000</pubDate>
      <link>https://forem.com/n0npax/optimising-docker-builds-for-go-4i9g</link>
      <guid>https://forem.com/n0npax/optimising-docker-builds-for-go-4i9g</guid>
      <description>&lt;p&gt;This article describes how to improve build time for Docker containers with Go applications. It focuses on speeding up the build process rather than building images from scratch. It's updated(2026) version of my old medium article from 2023&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem to Solve
&lt;/h2&gt;

&lt;p&gt;Let's start with an issue definition: &lt;strong&gt;Building a Go app on a laptop is quick, but building the same app inside Docker takes ages.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is building an app on a local machine fast?
&lt;/h3&gt;

&lt;p&gt;Golang produces a binary file. Do you remember C? C also produces a binary file. Let's recap how we could compile a program in C. I promise, we will get back to Go soon.&lt;/p&gt;

&lt;p&gt;Let's consider the simplified Makefile below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;CFLAGS&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nt"&gt;-Wall&lt;/span&gt; &lt;span class="nt"&gt;-Werror&lt;/span&gt;
&lt;span class="nl"&gt;.default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;

&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;
&lt;span class="c"&gt;# Let's cheat a bit and hardcode main.o, foo.o and bar.o
&lt;/span&gt;&lt;span class="nl"&gt;app&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;foo.o bar.o main.o&lt;/span&gt;
    &lt;span class="c"&gt;# link object files&lt;/span&gt;
    &lt;span class="p"&gt;${&lt;/span&gt;CC&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; app &lt;span class="nv"&gt;$^&lt;/span&gt;

&lt;span class="c"&gt;# Build ${name}.o based on ${name}.c
&lt;/span&gt;&lt;span class="nl"&gt;%.o&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;%.c&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;building &lt;span class="nv"&gt;$@&lt;/span&gt;
    &lt;span class="p"&gt;${&lt;/span&gt;CC&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;${&lt;/span&gt;CFLAGS&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nv"&gt;$&amp;lt;&lt;/span&gt;

&lt;span class="c"&gt;# Remove object files and binary
&lt;/span&gt;&lt;span class="nl"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.o app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;make app&lt;/code&gt; will build an app artefact. Let's break it down to understand when it's slow and when it's fast.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;What if it's a clean build?&lt;/strong&gt;
There are no object files, so &lt;code&gt;CC&lt;/code&gt; (clang) will create object files. &lt;code&gt;foo.o&lt;/code&gt; will be created based on &lt;code&gt;foo.c&lt;/code&gt;, &lt;code&gt;bar.o&lt;/code&gt; based on &lt;code&gt;bar.c&lt;/code&gt; and so on. Once all object files are ready, &lt;code&gt;CC&lt;/code&gt; will link them creating our app. Because creating object files takes time, the build will be slow.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;What if &lt;code&gt;make app&lt;/code&gt; is invoked again?&lt;/strong&gt;
All object files are already in place, so &lt;code&gt;CC&lt;/code&gt; will just link them again. Very little computing power is required, so the action will be quick.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;What if we modify only &lt;code&gt;foo.c&lt;/code&gt;?&lt;/strong&gt;
Modification to &lt;code&gt;foo.c&lt;/code&gt; will enforce &lt;code&gt;foo.o&lt;/code&gt; recreation. &lt;code&gt;bar.o&lt;/code&gt; will stay untouched. &lt;code&gt;CC&lt;/code&gt; will link object files again. Only required files are rebuilt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fewer actions the computer performs, the faster the response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let's get back to Golang
&lt;/h3&gt;

&lt;p&gt;If you run &lt;code&gt;go help cache&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The go command caches build outputs for reuse in future builds.
The default location for cache data is a subdirectory named go-build
in the standard user cache directory for the current operating system.
Setting the GOCACHE environment variable overrides this default,
and running 'go env GOCACHE' prints the current cache directory.

The go command periodically deletes cached data that has not been
used recently. Running 'go clean -cache' deletes all cached data.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So Golang has a very similar approach; it uses a cache to preserve build outputs. If build outputs are present, less compute power is required to compile the program and local execution is quicker.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Docker Layers Work
&lt;/h2&gt;

&lt;p&gt;If a layer is already present on the machine, it will be reused. If the layer changes, then all downstream layers need to be rebuilt. As per the picture below, all layers from &lt;code&gt;COPY&lt;/code&gt; to the end of the Dockerfile will re-run, which is time-consuming.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6jyj17o77zjua59a1wlo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6jyj17o77zjua59a1wlo.png" alt=" " width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Assuming the previous build inside Docker generated 99% of the reusable build outputs, they are not available to the next build because the cache from the old layer is discarded.&lt;/p&gt;

&lt;p&gt;Fortunately, Docker offers cache management, which makes it possible to reuse cached files between runs even if layers are changed.&lt;/p&gt;

&lt;p&gt;Consider this Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /workspace&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; CGO_ENABLED=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GOCACHE=/go-cache&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./src ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/go-cache go build &lt;span class="nt"&gt;-o&lt;/span&gt; app ./...

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /workspace/app /bin/app&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/bin/app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please focus on the &lt;code&gt;GOCACHE&lt;/code&gt; environment variable. It has been overwritten to point to a custom location to mitigate operating system and Go installation method differences. Later its location is explicitly specified as a cache for a &lt;code&gt;go build&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;The first run will populate &lt;code&gt;GOCACHE&lt;/code&gt; with the actual cache. Subsequent builds will be faster as they can benefit from the existing cache. Assuming only a single file has changed, the vast majority of build outputs can be reused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching Dependencies
&lt;/h2&gt;

&lt;p&gt;Further inspection suggests that changing &lt;code&gt;go.sum&lt;/code&gt; or &lt;code&gt;go.mod&lt;/code&gt; forces re-fetching all dependencies over and over. Any change to &lt;code&gt;go.sum&lt;/code&gt; forces a re-run of all depending layers which includes &lt;code&gt;go mod download&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Can we use the same approach to solve the dependency caching issue? Sure we can. This time we're going to focus on caching &lt;code&gt;GOMODCACHE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's extend the Dockerfile to cover dependencies caching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /workspace&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; CGO_ENABLED=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GOCACHE=/go-cache&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GOMODCACHE=/gomod-cache&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./src/go.* ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/gomod-cache &lt;span class="se"&gt;\
&lt;/span&gt;  go mod download &lt;span class="c"&gt;# line to be removed in final, production ready dockerfile&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./src ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/gomod-cache &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/go-cache &lt;span class="se"&gt;\
&lt;/span&gt;  go build &lt;span class="nt"&gt;-o&lt;/span&gt; app ./...

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /workspace/app /bin/app&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; 65333&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/bin/app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the dependencies and build outputs caches are present and will be used by the Docker build. Only missing dependencies will be fetched and stored with the cache. Let’s consider the picture below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbakiwdquzvkqfpucsrx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbakiwdquzvkqfpucsrx.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Green layers are re-run, but thanks to the cache, their execution time is reduced to a minimum. Now Docker build is much faster 🤩.&lt;/p&gt;

&lt;h3&gt;
  
  
  But are all steps needed?
&lt;/h3&gt;

&lt;p&gt;Nah… We can safely remove:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/gomod-cache &lt;span class="se"&gt;\
&lt;/span&gt;  go mod download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;go mod download&lt;/code&gt; will pull all dependencies defined in &lt;code&gt;go.mod&lt;/code&gt;. &lt;code&gt;go build&lt;/code&gt; will pull only those which are actually required and the sequential dependency between the pull and build steps is no longer a concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static Linking with CGO_ENABLED=0
&lt;/h2&gt;

&lt;p&gt;By default, Go might use &lt;code&gt;cgo&lt;/code&gt; to link against the host's C libraries (like &lt;code&gt;libc&lt;/code&gt;). This creates a dynamic binary that requires those libraries to be present at runtime. Since the &lt;code&gt;scratch&lt;/code&gt; image is empty, a dynamic binary will fail to start with a cryptic "file not found" error.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;CGO_ENABLED=0&lt;/code&gt; forces Go to produce a &lt;strong&gt;statically linked binary&lt;/strong&gt;. This has three major benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Portability&lt;/strong&gt;: The binary contains everything it needs and can run on any Linux kernel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: By using &lt;code&gt;scratch&lt;/code&gt;, your production image contains zero shell, zero package managers, and zero C libraries, drastically reducing the attack surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size&lt;/strong&gt;: &lt;code&gt;scratch&lt;/code&gt; images are as small as they can possibly be—literally just your binary and any assets you explicitly include.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Performance Impact of CGO
&lt;/h3&gt;

&lt;p&gt;While the primary reasons for &lt;code&gt;CGO_ENABLED=0&lt;/code&gt; are portability and security, there is a tangible impact on build performance. In a clean build environment (like a CI worker), disabling CGO avoids the overhead of invoking the C toolchain (compiler, linker).&lt;/p&gt;

&lt;p&gt;For a large project like &lt;strong&gt;minikube&lt;/strong&gt;, the difference is noticeable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;CGO_ENABLED=0&lt;/strong&gt;: ~32.3s total&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;CGO_ENABLED=1&lt;/strong&gt;: ~40.1s total&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By disabling CGO, we achieved a &lt;strong&gt;~20% faster clean build&lt;/strong&gt; 🤯. At runtime, pure Go code also avoids the overhead of stack switching required when calling C code, though for most web applications, this difference is negligible compared to the build-time gains.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;tested with minikube codebase as follow&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;time &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1_OR_0 &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"darwin"&lt;/span&gt; &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"arm64"&lt;/span&gt;  &lt;span class="se"&gt;\&lt;/span&gt;
        go build &lt;span class="nt"&gt;-tags&lt;/span&gt; &lt;span class="s2"&gt;"libvirt_dlopen"&lt;/span&gt; &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-X k8s.io/minikube/pkg/version.version=v1.37.0 -X k8s.io/minikube/pkg/version.isoVersion=v1.37.0-1765151505-21409 -X k8s.io/minikube/pkg/version.gitCommitID="&lt;/span&gt;d96de0585719fe650d457f0055205b427d4b7bdb&lt;span class="s2"&gt;" -X k8s.io/minikube/pkg/version.storageProvisionerVersion=v5"&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; out/minikube-darwin-arm64 k8s.io/minikube/cmd/minikube
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Solution Limitations
&lt;/h2&gt;

&lt;p&gt;The proposed approach is not limitless. It's &lt;del&gt;2023&lt;/del&gt; 2026, a big chunk of work is happening in the cloud with ephemeral workers. This means the cache won't be available for sub-sequential runs, as the worker won't exist anymore.&lt;/p&gt;

&lt;p&gt;This issue may be mitigated by rsync. It's possible to rsync the content of the cache to layer and push the builder image to registry or rsync it to the s3/gcs bucket. This solution comes with a price tag: The builder image will be heavy and a time penalty will be added to every build (rsync takes time). It's important to remember that the mounted cache is not stored within the layer, so it won't be pushed within the builder image by default.&lt;/p&gt;

&lt;p&gt;Even if further enhancements with rsync and pushing builder image to the registry is possible, I'd suggest checking if the local cache is enough, as the complexity &amp;amp; price tag of the extended solution may outweigh its benefits.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD and Remote Caching
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Fast forward to 2026:&lt;/em&gt; Remote Cache Backends have become the "missing link" that solves the ephemeral worker issue. However, it is crucial to understand the distinction between &lt;strong&gt;Layer Caching&lt;/strong&gt; and &lt;strong&gt;Cache Mounts&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0sapoe44sjs915m2uqn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0sapoe44sjs915m2uqn.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Cache Mounts (&lt;code&gt;--mount=type=cache&lt;/code&gt;)&lt;/strong&gt;: These are designed for &lt;strong&gt;on-machine&lt;/strong&gt; persistence. They are extremely fast but stay local to the BuildKit instance. They are &lt;strong&gt;not&lt;/strong&gt; exported by remote backends.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Layer Caching (&lt;code&gt;--cache-to/from&lt;/code&gt;)&lt;/strong&gt;: These backends (like &lt;code&gt;gha&lt;/code&gt; or &lt;code&gt;registry&lt;/code&gt;) export the finalized image layers to an external service. These &lt;strong&gt;are&lt;/strong&gt; persisted across ephemeral runners.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  GitHub Actions Backend
&lt;/h3&gt;

&lt;p&gt;If you are using GitHub Actions, the &lt;code&gt;gha&lt;/code&gt; backend is an efficient way to share &lt;em&gt;layer&lt;/em&gt; cache across runs. Because &lt;code&gt;type=cache&lt;/code&gt; mounts are not exported, you should combine them with a &lt;strong&gt;Dependency Layer Pattern&lt;/strong&gt; for the best results on GHA.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; go.mod go.sum ./&lt;/span&gt;
&lt;span class="c"&gt;# this layer can be cached by layer cache&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod download

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="c"&gt;# this layer can not be cached by layer cache. it's only cache mount&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/go-cache go build &lt;span class="nt"&gt;-o&lt;/span&gt; app ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To enable this in your workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
  &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
  &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my_app/my_service:{{ github.sha }}&lt;/span&gt;
  &lt;span class="na"&gt;build-args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;BUILD_VERSION=${{ github.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;mode=max&lt;/code&gt; tells BuildKit to export all intermediate layers, including your &lt;code&gt;go mod download&lt;/code&gt; layer, ensuring that subsequent runs on fresh workers can skip the download entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registry Backend
&lt;/h3&gt;

&lt;p&gt;Alternatively, you can store the cache directly in your Docker registry. This is useful if you are using a CI provider other than GitHub Actions or want a unified cache location.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;registry,ref&lt;span class="o"&gt;=&lt;/span&gt;my-repo/app:build-cache &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;registry,ref&lt;span class="o"&gt;=&lt;/span&gt;my-repo/app:build-cache,mode&lt;span class="o"&gt;=&lt;/span&gt;max &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; my_service &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Specifying custom paths for &lt;code&gt;GOCACHE&lt;/code&gt; and &lt;code&gt;GOMODCACHE&lt;/code&gt; provides installation-independent paths and reduces dependency on the underlying OS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GOCACHE=/go-cache&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GOMODCACHE=/gomod-cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the balanced performance, you may consider adopting a hybrid approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Use Image Layers&lt;/strong&gt; (e.g., &lt;code&gt;RUN go mod download&lt;/code&gt;) for dependencies you want to persist across CI runs via &lt;code&gt;gha&lt;/code&gt; or &lt;code&gt;registry&lt;/code&gt; backends.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Use Cache Mounts&lt;/strong&gt; (&lt;code&gt;--mount=type=cache&lt;/code&gt;) to speed up local development and internal stages of a single build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But I'd advise to stick to one approach. Hybrid means problems from both sides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Talk Numbers
&lt;/h2&gt;

&lt;p&gt;For the tests purposes given Dockerfiles focused on local build performance were added to local copy &lt;strong&gt;minikube&lt;/strong&gt; project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dockerfile.with-caching:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; make
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /workspace&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GOCACHE=/go-cache&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GOMODCACHE=/gomod-cache&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./go.* ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/gomod-cache &lt;span class="se"&gt;\
&lt;/span&gt;  go mod download &lt;span class="c"&gt;# this line exists only to show time saved on fetch step. it should NOT exists in actual dockerfile&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./ ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/gomod-cache &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/go-cache &lt;span class="se"&gt;\
&lt;/span&gt;   make linux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Standard Dockerfile.without-caching:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; make
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /workspace&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./go.* ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod download
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./ ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;make linux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changes to source code were done using commands like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# change code&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"s/expected docker.EndpointMeta/expected docker.EndpointMeta &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RANDOM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/g"&lt;/span&gt; cmd/minikube/main.go
&lt;span class="c"&gt;# change deps&lt;/span&gt;
go get go.opentelemetry.io/otel@main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results once the cache has been populated by the previous build and the code has been changed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With caching enabled:&lt;/strong&gt;&lt;br&gt;
Building 36.0s (14/14) FINISHED&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;[builder 7/9] RUN --mount=type=cache,target=/gomod-cache go mod download&lt;/code&gt; &lt;strong&gt;0.8s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;[builder 8/9] COPY ./ ./&lt;/code&gt; &lt;strong&gt;1.6s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;[builder 9/9] RUN --mount=type=cache,target=/gomod-cache --mount=type=cache,target=/go-cache make linux&lt;/code&gt; &lt;strong&gt;33.6s&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Without caching enabled:&lt;/strong&gt;&lt;br&gt;
Building 114.2s (12/12) FINISHED&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;[5/7] RUN go mod download&lt;/code&gt; &lt;strong&gt;65.7s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;[6/7] COPY ./ ./&lt;/code&gt; &lt;strong&gt;1.1s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;[7/7] RUN make linux&lt;/code&gt; &lt;strong&gt;37.4s&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest advantage of caching is shown with the &lt;code&gt;go mod download&lt;/code&gt; step, where time was reduced from &lt;strong&gt;65.7s to 0.8s&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This article &lt;strong&gt;prioritizes local build speed&lt;/strong&gt; over CI/CD performance. For CI/CD improvements, you should focus more on the CI/CD and Remote Caching section.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>docker</category>
      <category>go</category>
      <category>performance</category>
      <category>ci</category>
    </item>
    <item>
      <title>gemini-cli: My Local Hero for packer and systemd</title>
      <dc:creator>Marcin Niemira</dc:creator>
      <pubDate>Fri, 13 Mar 2026 05:54:00 +0000</pubDate>
      <link>https://forem.com/n0npax/gemini-cli-my-local-hero-for-packer-and-systemd-38ii</link>
      <guid>https://forem.com/n0npax/gemini-cli-my-local-hero-for-packer-and-systemd-38ii</guid>
      <description>&lt;h1&gt;
  
  
  The Local Hero: Automating the Boring Parts of AI Infrastructure
&lt;/h1&gt;

&lt;p&gt;Lately, I’ve been working on an AI agent for a small translation company called &lt;a href="https://insighter.com.au" rel="noopener noreferrer"&gt;insighter&lt;/a&gt;. At its core, it’s an agent equipped with some specialized tools and a rather extensive prompt. &lt;/p&gt;

&lt;p&gt;Because the project requires stateful resources, I decided to accept "statefulness" as a lesser evil for this specific use case and deployed it to a Google Cloud Platform (GCP) VM. The setup itself is straightforward: &lt;strong&gt;Packer&lt;/strong&gt; for the machine image and a standard deployment pipeline. Nothing too wild.&lt;/p&gt;

&lt;p&gt;However, the part that truly impressed me was how easily I was able to &lt;strong&gt;do things right&lt;/strong&gt;. The architecture has several moving parts: the &lt;a href="https://github.com/googleapis/genai-toolbox" rel="noopener noreferrer"&gt;mcp-toolbox&lt;/a&gt;, a &lt;strong&gt;Chroma&lt;/strong&gt; vector database, a few other &lt;strong&gt;MCP servers&lt;/strong&gt;, and my own supporting microservice*&lt;em&gt;s&lt;/em&gt;* running on &lt;code&gt;localhost&lt;/code&gt;. Naturally, these all need to start up in a specific order to function correctly.&lt;/p&gt;

&lt;p&gt;I know how to write proper &lt;code&gt;systemd&lt;/code&gt; services. I know how to pass environment variables and define startup dependencies. But the "mental tax" of recalling the exact syntax, crafting the manifests, and testing the logic usually takes more time than the quality of such a small solution seems to justify.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;gemini-cli&lt;/strong&gt; became my local hero. &lt;/p&gt;

&lt;p&gt;With just a few prompts, it analyzed the local environment, identified the necessary dependencies, and generated clean, production-ready &lt;code&gt;systemd&lt;/code&gt; services in minutes. The real win here wasn't just the code generation. It was the ability to delegate the "boring" infrastructure tasks, ensuring the job was done correctly with minimal manual effort.&lt;/p&gt;




&lt;h3&gt;
  
  
  Key Takeaway
&lt;/h3&gt;

&lt;p&gt;When doing things right is this cheap, taking shortcuts is no longer justifiable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>cli</category>
      <category>gemini</category>
    </item>
    <item>
      <title>I’m Not an SEO Guru, So I Built a dynamic Content Engine with Gemini-CLI Instead</title>
      <dc:creator>Marcin Niemira</dc:creator>
      <pubDate>Wed, 11 Mar 2026 10:42:07 +0000</pubDate>
      <link>https://forem.com/n0npax/im-not-an-seo-guru-so-i-built-a-dynamic-content-engine-with-gemini-cli-instead-4cn5</link>
      <guid>https://forem.com/n0npax/im-not-an-seo-guru-so-i-built-a-dynamic-content-engine-with-gemini-cli-instead-4cn5</guid>
      <description>&lt;h1&gt;
  
  
  How I Used Gemini-CLI and Golang to Scale a Tiny Translation Business from 10 to 600+ Impressions
&lt;/h1&gt;

&lt;p&gt;As life happens, I’ve stumbled into helping a tiny business: a new translation service in Australia called &lt;a href="https://insighter.com.au" rel="noopener noreferrer"&gt;insighter&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Initially, I crafted a simple website. The stack was &lt;strong&gt;Golang&lt;/strong&gt;, &lt;strong&gt;HTMX&lt;/strong&gt;, and &lt;strong&gt;Tailwind&lt;/strong&gt;. It was clean, fast, and... invisible. Google Search Console showed around 10 impressions a day and zero clicks.&lt;/p&gt;

&lt;p&gt;Disclaimer: I’m not an SEO guru. I’m just a random engineer who accidentally stepped into the world of &lt;strong&gt;Programmatic SEO (pSEO)&lt;/strong&gt;. Here is the 6-step engineering log of how I used LLMs to build a content engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: The Low-Hanging Fruit (Metadata)
&lt;/h2&gt;

&lt;p&gt;I started by improving page quality and metadata. I let &lt;code&gt;gemini-cli&lt;/code&gt; do the heavy lifting, generating keyword-rich tags and descriptions for the existing static content. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run an SEO checker to find missing tags.&lt;/li&gt;
&lt;li&gt;Feed the page context to Gemini.&lt;/li&gt;
&lt;li&gt;Manual tweaks to ensure it didn't sound like a robot wrote it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; SEO tools stopped screaming at me. A solid baseline, but not a game-changer yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Breaking the "Quality" Rule with pSEO
&lt;/h2&gt;

&lt;p&gt;Google traditionally advises focusing on a few high-quality pages rather than many similar ones. However, Australia has over &lt;strong&gt;2,600 postcodes&lt;/strong&gt;. I decided to ignore the "less is more" advice and went for bulk generation.&lt;/p&gt;

&lt;p&gt;I built 26 templates and programmatically generated pages for different locations. To avoid the "duplicate content" penalty, I got creative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unique Visuals:&lt;/strong&gt; I generated SVG images with random seeds and predefined colors. Every page got a unique fingerprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Skewing:&lt;/strong&gt; I built a "fun fact" bank (50 records) and state/territory specific twists (8). &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Math:&lt;/strong&gt; 2600 pages / 26 templates / 50 fun facts / 8 state-specific twists ~= 0.25. &lt;/p&gt;

&lt;p&gt;With a ratio below 1, the risk of an exact duplicate page is nearly zero. Of course this number is skewed as ACT has less postcodes than NWS, but principle stays.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; A visible bump. Impressions jumped by &lt;strong&gt;250–350 per day&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Pivoting Keywords (Languages &amp;amp; Countries)
&lt;/h2&gt;

&lt;p&gt;I applied the same logic from Step 2 but shifted focus to &lt;strong&gt;Language + Country&lt;/strong&gt; combinations. &lt;/p&gt;

&lt;p&gt;Instead of just "Translation Services," I targeted "NAATI Documents from France" or "NAATI French Translation".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Another &lt;strong&gt;100 impressions/day&lt;/strong&gt; added to the tally.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Building the "Link Mesh"
&lt;/h2&gt;

&lt;p&gt;Internal linking is SEO gold. Using the distance between latitudes and longitudes, I automated a "Nearby Locations" section. &lt;/p&gt;

&lt;p&gt;If you are on the &lt;strong&gt;Black Rock 3193&lt;/strong&gt; page, the app automatically suggests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Highett 3190&lt;/li&gt;
&lt;li&gt;Cheltenham 3192&lt;/li&gt;
&lt;li&gt;Brighton 3187&lt;/li&gt;
&lt;li&gt;Mentone 3194&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did the same for languages. Since Switzerland has multiple official languages, the page for Switzerland links to Italian, German, and French. The Italian page, in turn, links back to Switzerland, Italy, and San Marino. This irregular, non-1:1 mapping creates a natural-feeling web for crawlers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Harder to isolate, but overall site authority began to climb.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Template Variators (The "Mad Libs" Approach)
&lt;/h2&gt;

&lt;p&gt;To make the content even more unique, I implemented &lt;code&gt;DocumentPurposeVariations&lt;/code&gt; and &lt;code&gt;DocumentNounVariations&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Instead of every page saying &lt;em&gt;"We translate legal documents,"&lt;/em&gt; the template uses placeholders like &lt;code&gt;{DOC_NOUNS}&lt;/code&gt; and &lt;code&gt;{DOC_PURPOSES}&lt;/code&gt;. The engine picks from a bank of grammatically compatible strings. The result? Thousands of pages that are technically different but consistently accurate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Unsure yet as it's a fresh change, but the uniqueness score is high.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: JSON-LD and FAQ Injection
&lt;/h2&gt;

&lt;p&gt;Finally, I focused on Schema markup (&lt;code&gt;JSON-LD&lt;/code&gt;). For a postcode like &lt;strong&gt;3022&lt;/strong&gt; (which covers Ardeer and Deer Park East), I injected specific FAQ schemas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Q: Do you provide service in Ardeer?&lt;/em&gt; → &lt;em&gt;A: Yes.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Q: Do you provide service in Deer Park East?&lt;/em&gt; → &lt;em&gt;A: Yes.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tells Google exactly what the page is about in a machine-readable format.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The effort required to do this manually would never justify the gain for a "tiny business." But by treating SEO as a &lt;strong&gt;data engineering problem&lt;/strong&gt; and using &lt;code&gt;gemini-cli&lt;/code&gt; as a specialized intern, I built a pSEO solution for &lt;a href="https://insighter.com.au" rel="noopener noreferrer"&gt;insighter&lt;/a&gt; mostly in the background.&lt;br&gt;
Does it solve all the problems? Nah, backlinks or authority are not sorted yet, but it's already a huge improvement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway:&lt;/strong&gt; Don't just build a site. Build a system that generates the site.&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>seo</category>
      <category>google</category>
    </item>
  </channel>
</rss>
