<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://homelabpostmortem.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://homelabpostmortem.com/" rel="alternate" type="text/html" /><updated>2026-08-17T03:24:00+00:00</updated><id>https://homelabpostmortem.com/feed.xml</id><title type="html">The Homelab Postmortem</title><subtitle>Real, dated postmortems from running a homelab — what broke, how it was diagnosed, and the exact fix. Plus the verified scripts that came out of it.</subtitle><entry><title type="html">The rpi-clone PARTUUID trap: why your Pi 4B silently refuses to boot from a cloned SSD</title><link href="https://homelabpostmortem.com/2026/08/16/rpi-clone-partuuid-trap/" rel="alternate" type="text/html" title="The rpi-clone PARTUUID trap: why your Pi 4B silently refuses to boot from a cloned SSD" /><published>2026-08-16T00:00:00+00:00</published><updated>2026-08-16T00:00:00+00:00</updated><id>https://homelabpostmortem.com/2026/08/16/rpi-clone-partuuid-trap</id><content type="html" xml:base="https://homelabpostmortem.com/2026/08/16/rpi-clone-partuuid-trap/"><![CDATA[<p><strong>TL;DR</strong>: <code class="language-plaintext highlighter-rouge">rpi-clone</code> copies your SD card to a USB SSD beautifully, but it does <em>not</em> update the <code class="language-plaintext highlighter-rouge">root=PARTUUID=...</code> reference in the destination’s <code class="language-plaintext highlighter-rouge">cmdline.txt</code>. Flip the boot order to USB before catching this, and your Pi either boots the wrong disk or fails to boot at all. Five-minute fix once you know to look for it.</p>

<h2 id="the-setup">The setup</h2>

<p>Moving a Raspberry Pi’s root filesystem off the SD card and onto a USB-attached SSD is one of the highest-value changes you can make to a Pi that’s meant to run unattended for months: SD cards wear out under sustained write load, SSDs don’t (practically speaking), and boot/IO latency drops noticeably as a bonus.</p>

<p>The standard playbook is:</p>

<ol>
  <li>Update the bootloader EEPROM (<code class="language-plaintext highlighter-rouge">sudo rpi-eeprom-update -a</code>), reboot.</li>
  <li>Clone the running SD card to the SSD with <a href="https://github.com/billw2/rpi-clone"><code class="language-plaintext highlighter-rouge">rpi-clone</code></a>.</li>
  <li>Flip <code class="language-plaintext highlighter-rouge">raspi-config</code> → <em>Advanced Options</em> → <em>Boot Order</em> to prefer USB.</li>
  <li>Reboot and confirm <code class="language-plaintext highlighter-rouge">findmnt /</code> shows the SSD.</li>
</ol>

<p>Steps 1, 3, and 4 are exactly as advertised. Step 2 has a landmine in it.</p>

<h2 id="what-actually-happens">What actually happens</h2>

<p><code class="language-plaintext highlighter-rouge">rpi-clone</code> does a genuinely good job: it partitions the destination to match the source, <code class="language-plaintext highlighter-rouge">rsync</code>s the filesystem across, and rewrites the destination’s <code class="language-plaintext highlighter-rouge">/etc/fstab</code> with the <em>destination’s own</em> <code class="language-plaintext highlighter-rouge">PARTUUID</code>s. If you check <code class="language-plaintext highlighter-rouge">fstab</code> after cloning, it’s correct.</p>

<p>What it does <strong>not</strong> do is touch <code class="language-plaintext highlighter-rouge">/boot/firmware/cmdline.txt</code> on the destination. That file still contains the <strong>source disk’s</strong> <code class="language-plaintext highlighter-rouge">root=PARTUUID=...</code>, copied verbatim.</p>

<p>This is easy to miss because nothing in the clone process complains. The clone finishes, reports success, and looks done.</p>

<h2 id="why-it-bites-you-specifically-at-the-worst-moment">Why it bites you specifically at the worst moment</h2>

<p>You won’t notice during the clone. You won’t notice right after, either — you’re still running from the SD card at that point, so everything looks fine. The mismatch only matters the moment the firmware actually tries to boot the kernel <em>from the SSD</em>, using the SSD’s own <code class="language-plaintext highlighter-rouge">cmdline.txt</code> — which is exactly what happens right after you flip the boot order and reboot.</p>

<p>At that point the kernel is told “find your root filesystem at PARTUUID <code class="language-plaintext highlighter-rouge">10052bbd-02</code>” (the SD card’s identifier), while the disk it’s actually booting from carries an entirely different, freshly-generated PARTUUID (say, <code class="language-plaintext highlighter-rouge">b11c96d4-02</code>). Depending on whether the SD card is still physically present, you get either a boot that silently falls back to the SD card’s root filesystem (confusing — your changes “don’t stick”), or a kernel panic waiting for a root device that isn’t there (worse).</p>

<h2 id="how-to-catch-it-before-rebooting">How to catch it before rebooting</h2>

<p>Right after the clone finishes — <strong>before</strong> you touch the boot order — compare the source and destination PARTUUIDs directly:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># What the SD card's root partition is actually called</span>
<span class="nb">sudo </span>blkid /dev/mmcblk0p2

<span class="c"># What the SSD's root partition is actually called</span>
<span class="nb">sudo </span>blkid /dev/sda2

<span class="c"># What the SSD's own boot config *thinks* the root partition is called</span>
<span class="nb">sudo </span>mount /dev/sda1 /mnt
<span class="nb">grep</span> <span class="nt">-o</span> <span class="s1">'root=PARTUUID=[a-f0-9-]*'</span> /mnt/cmdline.txt
</code></pre></div></div>

<p>If the third value matches the first (the SD card’s) instead of the second (the SSD’s own), you have the bug.</p>

<h2 id="the-fix">The fix</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s2">"s/root=PARTUUID=&lt;OLD_PARTUUID&gt;/root=PARTUUID=&lt;NEW_PARTUUID&gt;/"</span> /mnt/cmdline.txt
</code></pre></div></div>

<p>Then do one more sweep to make sure nothing else on the destination still references the stale identifier — it’s a one-liner and costs nothing:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo grep</span> <span class="nt">-rl</span> <span class="s2">"&lt;OLD_PARTUUID&gt;"</span> /mnt 2&gt;/dev/null
</code></pre></div></div>

<p>If that returns nothing, unmount and reboot into the boot-order change with confidence.</p>

<p>A ready-to-run version of this check-and-fix — with a dry-run mode and an automatic backup of <code class="language-plaintext highlighter-rouge">cmdline.txt</code> before it touches anything — is in the <a href="/toolkit/">toolkit</a> that comes with this post.</p>

<h2 id="the-general-lesson">The general lesson</h2>

<p>Any tool that clones a running system to a new disk has to solve the “how does the new disk know it’s the new disk” problem somewhere. Some tools solve it by rewriting every reference at clone time; <code class="language-plaintext highlighter-rouge">rpi-clone</code> solves <em>most</em> of it that way (fstab) but leaves one file (cmdline.txt) untouched. That’s not a bug exactly — it’s a scope boundary the tool doesn’t advertise loudly. The general habit worth keeping: after any disk clone, before changing what your firmware boots from, grep the new disk for the old disk’s identifiers. If anything comes back, you’ve found your landmine before it found you.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[rpi-clone copies your SD card to a USB SSD beautifully, but it does not update the root=PARTUUID reference in the destination's cmdline.txt. Flip the boot order before catching this and your Pi either boots the wrong disk or fails to boot at all.]]></summary></entry></feed>