<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Btrfs | Filippo Berto</title><link>https://bertof.gitlab.io/tag/btrfs/</link><atom:link href="https://bertof.gitlab.io/tag/btrfs/index.xml" rel="self" type="application/rss+xml"/><description>Btrfs</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Wed, 22 Jul 2026 09:03:33 +0200</lastBuildDate><image><url>https://bertof.gitlab.io/media/icon_hu_29dc86bd9197aa17.png</url><title>Btrfs</title><link>https://bertof.gitlab.io/tag/btrfs/</link></image><item><title>Garage S3: LMDB Corruption Post-Mortem</title><link>https://bertof.gitlab.io/post/garage-s3-lmdb-corruption-postmortem/</link><pubDate>Wed, 22 Jul 2026 09:03:33 +0200</pubDate><guid>https://bertof.gitlab.io/post/garage-s3-lmdb-corruption-postmortem/</guid><description>&lt;p&gt;Yesterday I was migrating my data from an rclone-mounted bucket to a proper &lt;a href="https://opencloud.eu/" target="_blank" rel="noopener"&gt;OpenCloud&lt;/a&gt; setup backed by my home &lt;a href="https://garagehq.deuxfleurs.fr/" target="_blank" rel="noopener"&gt;Garage&lt;/a&gt; S3 cluster. Things were going smoothly: files copying over, users importing, the new stack finally taking shape.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Then the power went out.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When the server came back, Garage refused to start. The LMDB metadata database was completely corrupted, rendering the cluster inoperable. The block data (781k objects, ~57 GB) remained intact on a Btrfs RAID1, but without the LMDB index the mapping from S3 keys to block hashes was lost. The data in my personal bucket and the one used by OpenCloud as its storage backend: gone.&lt;/p&gt;
&lt;p&gt;It could have been worse. My most important data lives outside S3, backed by restic to a different target, and that backup pipeline was unaffected. Still, losing two buckets stings.&lt;/p&gt;
&lt;p&gt;That said, I didn&amp;rsquo;t choose S3 on a whim. This bucket needed to be accessed from both my homelab and several VPSs hosting public services, a mix of edge and cloud. S3 gives me a single consistent API and authentication layer everywhere, and Garage&amp;rsquo;s S3 gateway exposes it without vendor lock-in (unlike, &lt;em&gt;cough&lt;/em&gt;, certain other open-source S3 implementations). A local filesystem wouldn&amp;rsquo;t solve that. I like the option to add nodes for replication down the road, but a crash like this makes you question whether the complexity is worth it for a single-node deployment.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: LMDB is known to be vulnerable to corruption after an unclean shutdown. Garage provides a built-in snapshot mechanism (&lt;code&gt;metadata_auto_snapshot_interval&lt;/code&gt;) exactly for this scenario. I hadn&amp;rsquo;t enabled it. I also had no Btrfs snapshots on the metadata partition. If you run Garage on LMDB, enable snapshots &lt;strong&gt;today&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="system-layout"&gt;System Layout&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Metadata LMDB&lt;/strong&gt;: single-device Btrfs partition (&lt;code&gt;/dev/nvme0n1p2&lt;/code&gt;, 238 GiB NVMe, 98% full)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Block data&lt;/strong&gt;: Btrfs RAID1 (&lt;code&gt;/mnt/raid/&lt;/code&gt;, 5.5 TiB, HDDs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Garage version&lt;/strong&gt;: v2.x, &lt;code&gt;db_engine = &amp;quot;lmdb&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;COW&lt;/strong&gt;: enabled on both partitions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapshots/backups&lt;/strong&gt;: none for the metadata directory&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="timeline"&gt;Timeline&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;T-unknown&lt;/strong&gt;: Power outage. Server loses power mid-operation. Garage was writing to the LMDB database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T=0&lt;/strong&gt;: Server boots back up. Garage refuses to start. Attempting to inspect the LMDB file:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ mdb_stat -e /var/lib/garage/meta/db.lmdb/data.mdb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mdb_stat: src/mdb.c:...: Assertion &lt;span class="s1"&gt;&amp;#39;IS_BRANCH(mc-&amp;gt;mc_pg[mc-&amp;gt;mc_top])&amp;#39;&lt;/span&gt; failed.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Aborted
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ mdb_copy -c /var/lib/garage/meta/db.lmdb/data.mdb /tmp/recovery.mdb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mdb_copy: src/mdb.c:...: Assertion &lt;span class="s1"&gt;&amp;#39;IS_BRANCH(mc-&amp;gt;mc_pg[mc-&amp;gt;mc_top])&amp;#39;&lt;/span&gt; failed.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Aborted
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The LMDB b-tree is structurally gone. I sat there staring at the terminal for a good minute. No error message to decode, no partial recovery: just a dead database and 57 GB of unlabeled blocks on the RAID. Garage&amp;rsquo;s own docs &lt;a href="https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine" target="_blank" rel="noopener"&gt;warn about this&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;LMDB is prone to database corruption after an unclean shutdown (e.g. a process kill or a power outage).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;T+1h&lt;/strong&gt;: Garage service migrated to a fresh cluster on a different machine. Block data copied from the RAID. OpenCloud is re-targeted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T+2h&lt;/strong&gt;: Btrfs COW recovery attempted. All 10 candidate tree roots (generations 2477737–2477773) contain the same corrupt extents.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T+4h&lt;/strong&gt;: LMDB raw page scan. 956k pages, all &lt;code&gt;flags=0x0000&lt;/code&gt;, &lt;code&gt;lower=1&lt;/code&gt;, &lt;code&gt;upper≈24&lt;/code&gt;. No valid root page. The database was effectively erased.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T+24h&lt;/strong&gt;: Recovery abandoned. Data re-seeded from scratch where possible.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="root-cause"&gt;Root Cause&lt;/h2&gt;
&lt;p&gt;A power outage interrupted Garage mid-write on the LMDB metadata database. LMDB uses &lt;code&gt;mmap&lt;/code&gt;&amp;rsquo;d I/O and, by default, operates with &lt;code&gt;MDB_NOMETASYNC&lt;/code&gt; and &lt;code&gt;MDB_NOSYNC&lt;/code&gt; (Garage sets &lt;code&gt;metadata_fsync = false&lt;/code&gt; by default). Without &lt;code&gt;fsync&lt;/code&gt;, a power loss during a transaction commit leaves the b-tree in an inconsistent state: torn pages, missing branches, zero-filled root pointers.&lt;/p&gt;
&lt;p&gt;This is a &lt;a href="https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine" target="_blank" rel="noopener"&gt;well-documented limitation&lt;/a&gt; of LMDB, not a Garage bug. The recovery path Garage documents is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Replication&lt;/strong&gt; (requires &lt;code&gt;replication_factor ≥ 2&lt;/code&gt; and multiple nodes): delete the corrupt DB, restart, and run &lt;code&gt;garage repair -a --yes tables&lt;/code&gt; to resync from peers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metadata snapshots&lt;/strong&gt; (&lt;code&gt;metadata_auto_snapshot_interval&lt;/code&gt;): restore from a Garage-generated snapshot, then repair.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filesystem snapshots&lt;/strong&gt; (Btrfs/ZFS): restore from an old snapshot of the metadata directory.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I had none of these. I didn&amp;rsquo;t know about the snapshot feature; I hadn&amp;rsquo;t read the docs carefully enough. If I had, this post wouldn&amp;rsquo;t exist.&lt;/p&gt;
&lt;h2 id="recovery-attempts"&gt;Recovery Attempts&lt;/h2&gt;
&lt;h3 id="btrfs-cow-recovery"&gt;Btrfs COW recovery&lt;/h3&gt;
&lt;p&gt;The LMDB file had COW enabled. Each page write creates a new extent at a different physical location; old page data remains on disk until overwritten.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ btrfs-find-root /dev/nvme0n1p2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Well block &lt;span class="m"&gt;174354268160&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;gen: 2477773&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Well block &lt;span class="m"&gt;174354300928&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;gen: 2477772&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Found tree root at &lt;span class="m"&gt;174435041280&lt;/span&gt; gen &lt;span class="m"&gt;2477773&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Found tree root at &lt;span class="m"&gt;174433533952&lt;/span&gt; gen &lt;span class="m"&gt;2477772&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Found tree root at &lt;span class="m"&gt;172099076096&lt;/span&gt; gen &lt;span class="m"&gt;2477739&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;10 candidate tree roots identified, spanning generations &lt;strong&gt;2477737–2477773&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Each root was walked to find &lt;code&gt;EXTENT_DATA&lt;/code&gt; items for the LMDB inode. The extent map had not changed significantly between generations; only the final extent was COW&amp;rsquo;d at gen 2477738, and all earlier extents pointed to the same (corrupt) physical blocks.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ btrfs restore -t &amp;lt;root&amp;gt; /dev/nvme0n1p2 /tmp/recovery/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# btrfs restore cannot traverse FS_TREE roots at b-tree level 3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Direct file extraction from old generations was not possible.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ btrfs restore -r /dev/nvme0n1p2 /tmp/recovery/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Produces the same corrupt file; the inode&amp;#39;s extent map had not reverted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;: The corruption happened before any Btrfs snapshot could diverge. The old page data was in Btrfs free space with no index to locate it. COW alone is not a backup. You need explicit snapshots.&lt;/p&gt;
&lt;h3 id="lmdb-raw-page-analysis"&gt;LMDB raw page analysis&lt;/h3&gt;
&lt;p&gt;A custom scanner was written to walk the LMDB file and recover any valid b-tree pages. This felt promising at first: surely some pages survived. Scanning through the raw bytes, looking for recognizable LMDB page headers, hoping for a valid root.&lt;/p&gt;
&lt;p&gt;This is exactly the kind of task where AI tools shine. You have a stable read-only copy of the corrupt file, a well-defined binary format, and a one-off analysis script you&amp;rsquo;ll never need again. Perfect for throwing at an LLM and iterating on the output.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Total pages: ~956,000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Leaf pages: 983
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Branch pages: 304
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Overflow: 1,938
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Root pages:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Active root at position 956,702 → points to zero-filled page
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Last root at position 956,708 → points to zero-filled page
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Freelist:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; freeid counters: 1, 2 → fully-allocated file, no released pages
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;All pages:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; flags = 0x0000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; lower = 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; upper ≈ 24 ← metadata pointer structure erased
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The corruption pattern is consistent with &lt;code&gt;mdb_env_open(MDB_CREATE)&lt;/code&gt; initializing a new empty database over the existing file. The file was re-opened after the crash with no readable b-tree root.&lt;/p&gt;
&lt;p&gt;This is a deliberate LMDB design choice: &lt;code&gt;MDB_CREATE&lt;/code&gt; makes &lt;code&gt;mdb_env_open&lt;/code&gt; silently create or overwrite the database file if the header is unreadable, rather than returning an error. The rationale is performance: LMDB avoids a full integrity scan at startup and trusts the OS to have flushed pages correctly. If you used &lt;code&gt;metadata_fsync = true&lt;/code&gt;, LMDB would have at least synced the meta page, giving the header enough integrity to survive a crash. But with the default &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;MDB_NOSYNC&lt;/code&gt; in play, a torn meta page looks exactly like an uninitialized file, and LMDB&amp;rsquo;s fail-safe is to start fresh rather than halt. It&amp;rsquo;s a speed-over-safety tradeoff that makes sense for the read-optimized workloads LMDB was built for, but backfires catastrophically when power is lost mid-write.&lt;/p&gt;
&lt;h3 id="orphaned-block-extraction-not-executed"&gt;Orphaned block extraction (not executed)&lt;/h3&gt;
&lt;p&gt;781k orphaned zstd-compressed block files (~57 GB) exist on the RAID. I could see them: &lt;code&gt;ls /mnt/raid/garage/data/&lt;/code&gt; dumped thousands of hash-named files. But without the LMDB mapping of &lt;code&gt;(bucket, key) → block hash&lt;/code&gt;, blocks cannot be attributed to objects. A content-based classification (magic bytes → file extension) was considered but, at some point, you have to accept the loss and move on.&lt;/p&gt;
&lt;h2 id="lessons-learned"&gt;Lessons Learned&lt;/h2&gt;
&lt;h3 id="what-went-wrong"&gt;What went wrong&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No metadata snapshots&lt;/strong&gt;: Garage has provided &lt;code&gt;metadata_auto_snapshot_interval&lt;/code&gt; since v0.9.4. &lt;em&gt;I hadn&amp;rsquo;t enabled it&lt;/em&gt;. This single config line would have made recovery trivial: stop Garage, swap in the snapshot, run &lt;code&gt;garage repair -a --yes tables&lt;/code&gt;, done. Honestly, with LMDB being the default engine and its corruption-on-power-loss behavior being &lt;a href="https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#db_engine" target="_blank" rel="noopener"&gt;documented&lt;/a&gt;, this option should be enabled by default.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No Btrfs snapshots on the metadata partition&lt;/strong&gt;: the RAID had snapper timeline snapshots, but the metadata partition on the NVMe didn&amp;rsquo;t. A Btrfs snapshot on the metadata directory would have been a second line of defense.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No replication&lt;/strong&gt;: a single-node cluster with &lt;code&gt;replication_factor = 1&lt;/code&gt; means no peer to resync from. With just one additional node, I could have deleted the corrupt DB and recovered automatically.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No monitoring&lt;/strong&gt;: no alerting for Garage crash loops or LMDB assertion failures. I discovered the issue when my laptop was showing an empty folder instead of the mounted bucket. Then OpenCloud started returning 503s.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="what-went-well"&gt;What went well&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Btrfs RAID1 preserved block data&lt;/strong&gt;: all block data (~57 GB) was intact and transferable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;restic backups outside S3&lt;/strong&gt;: my critical data lives outside S3 and is backed by restic to a separate target. That pipeline was unaffected. The data I actually care about is safe.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapper timelines on RAID&lt;/strong&gt;: &lt;code&gt;/mnt/raid/&lt;/code&gt; subvolumes had Btrfs snapshot timelines (though the metadata wasn&amp;rsquo;t on RAID, so this didn&amp;rsquo;t help).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="preventive-measures-implemented"&gt;Preventive Measures Implemented&lt;/h2&gt;
&lt;h3 id="garage-metadata-snapshots"&gt;Garage metadata snapshots&lt;/h3&gt;
&lt;p&gt;Enabled Garage&amp;rsquo;s built-in snapshot mechanism. The snapshots are stored on the same RAID subvolume that already has snapper timeline snapshots, so they inherit Btrfs-level protection automatically:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# /etc/garage/config.toml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;metadata_auto_snapshot_interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;6h&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;metadata_snapshots_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/mnt/raid/garage/snapshots&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;metadata_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/var/lib/garage/meta&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/mnt/raid/garage/data&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Garage keeps the two most recent snapshots and deletes older ones automatically. Recovery from a snapshot is &lt;a href="https://garagehq.deuxfleurs.fr/documentation/operations/recovering/#corrupted_meta" target="_blank" rel="noopener"&gt;straightforward&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /var/lib/garage/meta
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv db.lmdb db.lmdb.bak
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp -r snapshots/2026-07-22T06:00:00Z db.lmdb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl restart garage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;garage repair -a --yes tables
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Garage docs &lt;a href="https://garagehq.deuxfleurs.fr/documentation/operations/recovering/#corrupted_meta" target="_blank" rel="noopener"&gt;point out&lt;/a&gt; that filesystem-level snapshots may be corrupted if taken during a write. Garage&amp;rsquo;s own snapshots are crash-consistent and should be your primary recovery path. Btrfs snapshots are a best-effort secondary line.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="future-recommendations"&gt;Future Recommendations&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Btrfs snapshots on the metadata partition&lt;/strong&gt;: add snapper timeline snapshots on &lt;code&gt;/var/lib/garage/meta&lt;/code&gt; as a second layer of defense:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapper -c garage-meta create-config /var/lib/garage/meta
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scheduled metadata export&lt;/strong&gt;: &lt;code&gt;garage meta dump&lt;/code&gt; to a restic or S3 target:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&lt;/span&gt;garage meta dump /tmp/garage-meta-dump.json
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;restic backup /tmp/garage-meta-dump.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Monitoring&lt;/strong&gt;: alert on Garage process crashes and LMDB integrity. A simple systemd &lt;code&gt;OnFailure&lt;/code&gt; hook would catch crash loops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-node replication&lt;/strong&gt;: even a second low-power node with &lt;code&gt;replication_factor = 2&lt;/code&gt; would eliminate the single point of failure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Move metadata to RAID&lt;/strong&gt;: migrating the metadata directory to the Btrfs RAID alongside block data would simplify backup and recovery to a single snapper configuration.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="roundup"&gt;Roundup&lt;/h2&gt;
&lt;p&gt;LMDB corruption after power loss is a known issue. Garage ships with the tools to prevent this (&lt;code&gt;metadata_auto_snapshot_interval&lt;/code&gt;, Btrfs/ZFS snapshot support, and multi-node replication), but none of them help if they aren&amp;rsquo;t configured. And I still think &lt;code&gt;metadata_auto_snapshot_interval&lt;/code&gt; should default to something like &lt;code&gt;&amp;quot;6h&amp;quot;&lt;/code&gt; when &lt;code&gt;db_engine = &amp;quot;lmdb&amp;quot;&lt;/code&gt;. It&amp;rsquo;s a footgun that costs users their data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you run Garage on LMDB, stop now and check:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Is &lt;code&gt;metadata_auto_snapshot_interval&lt;/code&gt; set? If not, add it to your &lt;code&gt;garage.toml&lt;/code&gt; and restart.&lt;/li&gt;
&lt;li&gt;Are the snapshots stored somewhere with redundancy? Set &lt;code&gt;metadata_snapshots_dir&lt;/code&gt; to a path on your RAID.&lt;/li&gt;
&lt;li&gt;Do you have Btrfs/ZFS snapshots on the metadata directory? If you use snapper, add a config for it.&lt;/li&gt;
&lt;li&gt;Can you survive losing a single node? If not, add a second node and bump &lt;code&gt;replication_factor&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The cost of these precautions is near-zero. The cost of skipping them is an evening of recovery work and a permanent knot in your stomach when you see a thunderstorm approaching.&lt;/p&gt;
&lt;p&gt;This is not pro-grade hardware in a datacenter with redundant power. It&amp;rsquo;s a home server plugged into a wall outlet. Failures are not just possible. They&amp;rsquo;re expected. If your data matters, act like it.&lt;/p&gt;</description></item></channel></rss>