<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Images on inetshell</title><link>https://inet.sh/en/tags/images/</link><description>Recent content in Images on inetshell</description><generator>Hugo</generator><language>en</language><lastBuildDate>Fri, 25 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/en/tags/images/index.xml" rel="self" type="application/rss+xml"/><item><title>k3s: use images without a registry (docker save → k3s ctr images import) and clean up the leftovers</title><link>https://inet.sh/en/posts/k3s/import-images-without-registry/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/k3s/import-images-without-registry/</guid><description>How to run an image you built with Docker on k3s without standing up a registry: docker save, copy the tar, and k3s ctr images import on every node, with imagePullPolicy Never. The alternatives for several nodes (the agent/images directory, k3s embedded registry) and how to list and delete images with k3s crictl.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<p>On the machine where you build:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker build -t test-app:v1.0.0 .
</span></span><span class="line"><span class="cl">docker save -o test-app-v1.0.0.tar test-app:v1.0.0
</span></span><span class="line"><span class="cl">rsync -v test-app-v1.0.0.tar node1:/tmp/
</span></span></code></pre></td></tr></table>
</div>
</div><p>On <strong>every</strong> k3s node where the pod may land:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">sudo k3s ctr images import /tmp/test-app-v1.0.0.tar
</span></span><span class="line"><span class="cl">sudo k3s crictl images <span class="p">|</span> grep test-app        <span class="c1"># docker.io/library/test-app  v1.0.0</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>And in the Deployment, so kubelet does not try to pull it:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">test-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">test-app:v1.0.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">imagePullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Never</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h2 id="why">Why</h2>
<p>k3s runs on <strong>containerd</strong>, not Docker. An image that exists in the Docker on the same machine is invisible to k3s, and the first time a pod asks for it kubelet tries to pull it from Docker Hub and fails with <code>ErrImagePull</code>. There are three ways out: a registry (the right one past a certain size), importing the tar on every node, or letting k3s import it by itself from a directory.</p>
<p><code>k3s ctr</code> is containerd&rsquo;s <code>ctr</code> already pointed at the k3s socket and the <code>k8s.io</code> namespace, which is where kubelet and <code>crictl</code> look. If you use the bare <code>ctr</code> binary you need <code>--address /run/k3s/containerd/containerd.sock -n k8s.io</code>.</p>
<h2 id="alternatives-for-more-than-one-node">Alternatives for more than one node</h2>
<p><strong>Images directory.</strong> Every <code>.tar</code>, <code>.tar.gz</code>, or <code>.tar.zst</code> you drop into <code>/var/lib/rancher/k3s/agent/images/</code> is imported each time k3s starts (it is the documented mechanism for installs without Internet):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">sudo mkdir -p /var/lib/rancher/k3s/agent/images/
</span></span><span class="line"><span class="cl">sudo cp test-app-v1.0.0.tar /var/lib/rancher/k3s/agent/images/
</span></span><span class="line"><span class="cl">sudo systemctl restart k3s        <span class="c1"># or k3s-agent</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Advantage over the manual <code>import</code>: if kubelet&rsquo;s image garbage collector removes it, it comes back on the next start.</p>
<p><strong>Embedded registry (Spegel).</strong> Since k3s 1.29 servers can start with <code>--embedded-registry</code> (or <code>embedded-registry: true</code> in <code>/etc/rancher/k3s/config.yaml</code>); each node publishes on the P2P network (port 5001) the images it already has and the others pull them from it instead of the Internet. It requires listing the registries to mirror in <code>/etc/rancher/k3s/registries.yaml</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">mirrors</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">docker.io</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s2">&#34;*&#34;</span><span class="p">:</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>With that, you import the image on <strong>one</strong> node and the rest get it on their own, as long as the nodes can reach each other on 5001 and 6443.</p>
<p><strong>Your own registry.</strong> A <code>registry:2</code> or Harbor in the cluster, and <code>docker push</code>. It is the path once CI is involved.</p>
<h2 id="listing-and-cleaning">Listing and cleaning</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">sudo k3s crictl images                    <span class="c1"># what is on the node</span>
</span></span><span class="line"><span class="cl">sudo k3s crictl rmi --prune               <span class="c1"># delete the ones no container uses</span>
</span></span><span class="line"><span class="cl">sudo k3s crictl rmi docker.io/library/test-app:v1.0.0
</span></span><span class="line"><span class="cl">sudo k3s crictl rmi --all                 <span class="c1"># everything; kubelet will pull back what it needs</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>kubelet also collects on its own: when the disk holding <code>/var/lib/rancher/k3s/agent/containerd</code> goes over <strong>85 %</strong> it deletes unused images until it drops below <strong>80 %</strong>. The thresholds are changed on the service:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># /etc/rancher/k3s/config.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">kubelet-arg</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">image-gc-high-threshold=85</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">image-gc-low-threshold=80</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>If that disk runs short, moving the data-dir is <a href="/en/posts/k3s/migrate-data/">another post</a>.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>Every node.</strong> A pod scheduled on a node without the image gives <code>ErrImageNeverPull</code>. Either import on all of them, or label the node and use <code>nodeSelector</code>, or move to the embedded registry.</li>
<li><strong>Architecture.</strong> For ARM nodes (Raspberry Pi) the image must be <code>linux/arm64</code>: <code>docker buildx build --platform linux/arm64</code>.</li>
<li><strong><code>latest</code> hits the network.</strong> With the <code>latest</code> tag the default policy is <code>Always</code> and kubelet tries to pull it even if it exists. Use explicit tags and <code>Never</code> or <code>IfNotPresent</code>.</li>
<li><strong>The full name</strong> of an imported image is <code>docker.io/library/test-app:v1.0.0</code>; in the manifest <code>test-app:v1.0.0</code> resolves to the same thing.</li>
<li><strong>podman</strong>: <code>podman save --format docker-archive -o app.tar app:v1</code>, the format <code>ctr</code> imports with no surprises.</li>
<li><strong>A <code>crictl rmi --prune</code> also deletes what you imported</strong> if no container is using it at that moment. For what must survive, the <code>agent/images/</code> directory.</li>
</ul>
<p>References: <a href="https://docs.k3s.io/installation/airgap">Air-Gap Install</a> and <a href="https://docs.k3s.io/installation/registry-mirror">Embedded Registry Mirror</a> in the k3s documentation.</p>
]]></content:encoded></item></channel></rss>