Boot From SD Live on SSD | How to migrate Alpine Linux from SD card to SSD without reinstalling anything
SD cards are the default storage medium for Raspberry Pi but their low IOPS, limited write endurance, and susceptibility to corruption make them a poor foundation for anything beyond casual use. This post walks through migrating a live Alpine Linux installation to an external SSD without reinstalling the OS. The approach keeps the SD card as a thin bootloader host and pivots the root filesystem to the SSD using UUID-pinned fstab and cmdline.txt configuration.
<details id="toc-wrapper" style="border: 1px solid var(--primary-color, #444444); padding: 0.75rem 1rem; margin: 1rem 0; border-radius: 0.25em;">
<summary style="cursor: pointer; font-weight: 600;">Table of Contents</summary>
<nav id="TOC" role="doc-toc" style="margin-top: 0.75rem; text-align: left;">
<ul>
<li><a href="#why-sd-cards-fail-under-real-workloads" id="toc-why-sd-cards-fail-under-real-workloads"><span class="toc-section-number">1</span> Why SD Cards Fail Under Real Workloads</a></li>
<li><a href="#what-you-need-before-starting" id="toc-what-you-need-before-starting"><span class="toc-section-number">2</span> What You Need Before Starting</a></li>
<li><a href="#partitioning-and-formatting-the-ssd" id="toc-partitioning-and-formatting-the-ssd"><span class="toc-section-number">3</span> Partitioning and Formatting the SSD</a></li>
<li><a href="#migrating-the-root-filesystem" id="toc-migrating-the-root-filesystem"><span class="toc-section-number">4</span> Migrating the Root Filesystem</a></li>
<li><a href="#pivoting-the-boot-configuration" id="toc-pivoting-the-boot-configuration"><span class="toc-section-number">5</span> Pivoting the Boot Configuration</a>
<ul>
<li><a href="#updating-fstab" id="toc-updating-fstab"><span class="toc-section-number">5.1</span> Updating fstab</a></li>
<li><a href="#updating-cmdline.txt" id="toc-updating-cmdline.txt"><span class="toc-section-number">5.2</span> Updating cmdline.txt</a></li>
</ul></li>
<li><a href="#verifying-the-migration" id="toc-verifying-the-migration"><span class="toc-section-number">6</span> Verifying the Migration</a></li>
<li><a href="#what-to-do-with-the-sd-card-root-partition" id="toc-what-to-do-with-the-sd-card-root-partition"><span class="toc-section-number">7</span> What to Do with the SD Card Root Partition</a></li>
<li><a href="#power-and-long-term-health" id="toc-power-and-long-term-health"><span class="toc-section-number">8</span> Power and Long-Term Health</a></li>
</ul>
</nav>
</details>
<h1 data-number="1" id="why-sd-cards-fail-under-real-workloads"><span class="header-section-number">1</span> Why SD Cards Fail Under Real Workloads</h1>
<p>Consumer SD cards are built for camera-style writes: big sequential bursts, then idle time. SBCs do the opposite: random reads, random writes, metadata churn, logs, and swap. That mismatch causes most failures.</p>
<p>The gap is large. A decent SD card does about 4,000 random 4K read IOPS. A budget SATA SSD can do 80,000 or more. You feel this in databases, container layers, and <code>apk</code> operations.</p>
<p>Write endurance is the second problem. SD cards usually rely on TLC NAND rated at roughly 1,000 to 3,000 write cycles per cell. SSDs spread wear better and support TRIM. On a Pi running Docker with persistent volumes, SD cards can degrade within months.</p>
<p>The hybrid approach here avoids reinstalling the OS. The bootloader lives on the SD card because that is what the Pi hardware expects. Everything else moves.</p>
<h1 data-number="2" id="what-you-need-before-starting"><span class="header-section-number">2</span> What You Need Before Starting</h1>
<p>Three tools are used: <code>fdisk</code> for partitioning, <code>mkfs.ext4</code> for formatting, and <code>rsync</code> for the migration. All are available in Alpine’s package repositories.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">apk</span> add e2fsprogs rsync util-linux</span></code></pre></div>
<p>Hardware quality matters. A USB 3.0 to SATA adapter is fine for 2.5” SSDs. For NVMe, use a proper USB 3.1 Gen 2 enclosure. Cheap adapters often cause disconnects and data errors.</p>
<p>Before touching anything, identify your drives:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">lsblk</span></span></code></pre></div>
<p>The SD card will be <code>mmcblk0</code>. The SSD will appear as <code>sda</code> (or <code>sdb</code> if something else is connected). Confirm device names before every write operation. Getting this wrong means wiping the wrong disk.</p>
<h1 data-number="3" id="partitioning-and-formatting-the-ssd"><span class="header-section-number">3</span> Partitioning and Formatting the SSD</h1>
<p>Launch <code>fdisk</code> on the SSD:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="ex">fdisk</span> /dev/sda</span></code></pre></div>
<p>Inside the prompt:</p>
<pre><code>o # new DOS partition table
n # new partition
p # primary
1 # partition number
# accept default first sector
# accept default last sector (use the full disk)
t # set type
83 # Linux filesystem
w # write and exit</code></pre>
<p>Format the new partition as ext4:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="ex">mkfs.ext4</span> /dev/sda1</span></code></pre></div>
<p>ext4 is the right choice here. Journaling improves recovery after unclean shutdowns, and Alpine supports it out of the box.</p>
<h1 data-number="4" id="migrating-the-root-filesystem"><span class="header-section-number">4</span> Migrating the Root Filesystem</h1>
<p>Mount the SSD partition:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">mount</span> /dev/sda1 /mnt</span></code></pre></div>
<p>Then copy everything across using <code>rsync</code>:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="fu">rsync</span> <span class="at">-aXv</span> <span class="at">--exclude</span><span class="op">=</span><span class="dt">{</span><span class="st">"/dev/*"</span><span class="op">,</span><span class="st">"/proc/*"</span><span class="op">,</span><span class="st">"/sys/*"</span><span class="op">,</span><span class="st">"/tmp/*"</span><span class="op">,</span><span class="st">"/run/*"</span><span class="op">,</span><span class="st">"/mnt/*"</span><span class="op">,</span><span class="st">"/media/*"</span><span class="op">,</span><span class="st">"/lost+found"</span><span class="dt">}</span> / /mnt/</span></code></pre></div>
<p>Why these flags:</p>
<ul>
<li><code>-a</code> preserves permissions, ownership, symlinks, and timestamps</li>
<li><code>-X</code> preserves extended attributes, including ACLs and Alpine’s <code>apk</code> package metadata</li>
<li>The <code>--exclude</code> list skips virtual filesystems. <code>/proc</code> and <code>/sys</code> are generated at boot, and <code>/dev</code> is managed by <code>mdev</code></li>
</ul>
<p>Let <code>rsync</code> finish before moving on. On a 32GB SD card, this usually takes a few minutes over USB 3.0.</p>
<h1 data-number="5" id="pivoting-the-boot-configuration"><span class="header-section-number">5</span> Pivoting the Boot Configuration</h1>
<p>This is the critical step. Two files decide where root mounts after boot.</p>
<h2 data-number="5.1" id="updating-fstab"><span class="header-section-number">5.1</span> Updating fstab</h2>
<p>The SSD’s <code>/etc/fstab</code> must point root to the SSD, not the SD card. Use UUIDs, not device names.</p>
<p>Get the UUID of the SSD partition:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">blkid</span> /dev/sda1</span></code></pre></div>
<p>Output will look like:</p>
<pre><code>/dev/sda1: UUID="a1b2c3d4-e5f6-7890-abcd-ef1234567890" TYPE="ext4"</code></pre>
<p>Edit <code>/mnt/etc/fstab</code> (the fstab on the SSD, not the currently-running one):</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="fu">vi</span> /mnt/etc/fstab</span></code></pre></div>
<p>Replace the existing root entry with:</p>
<pre><code>UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890 / ext4 defaults 0 1</code></pre>
<p>UUIDs are stable. Device names like <code>/dev/sda1</code> can change when USB devices appear in a different order.</p>
<h2 data-number="5.2" id="updating-cmdline.txt"><span class="header-section-number">5.2</span> Updating cmdline.txt</h2>
<p>The SD bootloader reads <code>cmdline.txt</code> to find root. On Alpine for Raspberry Pi, it is at <code>/media/mmcblk0p1/cmdline.txt</code>.</p>
<p>Open it:</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="fu">vi</span> /media/mmcblk0p1/cmdline.txt</span></code></pre></div>
<p>Find the <code>root=</code> parameter. It currently points to the SD card’s root partition, something like:</p>
<pre><code>root=/dev/mmcblk0p2</code></pre>
<p>Replace that with the SSD’s UUID:</p>
<pre><code>root=UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890</code></pre>
<p>Add <code>rootwait</code> to the same line if it is not already there:</p>
<pre><code>root=UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890 rootwait</code></pre>
<p><code>rootwait</code> makes the kernel wait for USB storage before mounting root. Without it, boot can race USB detection and panic. For USB-attached root, this flag is essential.</p>
<p>Keep <code>cmdline.txt</code> on a single line. Line breaks can break boot.</p>
<h1 data-number="6" id="verifying-the-migration"><span class="header-section-number">6</span> Verifying the Migration</h1>
<p>Reboot:</p>
<div class="sourceCode" id="cb16"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="ex">reboot</span></span></code></pre></div>
<p>After the system comes back up, confirm which device is mounted as root:</p>
<div class="sourceCode" id="cb17"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb17-1"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a><span class="fu">df</span> <span class="at">-h</span> /</span>
<span id="cb17-2"><a href="#cb17-2" aria-hidden="true" tabindex="-1"></a><span class="fu">mount</span> <span class="kw">|</span> <span class="fu">grep</span> <span class="st">" / "</span></span></code></pre></div>
<p>Root should show <code>/dev/sda1</code> (or the UUID), not <code>mmcblk0p2</code>. If it still shows the SD card, recheck <code>cmdline.txt</code> and the UUID.</p>
<p>Confirm the SSD capacity is available:</p>
<div class="sourceCode" id="cb18"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="fu">df</span> <span class="at">-h</span> /</span></code></pre></div>
<p>Run a quick write test:</p>
<div class="sourceCode" id="cb19"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="fu">dd</span> if=/dev/zero of=/tmp/test bs=1M count=512 oflag=direct</span></code></pre></div>
<h1 data-number="7" id="what-to-do-with-the-sd-card-root-partition"><span class="header-section-number">7</span> What to Do with the SD Card Root Partition</h1>
<p>The old root on <code>mmcblk0p2</code> still exists. You have two choices.</p>
<p>Leave it as a fallback snapshot. If the SSD fails, point <code>cmdline.txt</code> back to <code>root=/dev/mmcblk0p2</code> and boot the old system.</p>
<p>Wipe it if you need space and trust the SSD setup. Delete the partition in <code>fdisk</code> and reuse the space, or keep a small recovery layout.</p>
<p>The boot partition (<code>mmcblk0p1</code>) must stay intact regardless. Without it, the Pi cannot boot at all.</p>
<h1 data-number="8" id="power-and-long-term-health"><span class="header-section-number">8</span> Power and Long-Term Health</h1>
<p>SSDs draw more power than SD cards. A 2.5” SATA SSD can pull 2 to 3W under load, and a Pi 4 can pull up to 6W. A busy setup can pass 10W sustained. Use at least a 15W supply (5V/3A), with 25W as a safer target. Weak power causes resets, USB drops, and corruption.</p>
<p>For SSD longevity, enable periodic TRIM. The <code>discard</code> mount option in <code>fstab</code> issues TRIM on every delete, which works but adds latency. The better approach is a scheduled <code>fstrim</code>:</p>
<div class="sourceCode" id="cb20"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb20-1"><a href="#cb20-1" aria-hidden="true" tabindex="-1"></a><span class="ex">fstrim</span> <span class="at">-v</span> /</span></code></pre></div>
<p>Run this weekly with cron or an OpenRC scheduled task. It helps keep performance steady as the drive fills.</p>
<p>An SD card with databases and Docker volumes is a countdown. This migration takes under an hour and turns the system into something you can trust.</p>
Comments
No comments yet
Be the first to comment!