73 lines
122 KiB
HTML
73 lines
122 KiB
HTML
|
<!DOCTYPE html> <html dir=auto style lang=en><!--
|
|||
|
Page saved with SingleFile
|
|||
|
url: https://thiagowfx.github.io/2022/01/alpine-linux-on-raspberry-pi-diskless-mode-with-persistent-storage/
|
|||
|
saved date: Thu Mar 09 2023 10:38:11 GMT+0100 (Central European Standard Time)
|
|||
|
--><meta charset=utf-8><meta http-equiv=x-ua-compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name=robots content="index, follow"><title>★ Alpine Linux on Raspberry Pi: Diskless Mode with persistent storage | Not Just Serendipity</title><meta name=keywords content=linux,selfhosted><meta name=description content="Use case: Given an Alpine Linux diskless1 installation meant for
|
|||
|
a Raspberry Pi setup, we would like to add a persistent storage component to it
|
|||
|
to make it survive across reboots."><meta name=author content="Thiago Perrotta"><link rel=canonical href=https://thiagowfx.github.io/2022/01/alpine-linux-on-raspberry-pi-diskless-mode-with-persistent-storage/><style>:root{--gap:24px;--content-gap:20px;--nav-width:1024px;--main-width:720px;--header-height:60px;--footer-height:60px;--radius:8px;--theme:rgb(255,255,255);--entry:rgb(255,255,255);--primary:rgb(30,30,30);--secondary:rgb(108,108,108);--tertiary:rgb(214,214,214);--content:rgb(31,31,31);--hljs-bg:rgb(28,29,33);--code-bg:rgb(245,245,245);--border:rgb(238,238,238)}.dark{--theme:rgb(29,30,32);--entry:rgb(46,46,51);--primary:rgb(218,218,219);--secondary:rgb(155,156,157);--tertiary:rgb(65,66,68);--content:rgb(196,196,197);--hljs-bg:rgb(46,46,51);--code-bg:rgb(55,56,62);--border:rgb(51,51,51)}*,::after,::before{box-sizing:border-box}html{-webkit-tap-highlight-color:transparent;overflow-y:scroll}a,button,body,h1,h2,h3{color:var(--primary)}body{font-size:18px;line-height:1.6;word-break:break-word;background:var(--theme)}article,footer,header,main{display:block}h1,h2,h3{line-height:1.2}h1,h2,h3,p{margin-top:0;margin-bottom:0}ul{padding:0}a{text-decoration:none}body,ul{margin:0}button{padding:0;font:inherit;background:0 0;border:0}button{cursor:pointer}img{max-width:100%}.footer,.top-link{font-size:12px;color:var(--secondary)}.footer{max-width:calc(var(--main-width) + var(--gap)*2);margin:auto;padding:calc((var(--footer-height) - var(--gap))/2) var(--gap);text-align:center;line-height:24px}.footer span{margin-inline-start:1px;margin-inline-end:1px}.footer span:last-child{white-space:nowrap}.footer a{color:inherit;border-bottom:1px solid var(--secondary)}.footer a:hover{border-bottom:1px solid var(--primary)}.top-link{position:fixed;bottom:60px;right:30px;z-index:99;background:var(--tertiary);width:42px;height:42px;padding:12px;border-radius:64px;transition:visibility .5s,opacity .8s linear}.top-link,.top-link svg{filter:drop-shadow(0 0 0 var(--theme))}.footer a:hover,.top-link:hover{color:var(--primary)}.top-link:focus,#theme-toggle:focus{outline:0}.nav{display:flex;flex-wrap:wrap;justify-content:space-between;max-width:calc(var(--nav-width) + var(--gap)*2);margin-inline-start:auto;margin-inline-end:auto;line-height:var(--header-height)}.nav a{display:block}.logo,#menu{display:flex;margin:auto var(--gap)}.logo{flex-wrap:inherit}.logo a{font-size:24px;font-weight:700}.logo a img{display:inline;vertical-align:middle;pointer-events:none;transform:translate(0,-10%);border-radius:6px;margin-inline-end:8px}button#theme-toggle{font-size:26px;margin:auto 4px}body.dark #moon{vertical-align:middle;display:none}body:not(.dark) #sun{display:none}#menu{list-style:none;word-break:keep-all;overflow-x:auto;white-space:nowrap}#menu li+li{margin-inline-start:var(--gap)}#menu a{font-size:16px}.logo-switches{display:inline-flex;margin:auto 4px}.logo-switches{flex-wrap:inherit}.main{position:relative;min-height:calc(100vh - var(--header-height) - var(--footer-height));max-width:calc(var(--main-width) + var(--gap)*2);margin:auto;padding:var(--gap)}code{direction:ltr}div.highlight,pre{position:relative}.post-header{margin:24px auto var(--content-gap)}.post-title{margin-bottom:2px;font-size:40px}.post-meta{color:var(--secondary);font-size:14px;display:flex;flex-wrap:wrap}.post-content{color:var(--content)}.post-content h3{margin:24px 0 16px}.post-content h2{margin:32px auto 24px;font-size:32px}.post-content h3{font-size:24px}.post-content a,.toc a:hover{box-shadow:0 1px;box-decoration-break:clone;-webkit-box-decoration-break:clone}.post-content ol,.post-content p,.post-content ul{margin-bottom:var(--content-gap)}.post-content ol,.post-content ul{padding-inline-start:20px}.post-content li{margin-top:5px}.post-content li p{margin-bottom:0}.post-content .highlight:not(table){margin:10px auto;background:var(--hljs-bg)!important;border-radius:var(--radius);direction:ltr}.post-content .highlight pre{margin:0}.post-content code{margin:auto 4px;padding:4px 6px;font-size:.78em;line-height:1.5;background:var(--code-bg);border-radius:2px}.post-content pre
|
|||
|
<link rel=icon type=image/png sizes=32x32 href="data:image/vnd.microsoft.icon;base64,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
|
|||
|
a Raspberry Pi setup, we would like to add a persistent storage component to it
|
|||
|
to make it survive across reboots."><meta property=og:type content=article><meta property=og:url content=https://thiagowfx.github.io/2022/01/alpine-linux-on-raspberry-pi-diskless-mode-with-persistent-storage/><meta property=article:section content=posts><meta property=article:published_time content=2022-01-15T23:18:56-05:00><meta property=article:modified_time content=2022-01-15T23:18:56-05:00><meta name=twitter:card content=summary><meta name=twitter:title content="★ Alpine Linux on Raspberry Pi: Diskless Mode with persistent storage"><meta name=twitter:description content="Use case: Given an Alpine Linux diskless1 installation meant for
|
|||
|
a Raspberry Pi setup, we would like to add a persistent storage component to it
|
|||
|
to make it survive across reboots."><script type=application/ld+json>{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Posts","item":"https://thiagowfx.github.io/posts/"},{"@type":"ListItem","position":2,"name":"★ Alpine Linux on Raspberry Pi: Diskless Mode with persistent storage","item":"https://thiagowfx.github.io/2022/01/alpine-linux-on-raspberry-pi-diskless-mode-with-persistent-storage/"}]}</script><script type=application/ld+json>{"@context":"https://schema.org","@type":"BlogPosting","headline":"★ Alpine Linux on Raspberry Pi: Diskless Mode with persistent storage","name":"★ Alpine Linux on Raspberry Pi: Diskless Mode with persistent storage","description":"Use case: Given an Alpine Linux diskless1 installation meant for a Raspberry Pi setup, we would like to add a persistent storage component to it to make it survive across reboots.\n","keywords":["linux","selfhosted"],"articleBody":"Use case: Given an Alpine Linux diskless1 installation meant for a Raspberry Pi setup, we would like to add a persistent storage component to it to make it survive across reboots.\nGoal The Alpine Linux Wiki covers most of the installation process, hence I will only document the bits that were lacking and/or confusing therein.\nMy use case is the following:\nGiven a Raspberry Pi 3B with an old 4GiB SD Card as CF storage2, install Alpine Linux in diskless mode. Find a way to preserve modifications in /etc and /var, as well as any installed packages through its apk package manager.\nLet’s follow the steps outlined in the wiki.\nCopy Alpine to the SD Card Grab the SD card and install Alpine Linux in it.\nAlpine provides officially supported images designed for the Raspberry Pi.\nMost Linux distributions provide an .iso or .img file to be installed with a tool like Balena Etcher, Rufus, Raspberry Pi Imager or plain dd3.\nAlpine is not like most Linux distributions: Instead, it provides a .tar.gz archive with files that should be copied directly to the SD card. Grab the latest version (3.15 at the time of this post) from https://alpinelinux.org/downloads/. There are 3 options:\narmhf: Works with all Pis, but may perform less optimally on recent versions.\narmv7: Works with the Pi 3B, 32-bit.\naarch64: Works with the Pi 3B, 64-bit.\nI opted for aarch64 to make it 64-bit, but armv7 would also have worked well for my setup. In fact, Raspberry Pi OS (Debian) uses armv7 (32-bit) at the time of this writing.\nBefore copying files over, format the SD Card. As I was doing this from a Windows machine because it was the only one I had readily available with a SD card slot, I just used the native Windows Disk Management tool to do so. I decided to allocate a 100MB4 FAT32 partition. The rest of the SD card would be blank for now. Alpine is surprisingly small, 100MB was more than enough for the kernel and other needed files.\nOnce the SD card is formatted, copy the files over to it. It turns out Windows cannot extract tarballs (.tar.gz); a tool like 7-zip should do the job. Copy the files over to the root of the newly allocated FAT32 partition, and then safely eject the SD card.\nBoot Alpine from the SD Card The next step is to insert the SD Card into the Pi and then boot. I had some trouble in this step and eventually figured out I didn’t mark the primary FAT32 partition as bootable. Unfortunately it’s not straightforward to mark the partition as bootable from Windows. On a Linux machine there’s a wide array of tools to do so: fdisk, cfdisk (TUI), sfdisk (scriptable fdisk), parted, gparted (GUI) are some of them. I worked around that by installing Raspberry Pi OS on the SD card with the Raspberry Pi imager, and then overwriting it with the Alpine files. This works because the Raspberry PI OS installation marks the FAT32 partition as bootable.\nInstall Alpine Installing Alpine is well documented in the wiki thus it won’t be covered here. It basically comes down to invoking setup-alpine, which then invokes other setup-* scripts.\nKeep in mind we’re not really “insta
|
|||
|
a Raspberry Pi setup, we would like to add a persistent storage component to it
|
|||
|
to make it survive across reboots.<h2 id=goal>Goal<a class="anchor sf-hidden" aria-hidden=true href=#goal hidden>#</a></h2><p>The <a href=https://wiki.alpinelinux.org/wiki/Installation>Alpine Linux Wiki</a> covers most of the installation process, hence I will only document the bits that were lacking and/or confusing therein.<p>My use case is the following:<blockquote><p>Given a Raspberry Pi 3B with an old 4GiB SD Card as CF storage<sup id=fnref:2><a href=#fn:2 class=footnote-ref role=doc-noteref>2</a></sup>, install Alpine Linux in diskless mode. Find a way to preserve modifications in <code>/etc</code> and <code>/var</code>, as well as any installed packages through its <code>apk</code> package manager.</p></blockquote><p>Let’s follow the steps outlined in the wiki.<h2 id=copy-alpine-to-the-sd-card>Copy Alpine to the SD Card<a class="anchor sf-hidden" aria-hidden=true href=#copy-alpine-to-the-sd-card hidden>#</a></h2><blockquote><p>Grab the SD card and install Alpine Linux in it.</p></blockquote><p>Alpine provides officially supported images designed for the Raspberry Pi.<p>Most Linux distributions provide an <code>.iso</code> or <code>.img</code> file to be installed with a tool like <a href=https://www.balena.io/etcher/>Balena Etcher</a>, <a href=https://rufus.ie/en/>Rufus</a>, <a href=https://www.raspberrypi.com/news/raspberry-pi-imager-imaging-utility/><strong>Raspberry Pi Imager</strong></a> or plain <code>dd</code><sup id=fnref:3><a href=#fn:3 class=footnote-ref role=doc-noteref>3</a></sup>.<p>Alpine is not like most Linux distributions: Instead, it provides a <code>.tar.gz</code> archive with files that should be copied directly to the SD card. Grab the latest version (3.15 at the time of this post) from <a href=https://alpinelinux.org/downloads/>https://alpinelinux.org/downloads/</a>. There are 3 options:<ul><li><p><code>armhf</code>: Works with all Pis, but may perform less optimally on recent versions.</p><li><p><code>armv7</code>: Works with the Pi 3B, 32-bit.</p><li><p><code>aarch64</code>: Works with the Pi 3B, 64-bit.</p></ul><p>I opted for <code>aarch64</code> to make it 64-bit, but <code>armv7</code> would also have worked well for my setup. In fact, Raspberry Pi OS (Debian) uses <code>armv7</code> (32-bit) at the time of this writing.<p>Before copying files over, format the SD Card. As I was doing this
|
|||
|
from a Windows machine because it was the only one I had readily available with
|
|||
|
a SD card slot, I just used the native Windows Disk Management tool to do so.
|
|||
|
I decided to allocate a 100MB<sup id=fnref:4><a href=#fn:4 class=footnote-ref role=doc-noteref>4</a></sup> FAT32 partition. The rest of the SD card would be
|
|||
|
blank for now. Alpine is surprisingly small, 100MB was more than enough for the kernel and other needed files.<p>Once the SD card is formatted, copy the files over to it. It turns out Windows cannot extract tarballs (<code>.tar.gz</code>); a tool like <a href=https://www.7-zip.org/>7-zip</a> should do the job. Copy the files over to the root of the newly allocated FAT32 partition, and then safely eject the SD card.<h2 id=boot-alpine-from-the-sd-card>Boot Alpine from the SD Card<a class="anchor sf-hidden" aria-hidden=true href=#boot-alpine-from-the-sd-card hidden>#</a></h2><p>The next step is to insert the SD Card into the Pi and then boot. I had some trouble in this step and eventually figured out I didn’t mark the primary FAT32 partition as bootable. Unfortunately it’s not straightforward to mark the partition as bootable from Windows. On a Linux machine there’s a wide array of tools to do so: <code>fdisk</code>, <code>cfdisk</code> (TUI), <code>sfdisk</code> (scriptable <code>fdisk</code>), <code>parted</code>, <code>gparted</code> (GUI) are some of them. I worked around that by installing Raspberry Pi OS on the SD card with the Raspberry Pi imager, and then overwriting it with the Alpine files. This works because the Raspberry PI OS installation marks the FAT32 partition as bootable.<h2 id=install-alpine>Install Alpine<a class="anchor sf-hidden" aria-hidden=true href=#install-alpine hidden>#</a></h2><p>Installing Alpine is well documented in the <a href=https://wiki.alpinelinux.org/wiki/Installation>wiki</a> thus it won’t be covered here. It basically comes down to invoking <code>setup-alpine</code>, which then invokes other <code>setup-*</code> scripts.<p>Keep in mind we’re not really “installing” Alpine as this is a diskless installation. A more accurate term here would be “configuring”.<p>Before invoking the installation script, I created a second primary partition in the SD card, set to <code>ext4</code>:<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class="language-shell hljs" data-lang=shell><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Configure networking to get working internet access.</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> setup-interfaces</span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Install some partitioning tools.</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> apk add cfdisk e2fsprogs</span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Create a second partition (mmcblk0p2) and write it.</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> cfdisk /dev/mmcblk0</span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Format the partition as ext4.</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> mkfs.ext4 /dev/mmcblk0p2</span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Mount the partition under /media.</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> mount /dev/mmcblk0p2 /media/mmcblk0p2</span>
|
|||
|
</span></span></code></pre><button class="copy-code sf-hidden">copy</button></div><p>The installation is straightforward, we just need to pay attention to a few select steps:<ul><li><code>setup-disk</code>: Select <code>none</code> to ensure a <code>diskless</code> installation<sup id=fnref:5><a href=#fn:5 class=footnote-ref role=doc-noteref>5</a></sup>.<li><code>setup-apkcache</code>: Select <code>/media/mmcblk0p2/cache</code> to persist downloaded <code>apk</code> packages.<li><code>setup-lbu</code>: Edit <code>/etc/lbu/lbu.conf</code> and set <code>LBU_MEDIA="mmcblk0p2"</code>. Note: Do not add <code>/media</code> as it is implicit.</ul><p>Once the installation is complete, run <code>lbu commit</code> to persist the changes in the second partition. Once you do so, a <code><hostname>.apkovl.tar.gz</code><sup id=fnref:6><a href=#fn:6 class=footnote-ref role=doc-noteref>6</a></sup> file should materialize on <code>/media/mmcblk0p2/</code>.<p>This is a good moment to reboot. Before we do so, let’s cache the packages we had previously downloaded.<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class="language-shell hljs" data-lang=shell><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Cache packages.</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> apk cache download</span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> reboot</span>
|
|||
|
</span></span></code></pre><button class="copy-code sf-hidden">copy</button></div><h2 id=after-the-first-reboot>After the first reboot<a class="anchor sf-hidden" aria-hidden=true href=#after-the-first-reboot hidden>#</a></h2><p>If everything worked as expected, once you reboot all your previously installed packages should have been preserved and automatically restored / reinstalled, as well as your modifications done to <code>/etc</code>.<p>From this point on, whenever you install a new package that you want to be preserved for subsequent reboots, run <code>lbu commit</code> afterwards. For example:<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class="language-shell hljs" data-lang=shell><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> apk add vim</span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> lbu commit</span>
|
|||
|
</span></span></code></pre><button class="copy-code sf-hidden">copy</button></div><p>If you would like to see what is going to be committed, run <code>lbu status</code> or <code>lbu diff</code> before doing the actual commit. Whenever you commit, <code>/media/mmcblk0p2/<hostname>.apkovl.tar.gz</code> gets overwritten with your most recent modifications.<p>It’s possible to keep more than one backup file by changing <code>BACKUP_LIMIT=</code> in <code>/etc/lbu/lbu.conf</code>. This is specially handy if you decide to revert to an earlier system snapshot / state later on. The stock config looks like this:<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class="language-shell hljs" data-lang=shell><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> cat /etc/lbu/lbu.conf</span>
|
|||
|
</span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> what cipher to use with -e option</span></span>
|
|||
|
</span></span><span style=display:flex><span>DEFAULT_CIPHER<span style=color:#f92672>=</span>aes-256-cbc
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Uncomment the row below to encrypt config by default</span></span>
|
|||
|
</span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> ENCRYPTION=<span class=hljs-variable>$DEFAULT_CIPHER</span></span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Uncomment below to avoid <media> option to <span class=hljs-string>'lbu commit'</span></span></span>
|
|||
|
</span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Can also be <span class=hljs-built_in>set</span> to <span class=hljs-string>'floppy'</span></span></span>
|
|||
|
</span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> LBU_MEDIA=usb</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Set the LBU_BACKUPDIR variable <span class=hljs-keyword>in</span> <span class=hljs-keyword>case</span> you prefer to save the apkovls</span></span>
|
|||
|
</span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> <span class=hljs-keyword>in</span> a normal directory instead of mounting an external media.</span></span>
|
|||
|
</span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> LBU_BACKUPDIR=/root/config-backups</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Uncomment below to <span class=hljs-built_in>let</span> lbu make up to 3 backups</span></span>
|
|||
|
</span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> BACKUP_LIMIT=3</span></span>
|
|||
|
</span></span></code></pre><button class="copy-code sf-hidden">copy</button></div><p><strong>Tip</strong>: You can find the list of all explicitly installed packages in <code>/etc/apk/world</code>.<h2 id=the-last-piece-make-var-persistent>The last piece: make /var persistent<a class="anchor sf-hidden" aria-hidden=true href=#the-last-piece-make-var-persistent hidden>#</a></h2><p>There are three natural ways that come to mind to make <code>/var</code> persistent:<h3 id=a-separate-partition-or-file>A) Separate partition (or file)<a class="anchor sf-hidden" aria-hidden=true href=#a-separate-partition-or-file hidden>#</a></h3><p>Instead of two partitions (FAT32 and ext4), create 3 partitions: FAT32, ext4 and ext4. Use the latter one to mount <code>/var</code> on, saving this information in <code>/etc/fstab</code>. The main disadvantage of this setup is that you’ll need to allocate a fixed amount of space of each of the ext4 partitions and it may be difficult to figure out how to split the space between them.<p>A variant of this approach is to just create the third partition as a file:<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class="language-shell hljs" data-lang=shell><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> 500MB file</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> dd </span><span style=color:#66d9ef><span class=bash><span class=hljs-keyword>if</span></span></span><span style=color:#f92672><span class=bash>=</span></span><span class=bash>/dev/zero of</span><span style=color:#f92672><span class=bash>=</span></span><span class=bash>/media/mmcblk0p2/var.img bs</span><span style=color:#f92672><span class=bash>=</span></span><span class=bash>1M count</span><span style=color:#f92672><span class=bash>=</span></span><span style=color:#ae81ff><span class=bash>500</span></span><span class=bash> status</span><span style=color:#f92672><span class=bash>=</span></span><span class=bash>progress</span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> mkfs.ext4 /media/mmcblk0p2/var.img</span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> mount /media/mmcblk0p2/var.img /var</span>
|
|||
|
</span></span></code></pre><button class="copy-code sf-hidden">copy</button></div><p>This works because the Linux kernel supports mounting files as if they were device blocks, treating them as loop devices (pseudo-devices).<p>I don’t like these approaches because they shadow the preexisting <code>/var</code> from the boot media, which in turn messes up with existing services that use it such as <code>cron</code>: <code>% crontab -l</code> would fail. One workaround would be to mount a <code>/var</code> subdirectory instead: for example, <code>/var/lib/docker</code> for docker.<h3 id=b-bind-mount>B) Bind mount<a class="anchor sf-hidden" aria-hidden=true href=#b-bind-mount hidden>#</a></h3><p>This one is straightforward:<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class="language-shell hljs" data-lang=shell><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> mount --<span class=hljs-built_in>bind</span> /media/mmcblk0p2/var/lib/docker /var/lib/docker</span>
|
|||
|
</span></span></code></pre><button class="copy-code sf-hidden">copy</button></div><p>The actual partition lives in the SD card, however we make a bind mount under
|
|||
|
<code>/var</code>, which is like an <em>alias</em>. From <a href=https://unix.stackexchange.com/questions/198590/what-is-a-bind-mount>Stack Exchange</a>:<blockquote><p>A bind mount is an alternate view of a directory tree. Classically, mounting creates a view of a storage device as a directory tree. A bind mount instead takes an existing directory tree and replicates it under a different point. The directories and files in the bind mount are the same as the original. Any modification on one side is immediately reflected on the other side, since the two views show the same data.</p></blockquote><h3 id=c-overlay-mount>C) Overlay mount<a class="anchor sf-hidden" aria-hidden=true href=#c-overlay-mount hidden>#</a></h3><p>From <a href=https://wiki.archlinux.org/title/Overlay_filesystem>ArchWiki</a>:<blockquote><p>Overlayfs allows one, usually read-write, directory tree to be overlaid onto another, read-only directory tree. All modifications go to the upper, writable layer. This type of mechanism is most often used for live CDs but there is a wide variety of other uses.</p></blockquote><p>It’s perfect for our use case, which uses a live bootable SD card for Alpine. It blends the preexisting, ephemeral, in-memory <code>/var</code> with the persistent in-disk <code>/var</code>.<p>I wanted to mount <code>/var</code> directly but found it to be problematic for the same reasons mentioned earlier, therefore I just went with <code>/var/lib/docker</code> instead:<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class="language-shell hljs" data-lang=shell><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Create overlay upper and work directories.</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> mkdir -p /media/mmcblk0p2/var/lib/docker /media/mmcblk0p2/var/lib/docker-work</span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Add mountpoint entry to fstab. Note: The work dir must be an empty directory <span class=hljs-keyword>in</span> the same filesystem mount as the upper directory.</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> <span class=hljs-built_in>echo</span> </span><span style=color:#e6db74><span class=bash><span class=hljs-string>"overlay /var/lib/docker overlay lowerdir=/var/lib/docker,upperdir=/media/mmcblk0p2/var/lib/docker,workdir=/media/mmcblk0p2/var/lib/docker-work 0 0"</span></span></span><span class=bash> >> /etc/fstab</span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>
|
|||
|
</span></span></span><span style=display:flex><span><span style=color:#75715e><span class=hljs-meta>#</span><span class=bash> Mount all fstab entries, including our newly added one.</span></span>
|
|||
|
</span></span><span style=display:flex><span><span class=hljs-meta>%</span><span class=bash> mount -a</span>
|
|||
|
</span></span></code></pre><button class="copy-code sf-hidden">copy</button></div><h2 id=conclusion>Conclusion<a class="anchor sf-hidden" aria-hidden=true href=#conclusion hidden>#</a></h2><p>I opted for the third approach, using an overlay mount, it was the most
|
|||
|
seamless one. A bind mount would have been fine as well.<p>The final setup works surprisingly well:<ul><li>Alpine Linux is very lightweight and runs mostly from RAM<li><code>apk</code> cache is persistent to the ext4 partition<li><code>/var/</code> is persistent to the ext4 partition<li><code>lbu commit</code> persists changes in <code>/etc/</code> and <code>/home/</code> in the ext4 partition<li>Every reboot fully resets the system sans persistent components above</ul><h2 id=references>References<a class="anchor sf-hidden" aria-hidden=true href=#references hidden>#</a></h2><ul><li><a href=https://vincentserpoul.github.io/post/alpine-linux-rpi0/>https://vincentserpoul.github.io/post/alpine-linux-rpi0/</a><li><a href=http://dahl-jacobsen.dk/tips/blog/2021-04-08-docker-on-alpine-linux/>http://dahl-jacobsen.dk/tips/blog/2021-04-08-docker-on-alpine-linux/</a><li><a href=http://dahl-jacobsen.dk/tips/blog/2018-03-15-alpine-on-raspberry-pi/>http://dahl-jacobsen.dk/tips/blog/2018-03-15-alpine-on-raspberry-pi/</a></ul><div class=footnotes role=doc-endnotes><hr><ol><li id=fn:1><p>Running (almost) fully from RAM. <a href=#fnref:1 class=footnote-backref role=doc-backlink>↩︎</a></p><li id=fn:2><p>CF = Compact disk. <a href=#fnref:2 class=footnote-backref role=doc-backlink>↩︎</a></p><li id=fn:3><p>On Linux I’d usually opt for <code>dd</code>, on Windows the Raspberry Pi Imager is a sensible choice. <a href=#fnref:3 class=footnote-backref role=doc-backlink>↩︎</a></p><li id=fn:4><p>100MB is overly conservative, but keep in mind I had a very small SD Card, with only 4GiB storage. 250MB or even 500MB should be a more sensible default if you have a bigger SD Card (e.g. 32GiB). <a href=#fnref:4 class=footnote-backref role=doc-backlink>↩︎</a></p><li id=fn:5><p>An alternative is to select <code>data</code> disk mode, but it didn’t work for me. <a href=#fnref:5 class=footnote-backref role=doc-backlink>↩︎</a></p><li id=fn:6><p><em>ovl</em> is short for <em>overlay</em>. Not to be confused with <em>vol</em> for <em>volume</em>. <a href=#fnref:6 class=footnote-backref role=doc-backlink>↩︎</a></p></ol></div></div><footer class=post-footer><ul class=post-tags><li><a href=https://thiagowfx.github.io/tags/linux/>linux</a><li><a href=https://thiagowfx.github.io/tags/selfhosted/>selfhosted</a></ul></footer><div style=text-align:center><a href="mailto:tbperrotta@gmail.com?subject=RE: Not%20Just%20Serendipity comment for '%e2%98%85%20Alpine%20Linux%20on%20Raspberry%20Pi%3a%20Diskless%20Mode%20with%20persistent%20storage'" target=_blank><button>Reply via email</button></a></div></article></main><footer class=footer><span>Copyright © 2021 - 2023 Thiago Perrotta • <a href=https://creativecommons.org/licenses/by-nc-sa/4.0/>CC BY-NC-SA 4.0</a> • <a href=https://thiagowfx.github.io/index.xml>RSS</a> •</span>
|
|||
|
<span>Powered by
|
|||
|
<a href=https://gohugo.io/ rel="noopener noreferrer" target=_blank>Hugo</a> &
|
|||
|
<a href=https://github.com/adityatelange/hugo-PaperMod/ rel=noopener target=_blank>PaperMod</a></span></footer><a href=#top aria-label="go to top" title="Go to Top (Alt + G)" class=top-link id=top-link accesskey=g style=visibility:visible;opacity:1><svg xmlns=http://www.w3.org/2000/svg viewBox="0 0 12 6" fill=currentcolor><path d="M12 6H0l6-6z"></path></svg></a><script data-template-shadow-root>(()=>{document.currentScript.remove();processNode(document);function processNode(node){node.querySelectorAll("template[shadowroot]").forEach(element=>{let shadowRoot = element.parentElement.shadowRoot;if (!shadowRoot) {try {shadowRoot=element.parentElement.attachShadow({mode:element.getAttribute("shadowroot")});shadowRoot.innerHTML=element.innerHTML;element.remove()} catch (error) {} if (shadowRoot) {processNode(shadowRoot)}}})}})()</script>
|