<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="http://0.0.0.0:4000/feed.xml" rel="self" type="application/atom+xml" /><link href="http://0.0.0.0:4000/" rel="alternate" type="text/html" /><updated>2026-04-07T04:35:24+00:00</updated><id>http://0.0.0.0:4000/feed.xml</id><title type="html">Andy’s Thoughts</title><subtitle>it&apos;s like /dev/null but with more content</subtitle><author><name>Andrew Denner</name></author><entry><title type="html">Falcon Perception: 600 Million Parameters That Embarrass Models 50 Times Their Size (and What That Means for the Rest of Us)</title><link href="http://0.0.0.0:4000/2026/04/05/Falcon.html" rel="alternate" type="text/html" title="Falcon Perception: 600 Million Parameters That Embarrass Models 50 Times Their Size (and What That Means for the Rest of Us)" /><published>2026-04-05T15:35:14+00:00</published><updated>2026-04-05T15:35:14+00:00</updated><id>http://0.0.0.0:4000/2026/04/05/Falcon</id><content type="html" xml:base="http://0.0.0.0:4000/2026/04/05/Falcon.html"><![CDATA[<h1 id="falcon-perception-600-million-parameters-that-embarrass-models-50-times-their-size-and-what-that-means-for-the-rest-of-us">Falcon Perception: 600 Million Parameters That Embarrass Models 50 Times Their Size (and What That Means for the Rest of Us)</h1>

<p>Posted by Andrew Denner on April 05, 2026 · 20 mins read</p>

<p><em>Note: This post expands on <a href="https://www.marktechpost.com/2026/04/03/tii-releases-falcon-perception-a-0-6b-parameter-early-fusion-transformer-for-open-vocabulary-grounding-and-segmentation-from-natural-language-prompts/">TII’s Falcon Perception release</a> from April 3, 2026. I ran the analysis through my usual multi-tool stack — Claude for the deep dive, Grok for adversarial pressure-testing — because one AI’s opinion is just vibes. Two AIs arguing with each other starts to look like peer review. Opinions are my own.</em></p>

<hr />

<p>I have a confession: I spent last Thursday night reading a paper about positional embeddings that uses the golden ratio to achieve isotropic attention over 2D pixel grids. On purpose. For fun.</p>

<p>If that sentence made your eyes glaze over, stick around anyway, because what TII just released matters even if you never plan to implement a vision-language model. Falcon Perception is a 600-million-parameter model that does something nobody else has pulled off at this scale: you give it a photo and a natural language description like “the red mug behind the laptop,” and it gives you back a pixel-perfect segmentation mask. Not a bounding box. Not a rough outline. A mask.</p>

<p>And it does this better than Meta’s SAM 3 — which has more parameters — and demolishes Alibaba’s Qwen3-VL-30B — which has <em>fifty times</em> more parameters — on complex compositional queries. That’s the kind of result that makes you read a paper about the golden ratio at 11 PM on a Thursday.</p>

<h2 id="why-this-matters-beyond-computer-vision-twitter">Why This Matters Beyond Computer Vision Twitter</h2>

<p>The thing about most AI model releases is that they matter to a very specific group of people who spend a lot of time on arXiv and not a lot of time explaining things at dinner parties. Falcon Perception is different because it validates a thesis that has implications across all of AI: <strong>architecture matters more than scale</strong>.</p>

<p>We’ve been in the “just make it bigger” era for a few years now. More parameters, more data, more compute. And look, that works — GPT-5 and Claude are proof. But Falcon Perception shows that when you rethink the <em>architecture</em> from first principles instead of just bolting more layers onto the same design, you can get dramatically better results with dramatically less.</p>

<p>For those of us who care about running things locally — and I absolutely care about running things locally — this is the whole ballgame. A model that fits in 2.5 GB of VRAM and beats 30-billion-parameter competitors isn’t just a research curiosity. It’s the difference between needing an $8,000 GPU and needing the GPU that’s already in your desktop.</p>

<h2 id="the-core-innovation-throwing-out-the-lego-bricks">The Core Innovation: Throwing Out the Lego Bricks</h2>

<p>Here’s how every other vision-language model works (broadly): take an image, run it through a frozen vision encoder like CLIP or ViT, get a bag of visual features. Take some text, run it through a language model. Then somewhere in the middle, try to make these two streams of information talk to each other through projection layers or cross-attention modules. It’s a Lego-brick approach — snap the vision piece onto the language piece and hope the seams don’t show.</p>

<p>Falcon Perception says: what if we just… didn’t do that?</p>

<p>Instead, image patches and text tokens go into the <em>same</em> transformer from layer one. No separate vision encoder. No projection layer. No Lego bricks. One backbone, shared parameters, from the very first attention operation. This is “early fusion” — and it sounds simple, but making it work requires solving two hard problems.</p>

<p><strong>Problem 1: Images and text need different attention patterns.</strong> When you’re processing an image, you want every patch to see every other patch (bidirectional attention) because spatial context matters — the pixels on the left need to know about the pixels on the right. But when you’re generating text, you need causal attention — each token can only see the tokens before it, or you’re leaking the answer into the question.</p>

<p>Falcon Perception solves this with a <strong>hybrid attention mask</strong> within a single model. Image tokens attend bidirectionally. Text and task tokens attend causally. The same transformer acts simultaneously as a vision encoder <em>and</em> a language decoder. They implemented this using PyTorch’s FlexAttention API, which compiles the custom mask pattern into fused GPU kernels without ever materializing the full N×M attention matrix in memory.</p>

<p><strong>Problem 2: Flattening a 2D image into a 1D token sequence destroys spatial information.</strong> Standard RoPE (Rotary Position Embeddings) — the thing that tells a transformer where tokens are in a sequence — only encodes 1D position. Even axial 2D RoPE can only attend along rows or columns, not diagonally. So TII invented <strong>GGRoPE</strong> (Golden Gate RoPE), which assigns each dimension pair a direction vector rotated by multiples of π/φ (where φ is the golden ratio, ~1.618). This produces maximally uniform angular coverage over the 2D plane, meaning the model can attend to arbitrary 2D positions isotropically. The golden ratio shows up because it’s the most irrational number — its multiples produce the most evenly-spaced distribution of angles, just like sunflower seeds arrange themselves using the golden angle.</p>

<p>If that paragraph felt dense, here’s the Iowa farmer version: they figured out how to tell the model where pixels <em>actually are</em> in 2D space even though the model processes them as a flat list, and they used the same math that makes sunflowers pretty.</p>

<h2 id="how-it-actually-generates-a-segmentation-chain-of-perception">How It Actually Generates a Segmentation: Chain-of-Perception</h2>

<p>When you give Falcon Perception a query like “the blue car on the left,” it doesn’t just spit out a mask. It follows a structured sequence called <strong>Chain-of-Perception</strong>:</p>

<ol>
  <li><strong>Existence decision</strong>: Is this thing present? (<code class="language-plaintext highlighter-rouge">&lt;present&gt;</code> or <code class="language-plaintext highlighter-rouge">&lt;absent&gt;</code>)</li>
  <li><strong>Center coordinate</strong>: Where is it?</li>
  <li><strong>Size estimate</strong>: How big is it?</li>
  <li><strong>Segmentation embedding</strong>: Generate the mask.</li>
</ol>

<p>This ordering is deliberate and clever. By committing to “does this exist?” <em>before</em> trying to locate it, the model avoids hallucinating masks for objects that aren’t there. By resolving position and size <em>before</em> segmentation, the mask prediction is really just pixel refinement conditioned on already-resolved geometry. Each step constrains the next.</p>

<p>The coordinates aren’t tokens — they’re continuous values that get projected through <strong>Fourier features</strong> (random Gaussian matrix → sinusoidal space) to overcome neural networks’ natural spectral bias toward low-frequency functions. This is the kind of detail you’d miss in a press release but matters enormously for precision.</p>

<h2 id="the-numbers-that-made-me-stop-scrolling">The Numbers That Made Me Stop Scrolling</h2>

<p>The benchmark that matters most here is <strong>PBench</strong>, a new diagnostic that isolates five levels of semantic complexity. On simple object recognition (L0: “find the dog”), the gap between Falcon Perception and SAM 3 is negligible (+0.8 points). But watch what happens as prompts get harder:</p>

<ul>
  <li><strong>L1 (Attributes)</strong>: +9.2 points over SAM 3</li>
  <li><strong>L2 (OCR-guided, e.g., “the bottle labeled Pepsi”)</strong>: +13.4 points</li>
  <li><strong>L3 (Spatial understanding, e.g., “second from right”)</strong>: +21.9 points</li>
  <li><strong>L4 (Relations, e.g., “the dog being pet by the child”)</strong>: +15.8 points</li>
  <li><strong>Dense split (many objects, crowd scenes)</strong>: +14.2 points</li>
</ul>

<p>And then there’s the gut-punch number: on the PBench Dense split, Falcon Perception (0.6B parameters) scores <strong>72.6</strong>. Qwen3-VL-30B scores <strong>8.9</strong>. A model fifty times smaller scores eight times higher. That’s not a rounding error. That’s a paradigm.</p>

<p>The reason is architectural: early fusion lets visual and linguistic information interact at every layer, so compositional reasoning (“the red one behind the blue one that’s left of the door”) resolves naturally through deep cross-modal interaction. Late-fusion models see the image separately, understand the text separately, and try to match them up at the end — which works fine for “find the dog” but falls apart when the query requires genuine spatial reasoning.</p>

<h2 id="the-honest-trade-offs-because-every-ai-blog-post-should-have-this-section">The Honest Trade-offs (Because Every AI Blog Post Should Have This Section)</h2>

<p>Falcon Perception is not magic and it’s not a general-purpose tool. Here’s what you give up:</p>

<p><strong>It’s not a general VLM.</strong> You cannot ask it “What’s happening in this image?” or “Write a caption for this photo.” It does one thing — open-vocabulary grounding and segmentation from natural language — and does it better than anything else at this scale. If you need visual question answering, captioning, or multi-step reasoning, you still want Qwen-VL, LLaVA, or Claude’s vision capabilities.</p>

<p><strong>Presence calibration is weaker.</strong> SAM 3 has an MCC (Matthews Correlation Coefficient) of 0.82 for deciding whether an object exists in a scene; Falcon Perception scores 0.64. That means more false positives — the model will sometimes generate a mask for something that isn’t there. For safety-critical applications (medical imaging, autonomous driving), this gap matters. TII says early RL experiments are closing it (+8 points already), but the released model has this limitation.</p>

<p><strong>It’s brand new.</strong> Released March 31, 2026. Forty GitHub stars. No Ollama support, no GGUF quantization, no third-party inference providers. SAM 3 and YOLO-World have massive ecosystems. Falcon Perception has a research paper and a HuggingFace repo. If you need production stability today, it’s early.</p>

<p><strong>English only (for OCR).</strong> The companion Falcon OCR model (300M parameters) performs well on document understanding benchmarks but only supports English. PaddleOCR supports 109 languages. If your documents aren’t in English, this isn’t your tool yet.</p>

<h2 id="running-it-locally-yes-on-your-actual-hardware">Running It Locally: Yes, On Your Actual Hardware</h2>

<p>At 600M parameters in FP16, the model weights are about 1.2 GB. Total VRAM during inference is roughly 2–4 GB. That means:</p>

<ul>
  <li><strong>Any modern NVIDIA GPU</strong>: Works. A GTX 1650 (4 GB) will handle it.</li>
  <li><strong>Apple Silicon Mac</strong>: Works via the MLX backend — Metal GPU acceleration, no PyTorch needed.</li>
  <li><strong>Raspberry Pi / Edge</strong>: Not currently viable. Needs CUDA or Apple Silicon.</li>
  <li><strong>CPU-only</strong>: Not officially supported; would be very slow.</li>
</ul>

<p>The code is about as simple as vision models get:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">transformers</span> <span class="kn">import</span> <span class="n">AutoModelForCausalLM</span>
<span class="kn">from</span> <span class="n">PIL</span> <span class="kn">import</span> <span class="n">Image</span>

<span class="n">model</span> <span class="o">=</span> <span class="n">AutoModelForCausalLM</span><span class="p">.</span><span class="nf">from_pretrained</span><span class="p">(</span>
    <span class="sh">"</span><span class="s">tiiuae/falcon-perception</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">trust_remote_code</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">device_map</span><span class="o">=</span><span class="p">{</span><span class="sh">""</span><span class="p">:</span> <span class="sh">"</span><span class="s">cuda:0</span><span class="sh">"</span><span class="p">},</span>
<span class="p">)</span>
<span class="n">predictions</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">generate</span><span class="p">(</span><span class="n">Image</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="sh">"</span><span class="s">photo.jpg</span><span class="sh">"</span><span class="p">),</span> <span class="sh">"</span><span class="s">red mug</span><span class="sh">"</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<p>No quantization needed — and none currently available — because the model is already small enough to be practical at FP16. The initial <code class="language-plaintext highlighter-rouge">torch.compile</code> warmup takes 10–30 seconds, after which inference is roughly 350ms per query on an H100, somewhat slower on consumer cards.</p>

<h2 id="where-this-fits-in-the-broader-landscape">Where This Fits in the Broader Landscape</h2>

<p>The vision-language space is fragmented across overlapping tool categories, and Falcon Perception occupies a specific niche. Here’s how I think about it:</p>

<p><strong>If you need pure OCR</strong>: Falcon OCR (300M) is competitive on English documents, but PaddleOCR and Tesseract still own multilingual. Cloud services (Google Vision, Azure, Textract) win on production reliability but cost real money at scale.</p>

<p><strong>If you need real-time detection</strong>: YOLO-World runs at 52+ FPS and is production-proven. It gives you boxes, not masks, but for many applications boxes are fine. Falcon Perception is 150–350ms per query — fast, but not real-time.</p>

<p><strong>If you need “find the thing I described and give me a pixel mask”</strong>: Falcon Perception is now the best open-source option at any scale. The previous approach was a Grounding DINO → SAM pipeline (two models, more VRAM, more latency, box-then-refine instead of direct prediction). Falcon collapses that into one model call.</p>

<p><strong>If you need a general visual assistant</strong>: Claude, GPT-5, or Qwen-VL. Falcon Perception can’t converse about images — it can only ground and segment.</p>

<h2 id="what-this-means-for-my-work">What This Means for My Work</h2>

<p>I’ve been building science fair review tools that need to process student posters, lab notebooks, and compliance forms. The OCR piece is interesting — Falcon OCR at 300M parameters is small enough to run in a pipeline on modest hardware. The grounding piece could theoretically identify specific elements on a poster (“the data table” or “the hypothesis statement”) without pre-training on science fair templates. I haven’t tested this yet — and I want to, badly — but the architecture seems purpose-built for exactly this kind of “find the thing I described in this complex document” task.</p>

<p>More broadly, Falcon Perception is evidence that the moat in AI is shifting from “who has the most GPUs” to “who has the best architectural ideas.” TII, backed by Abu Dhabi’s sovereign research fund, keeps releasing models that punch far above their parameter weight. The Falcon family now spans language models (Falcon 3), hybrid architectures (Falcon-H1), reasoning (Falcon-H1R), edge deployment (Falcon Edge with 1.58-bit BitNet), and now dense visual perception. All Apache 2.0. All running on hardware you can actually afford.</p>

<p>The era of “you need a data center to do anything interesting with AI” isn’t over, but it’s developing cracks. And those cracks are shaped like a 600-million-parameter falcon.</p>

<hr />

<p><strong>Links:</strong></p>
<ul>
  <li><a href="https://arxiv.org/abs/2603.27365">arXiv paper: arxiv.org/abs/2603.27365</a></li>
  <li><a href="https://huggingface.co/tiiuae/Falcon-Perception">HuggingFace model: tiiuae/Falcon-Perception</a></li>
  <li><a href="https://github.com/tiiuae/Falcon-Perception">GitHub: tiiuae/Falcon-Perception</a></li>
  <li><a href="https://huggingface.co/blog/tiiuae/falcon-perception">HuggingFace blog post</a></li>
  <li><a href="https://vision.falcon.aidrc.tii.ae">Live playground: vision.falcon.aidrc.tii.ae</a></li>
</ul>

<hr />

<p><em>Andy Denner is a scientific computing scientist and runs <a href="https://denner.co">Denner Consulting LLC</a>. He builds AI-powered compliance tools for science fairs, advocates for privacy, and occasionally reads papers about the golden ratio on Thursday nights. Find him on X at <a href="https://x.com/adenner">@adenner</a>.</em></p>]]></content><author><name>Andrew Denner</name></author><summary type="html"><![CDATA[Falcon Perception: 600 Million Parameters That Embarrass Models 50 Times Their Size (and What That Means for the Rest of Us)]]></summary></entry><entry><title type="html">Flatpak Deep Dive: Portal Architecture and the Bubblewrap Sandbox (Part 4)</title><link href="http://0.0.0.0:4000/2026/03/24/flatpak4.html" rel="alternate" type="text/html" title="Flatpak Deep Dive: Portal Architecture and the Bubblewrap Sandbox (Part 4)" /><published>2026-03-24T15:35:14+00:00</published><updated>2026-03-24T15:35:14+00:00</updated><id>http://0.0.0.0:4000/2026/03/24/flatpak4</id><content type="html" xml:base="http://0.0.0.0:4000/2026/03/24/flatpak4.html"><![CDATA[<h1 id="flatpak-deep-dive-portal-architecture-and-the-bubblewrap-sandbox">Flatpak Deep Dive: Portal Architecture and the Bubblewrap Sandbox</h1>
<h2 id="ostree-user-namespaces-d-bus-portals-and-why-varapp-is-so-large">OSTree, User Namespaces, D-Bus Portals, and Why ~/.var/app Is So Large</h2>

<p><em>Andrew Denner · March 2026 · <a href="https://denner.co">denner.co</a></em>
<em>Part 4 of the <a href="https://denner.co/blog">Linux Universal Packages series</a></em></p>

<hr />

<p><em>Flatpak is the one I use daily. It’s also the one I understand well enough to know exactly what it’s doing when it annoys me. This part goes into the Bubblewrap sandbox, user namespaces, how OSTree stores runtimes, and the portal system that most people have seen without knowing what it is.</em></p>

<hr />

<h2 id="the-design-philosophy">The Design Philosophy</h2>

<p>Flatpak’s design differs from Snap’s in a few fundamental ways worth stating upfront:</p>

<p><strong>Decentralized</strong>: Flatpak has no central authority or mandatory store. You can run a Flatpak from any remote. Flathub is the de facto central repository but it’s not baked into the architecture. You can add your own remote, your company’s internal Flatpak repo, or install local .flatpak bundles.</p>

<p><strong>User namespaces, not setuid</strong>: Flatpak’s sandbox uses Linux user namespaces rather than <code class="language-plaintext highlighter-rouge">setuid</code> binaries (Snap’s <code class="language-plaintext highlighter-rouge">snap-confine</code> is setuid). This is a philosophical difference — user namespaces allow sandbox creation without elevated privileges. The tradeoff is more complex setup, and on some systems user namespaces have had security vulnerabilities.</p>

<p><strong>OSTree for runtime management</strong>: Runtimes (shared libraries, GTK, Qt) are stored and versioned using OSTree — a content-addressable, git-like filesystem. This is a genuinely novel approach to dependency management.</p>

<p><strong>Portal-mediated resource access</strong>: Rather than granting apps direct access to system resources (microphone, camera, files), Flatpak routes access through XDG portals — OS-managed D-Bus services that apply additional policy.</p>

<hr />

<h2 id="bubblewrap-the-sandbox-engine">Bubblewrap: The Sandbox Engine</h2>

<p>Flatpak uses <a href="https://github.com/containers/bubblewrap">Bubblewrap</a> (<code class="language-plaintext highlighter-rouge">bwrap</code>) as its sandbox primitive. Bubblewrap is a small C binary that creates Linux namespace containers without requiring root.</p>

<h3 id="linux-user-namespaces">Linux User Namespaces</h3>

<p>The key technology: <strong>user namespaces</strong> (<code class="language-plaintext highlighter-rouge">CLONE_NEWUSER</code>). A user namespace creates a mapping between user IDs inside the namespace and UIDs outside. Inside a user namespace, unprivileged code can simulate being root (UID 0) while the kernel maps that back to a real unprivileged UID.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Simplified concept of what bwrap does:</span>
<span class="n">clone</span><span class="p">(</span><span class="n">CLONE_NEWUSER</span> <span class="o">|</span> <span class="n">CLONE_NEWNS</span> <span class="o">|</span> <span class="n">CLONE_NEWPID</span> <span class="o">|</span> <span class="n">CLONE_NEWIPC</span> <span class="o">|</span> <span class="n">CLONE_NEWUTS</span><span class="p">,</span> <span class="p">...)</span>
<span class="c1">// Creates: new user ns + new mount ns + new pid ns + new ipc ns + new uts ns</span>
<span class="c1">// Then writes the UID/GID mapping:</span>
<span class="c1">// /proc/&lt;pid&gt;/uid_map: "0 &lt;real_uid&gt; 1"   (inside uid 0 = outside uid $USER)</span>
<span class="c1">// /proc/&lt;pid&gt;/gid_map: "0 &lt;real_gid&gt; 1"</span>
</code></pre></div></div>

<p>This namespace stack is what allows Flatpak to create a sandbox without root. Inside the sandbox:</p>
<ul>
  <li>The app sees itself as running as UID 0 in some operations (for filesystem setup)</li>
  <li>But the kernel knows this maps to your real unprivileged UID</li>
  <li>Root operations that require real root (loading kernel modules, changing system time) still fail</li>
</ul>

<h3 id="what-bwrap-actually-creates">What bwrap Actually Creates</h3>

<p>When you launch a Flatpak application, Flatpak invokes <code class="language-plaintext highlighter-rouge">bwrap</code> with a complex set of arguments that define the sandbox’s filesystem. A simplified version:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bwrap <span class="se">\</span>
  <span class="nt">--ro-bind</span> /usr/share/flatpak/exports/share/runtime/org.gnome.Platform/x86_64/45/files /usr <span class="se">\</span>
  <span class="nt">--ro-bind</span> /app /app <span class="se">\</span>
  <span class="nt">--bind</span> /run/user/1000/flatpak/app/org.gimp.GIMP /run/user/1000 <span class="se">\</span>
  <span class="nt">--bind</span> /home/user/.var/app/org.gimp.GIMP /home/user <span class="se">\</span>
  <span class="nt">--tmpfs</span> /tmp <span class="se">\</span>
  <span class="nt">--proc</span> /proc <span class="se">\</span>
  <span class="nt">--dev</span> /dev <span class="se">\</span>
  <span class="nt">--unshare-pid</span> <span class="se">\</span>
  <span class="nt">--unshare-ipc</span> <span class="se">\</span>
  <span class="nt">--new-session</span> <span class="se">\</span>
  <span class="nt">--</span> /app/bin/gimp-2.10
</code></pre></div></div>

<p>The app’s filesystem view is a construction of bind mounts:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">/usr</code> is the runtime (GNOME Platform), not the system’s <code class="language-plaintext highlighter-rouge">/usr</code></li>
  <li><code class="language-plaintext highlighter-rouge">/app</code> is the Flatpak app installation directory (read-only)</li>
  <li><code class="language-plaintext highlighter-rouge">~</code> is a redirected path pointing to <code class="language-plaintext highlighter-rouge">~/.var/app/org.gimp.GIMP</code> (the app’s isolated data dir)</li>
  <li><code class="language-plaintext highlighter-rouge">/tmp</code> is a fresh tmpfs (isolated)</li>
</ul>

<p>The app never sees your real <code class="language-plaintext highlighter-rouge">/usr</code>, never sees other apps’ data, and its <code class="language-plaintext highlighter-rouge">~</code> is a sandboxed directory, not your real home.</p>

<h3 id="the-mount-namespace">The Mount Namespace</h3>

<p>Inside the mount namespace, Flatpak can set up an arbitrary filesystem hierarchy using bind mounts — mapping paths from the outside namespace into specific locations inside. The kernel enforces that nothing inside can see mounts from outside unless explicitly bound in.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># You can inspect a running Flatpak's mount namespace:</span>
<span class="nv">FLATPAK_PID</span><span class="o">=</span><span class="si">$(</span>pgrep <span class="nt">-f</span> <span class="s2">"gimp"</span><span class="si">)</span>
<span class="nb">cat</span> /proc/<span class="nv">$FLATPAK_PID</span>/mounts | <span class="nb">head</span> <span class="nt">-20</span>

<span class="c"># Shows the bind-mounted directories that make up the sandbox view</span>
<span class="c"># /var/lib/flatpak/runtime/org.gnome.Platform/.../files on /usr type ext4 (ro,...)</span>
<span class="c"># tmpfs on /tmp type tmpfs</span>
<span class="c"># ...</span>
</code></pre></div></div>

<hr />

<h2 id="ostree-git-for-filesystems">OSTree: Git for Filesystems</h2>

<p>Flatpak uses <a href="https://ostreedev.github.io/ostree/">OSTree</a> to store and distribute runtimes. OSTree is the same technology used in Fedora CoreOS, RHEL for Edge, and some automotive embedded Linux systems. Understanding OSTree explains Flatpak’s storage model.</p>

<h3 id="the-content-addressable-store">The Content-Addressable Store</h3>

<p>OSTree stores file content as objects keyed by SHA256 hash, just like git stores blobs. Files with identical content are stored once, regardless of how many apps or runtime versions reference them.</p>

<p>The OSTree repository on a Flatpak system:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/var/lib/flatpak/repo/           # System Flatpak repo
~/.local/share/flatpak/repo/     # User Flatpak repo

repo/
├── config                       # Remote configuration
├── refs/
│   └── remotes/
│       └── flathub/
│           └── runtime/
│               └── org.gnome.Platform/x86_64/45  # → &lt;commit hash&gt;
├── objects/
│   ├── aa/                      # Files stored by hash prefix
│   │   └── bb1234...            # Content object
│   └── ...
└── state/
</code></pre></div></div>

<p>When you install GNOME Platform 45, OSTree downloads and stores each file by its content hash. When GNOME Platform 45 is updated to 45.1, only changed files are downloaded — unchanged files are already in the object store.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Inspect the OSTree repo:</span>
ostree <span class="nt">--repo</span><span class="o">=</span>/var/lib/flatpak/repo refs
<span class="c"># Shows all installed runtimes and apps as OSTree refs</span>

<span class="c"># See what changed between two commits:</span>
ostree <span class="nt">--repo</span><span class="o">=</span>/var/lib/flatpak/repo diff &lt;old-commit&gt; &lt;new-commit&gt;
</code></pre></div></div>

<h3 id="deployment-checking-out-runtimes">Deployment: Checking Out Runtimes</h3>

<p>When Flatpak needs to use a runtime, it “deploys” the OSTree checkout — creating a directory containing the runtime files, with OSTree’s hardlink optimization:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/var/lib/flatpak/runtime/org.gnome.Platform/x86_64/45/
└── active/
    ├── deploy              # OSTree metadata
    └── files/              # The actual runtime files
        ├── bin/
        ├── lib/
        └── share/
</code></pre></div></div>

<p>Critically, OSTree deployments use <strong>hardlinks</strong> to the object store wherever possible. Multiple runtime versions that share files share those files on disk (same inode). This is why the “500MB runtime” number is misleading — if GNOME Platform 44 and 45 share 80% of their files, the second installation uses much less disk than the first.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check actual disk usage with deduplication accounted for:</span>
flatpak disk-usage
<span class="c"># This accounts for shared files; raw du output does not</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">du -sh ~/.var/app</code> trick I mention in the talk only shows the <em>app data</em> directory. The runtimes live in <code class="language-plaintext highlighter-rouge">/var/lib/flatpak/runtime/</code> (system install) or <code class="language-plaintext highlighter-rouge">~/.local/share/flatpak/runtime/</code> (user install). Those are the ones that grow.</p>

<hr />

<h2 id="xdg-desktop-portals-the-broker-system">XDG Desktop Portals: The Broker System</h2>

<p>This is Flatpak’s most architecturally interesting idea. Instead of giving apps direct access to sensitive resources, they communicate through <strong>portals</strong> — D-Bus services provided by the host desktop environment.</p>

<h3 id="why-portals-exist">Why Portals Exist</h3>

<p>A sandboxed app can’t open <code class="language-plaintext highlighter-rouge">/home/user/Documents/</code> directly. But it can send a D-Bus message to <code class="language-plaintext highlighter-rouge">org.freedesktop.portal.FileChooser.OpenFile()</code>. The portal service (running <em>outside</em> the sandbox) shows a native file picker dialog. When the user selects a file, the portal returns a file descriptor to the app. The app never sees the filesystem path — it just gets a file descriptor to read.</p>

<p>This means:</p>
<ul>
  <li>The app can only access files the user explicitly chose</li>
  <li>The portal can apply additional policy (e.g., respecting the user’s sandboxed directory)</li>
  <li>The file picker looks native because <em>it is native</em> — it’s run by the host, not the app</li>
</ul>

<h3 id="portal-types">Portal Types</h3>

<p>The XDG portal stack defines over a dozen portal interfaces:</p>

<p><strong><code class="language-plaintext highlighter-rouge">org.freedesktop.portal.FileChooser</code></strong>: Open/save file dialogs. The most commonly used portal. Every time a Flatpak app shows a file chooser, this is what’s happening.</p>

<p><strong><code class="language-plaintext highlighter-rouge">org.freedesktop.portal.OpenURI</code></strong>: Open a URI using the system default handler. When a Flatpak app opens a URL, it asks this portal — which then uses your system’s configured browser.</p>

<p><strong><code class="language-plaintext highlighter-rouge">org.freedesktop.portal.Screenshot</code></strong>: Take a screenshot. The portal shows a confirmation dialog if desired.</p>

<p><strong><code class="language-plaintext highlighter-rouge">org.freedesktop.portal.ScreenCast</code></strong>: Screen recording/sharing (used by Zoom, OBS, Pipewire-based screensharing). The portal negotiates a PipeWire stream rather than giving the app direct <code class="language-plaintext highlighter-rouge">/dev/video*</code> access.</p>

<p><strong><code class="language-plaintext highlighter-rouge">org.freedesktop.portal.Camera</code></strong>: Similar — camera access via PipeWire stream, with user confirmation.</p>

<p><strong><code class="language-plaintext highlighter-rouge">org.freedesktop.portal.Notifications</code></strong>: Desktop notifications. Apps can’t write to the notification system directly.</p>

<p><strong><code class="language-plaintext highlighter-rouge">org.freedesktop.portal.Settings</code></strong>: Read desktop settings (color scheme, font, etc.) from the host environment. This is how Flatpak apps know whether to use dark mode.</p>

<p><strong><code class="language-plaintext highlighter-rouge">org.freedesktop.portal.Background</code></strong>: Request permission to run in the background/at startup.</p>

<p><strong><code class="language-plaintext highlighter-rouge">org.freedesktop.portal.Email</code></strong>: Compose an email (opens the default mail client).</p>

<p><strong><code class="language-plaintext highlighter-rouge">org.freedesktop.portal.Secret</code></strong>: Access the keyring/secret service.</p>

<h3 id="the-portal-architecture">The Portal Architecture</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Flatpak App (sandboxed)
  │
  │ D-Bus message to portal
  ▼
org.freedesktop.portal.Desktop (xdg-desktop-portal)
  │
  │ Routes to implementation
  ▼
org.freedesktop.impl.portal.gtk  (GNOME implementation)
org.freedesktop.impl.portal.kde  (KDE implementation)
org.freedesktop.impl.portal.wlr  (wlroots compositors)
  │
  │ Shows native UI (file picker, etc.)
  ▼
Result → file descriptor / URI / permission granted
  │
  ▼
Back to sandboxed app
</code></pre></div></div>

<p>The portal implementation is chosen based on your desktop environment. On GNOME, <code class="language-plaintext highlighter-rouge">xdg-desktop-portal-gnome</code> provides native GNOME file pickers. On KDE, <code class="language-plaintext highlighter-rouge">xdg-desktop-portal-kde</code> provides KDE dialogs. This is why Flatpak apps can show native-looking file pickers even when running outside their “home” desktop.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Running portals on your system:</span>
ps aux | <span class="nb">grep </span>xdg-desktop-portal
<span class="c"># xdg-desktop-portal (the broker)</span>
<span class="c"># xdg-desktop-portal-gnome (or -kde, -gtk)</span>

<span class="c"># D-Bus introspection of available portal interfaces:</span>
busctl <span class="nt">--user</span> introspect org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop
</code></pre></div></div>

<h3 id="pipewire-and-screencast-the-modern-media-story">PipeWire and ScreenCast: The Modern Media Story</h3>

<p>The ScreenCast portal is worth special attention because it solved a genuine problem. Before PipeWire, sandboxed apps had no good way to do screen capture or audio routing. X11 apps could grab the framebuffer, but Wayland compositors deliberately prevent this (no direct framebuffer access for apps).</p>

<p>The solution: the ScreenCast portal negotiates a PipeWire stream with the compositor. The compositor captures the screen and provides it as a PipeWire media stream. The sandboxed app connects to the PipeWire stream and gets video frames without ever having direct access to the display server.</p>

<p>This is why Flatpak apps (like Zoom’s Flatpak) can do screen sharing on Wayland. It’s also why <code class="language-plaintext highlighter-rouge">obs-studio</code> as a Flatpak works with PipeWire screen capture.</p>

<hr />

<h2 id="the-flatpak-info-file-sandbox-self-inspection">The <code class="language-plaintext highlighter-rouge">.flatpak-info</code> File: Sandbox Self-Inspection</h2>

<p>Every running Flatpak app has a <code class="language-plaintext highlighter-rouge">.flatpak-info</code> file visible at <code class="language-plaintext highlighter-rouge">/run/user/&lt;uid&gt;/flatpak/app/&lt;app-id&gt;/.flatpak-info</code>. This file describes the app’s sandbox configuration.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># While GIMP is running:</span>
<span class="nb">cat</span> /run/user/<span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span>/flatpak/app/org.gimp.GIMP/.flatpak-info

<span class="c"># [Application]</span>
<span class="c"># name=org.gimp.GIMP</span>
<span class="c"># runtime=runtime/org.gnome.Platform/x86_64/45</span>
<span class="c">#</span>
<span class="c"># [Instance]</span>
<span class="c"># instance-id=1234567890</span>
<span class="c"># app-path=/var/lib/flatpak/app/org.gimp.GIMP/...</span>
<span class="c"># runtime-path=/var/lib/flatpak/runtime/org.gnome.Platform/...</span>
<span class="c">#</span>
<span class="c"># [Context]</span>
<span class="c"># shared=network;ipc</span>
<span class="c"># filesystems=host</span>
</code></pre></div></div>

<p>This file is used by portal services to verify the app’s identity and determine which permissions it has. A portal can read this to decide whether to grant a request.</p>

<hr />

<h2 id="flatpak-permissions-the-full-model">Flatpak Permissions: The Full Model</h2>

<p>Flatpak permissions come from two sources: the app’s <strong>manifest</strong> (what the developer declared) and <strong>overrides</strong> (what the user has modified).</p>

<h3 id="the-manifest-context">The Manifest Context</h3>

<p>In a Flatpak manifest (used to build the app), the developer declares what permissions the app needs:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">finish-args</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">--share=network</span>          <span class="c1"># Access the network</span>
  <span class="pi">-</span> <span class="s">--share=ipc</span>              <span class="c1"># Shared memory IPC (X11 apps need this)</span>
  <span class="pi">-</span> <span class="s">--socket=wayland</span>         <span class="c1"># Wayland display server</span>
  <span class="pi">-</span> <span class="s">--socket=fallback-x11</span>    <span class="c1"># X11 fallback</span>
  <span class="pi">-</span> <span class="s">--socket=pulseaudio</span>      <span class="c1"># PulseAudio/PipeWire audio</span>
  <span class="pi">-</span> <span class="s">--device=dri</span>             <span class="c1"># GPU/DRI access (hardware rendering)</span>
  <span class="pi">-</span> <span class="s">--filesystem=home</span>        <span class="c1"># Access home directory</span>
  <span class="pi">-</span> <span class="s">--filesystem=host</span>        <span class="c1"># Access entire filesystem (dangerous)</span>
  <span class="pi">-</span> <span class="s">--talk-name=org.gtk.vfs.daemon</span>  <span class="c1"># Talk to this D-Bus service</span>
  <span class="pi">-</span> <span class="s">--env=GDK_BACKEND=wayland</span>       <span class="c1"># Set environment variable</span>
</code></pre></div></div>

<p>These permissions are compiled into the Flatpak and stored in the repo. When you install an app, these are the declared permissions.</p>

<h3 id="user-overrides">User Overrides</h3>

<p>Users can override permissions in both directions — grant more or restrict more:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Grant filesystem access that the manifest didn't declare:</span>
flatpak override <span class="nt">--user</span> <span class="nt">--filesystem</span><span class="o">=</span>~/Projects org.gimp.GIMP

<span class="c"># Remove a permission the manifest declared:</span>
flatpak override <span class="nt">--user</span> <span class="nt">--nofilesystem</span><span class="o">=</span>home org.gimp.GIMP

<span class="c"># Grant access to a specific D-Bus name:</span>
flatpak override <span class="nt">--user</span> <span class="nt">--talk-name</span><span class="o">=</span>org.freedesktop.NetworkManager org.example.App

<span class="c"># See all overrides:</span>
flatpak override <span class="nt">--user</span> <span class="nt">--show</span> org.gimp.GIMP

<span class="c"># Reset all overrides:</span>
flatpak override <span class="nt">--user</span> <span class="nt">--reset</span> org.gimp.GIMP
</code></pre></div></div>

<p>Flatseal is a GUI front-end for this override system. It reads the manifest’s declared permissions, shows the current override state, and lets you toggle them. This is what I call “transparent permissions” — you can see exactly what each app can do.</p>

<h3 id="the---filesystemhost-problem">The <code class="language-plaintext highlighter-rouge">--filesystem=host</code> Problem</h3>

<p><code class="language-plaintext highlighter-rouge">--filesystem=host</code> gives the Flatpak full access to the host filesystem (same as your user). Many apps include this because it’s easier than figuring out which specific paths they need.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check which Flatpaks have --filesystem=host:</span>
flatpak info <span class="nt">--file-access</span> &lt;app-id&gt;
<span class="c"># Or via Flatseal: look for "All user files" in Filesystem section</span>
</code></pre></div></div>

<p>When an app has <code class="language-plaintext highlighter-rouge">--filesystem=host</code>, the sandbox’s filesystem isolation provides almost no protection. The app can read and write your entire home directory. This is legal in Flatpak — the developer declared it and Flathub allows it with justification — but it’s worth knowing.</p>

<hr />

<h2 id="flathub-the-store-and-the-review-process">Flathub: The Store and the Review Process</h2>

<p>Flathub is not part of Flatpak — it’s a community-operated Flatpak remote hosted at <code class="language-plaintext highlighter-rouge">https://dl.flathub.org/</code>. But it’s so central to the Flatpak ecosystem that understanding it matters.</p>

<h3 id="submission-and-review">Submission and Review</h3>

<p>The review process for Flathub submissions:</p>

<ol>
  <li>Developer submits a PR to <a href="https://github.com/flathub/flathub">github.com/flathub/flathub</a> with their manifest</li>
  <li>Automated checks verify: manifest syntax, allowed build modules, no vendored binaries, no network access during build</li>
  <li>Human reviewers check: claimed permissions are reasonable, sources are legitimate, app ID matches domain control</li>
  <li>Initial approval creates the app repository</li>
  <li>Future updates are published by the developer (no per-update review after initial approval, but automated checks run)</li>
</ol>

<p>Notable review requirements:</p>
<ul>
  <li><strong>No proprietary bundled binaries in open-source apps</strong> (must be built from source)</li>
  <li><strong>App IDs must be reverse-domain with domain ownership</strong> (or use <code class="language-plaintext highlighter-rouge">io.github.username.AppName</code>)</li>
  <li><strong>No excessive permissions</strong> without justification (reviewers flag <code class="language-plaintext highlighter-rouge">--filesystem=host</code> for non-file-manager apps)</li>
  <li><strong>Sources must be verifiable</strong> (checksums/commits required, no bare URLs)</li>
</ul>

<p>Proprietary apps (Discord, Spotify, Steam) are allowed on Flathub but go through additional review. They’re typically binary bundles since the source isn’t available.</p>

<h3 id="verification">Verification</h3>

<p>Flathub supports developer verification — proving you control the app’s domain or GitHub identity. Verified apps show a checkmark in GNOME Software and other store frontends.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># See Flathub metadata including verification status:</span>
flatpak remote-info flathub org.gimp.GIMP
</code></pre></div></div>

<hr />

<h2 id="the-storage-architecture-what-takes-disk-space">The Storage Architecture: What Takes Disk Space</h2>

<p>Let me be precise about where disk usage comes from:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>System installation (/var/lib/flatpak/):
├── repo/                           # OSTree objects (content-addressable)
│   └── objects/                    # Shared file content, hardlinked
├── runtime/                        # Deployed runtimes
│   ├── org.gnome.Platform/         # ~600MB-1GB
│   ├── org.kde.Platform/           # ~600MB (if KDE apps installed)
│   └── org.freedesktop.Platform/   # ~300MB (base for many apps)
└── app/                            # Deployed apps
    ├── org.gimp.GIMP/              # App files (not user data)
    └── org.mozilla.firefox/

User installation (~/.local/share/flatpak/):
└── (Same structure, for user-installed apps)

User data (~/.var/app/):
├── org.gimp.GIMP/
│   ├── .config/                    # App configuration
│   ├── .local/                     # Local app data
│   └── cache/                      # App cache (safe to delete)
└── org.mozilla.firefox/
    └── ...
</code></pre></div></div>

<p>The OSTree object store deduplication means the real disk usage is usually less than <code class="language-plaintext highlighter-rouge">du</code> suggests. For accurate measurement:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># This command accounts for shared objects:</span>
flatpak disk-usage <span class="nt">--system</span>
flatpak disk-usage <span class="nt">--user</span>

<span class="c"># Output shows "Application Bytes" (real unique bytes) vs "Installed Size" (apparent)</span>
</code></pre></div></div>

<p>If you have GNOME and KDE apps, you’ll have both GNOME Platform and KDE Frameworks runtimes. These don’t share much — different library stacks. This is the realistic scenario where Flatpak’s storage overhead is highest.</p>

<hr />

<h2 id="the-sandbox-escape-what-breaks-isolation">The Sandbox Escape: What Breaks Isolation</h2>

<p>Flatpak’s sandbox is not perfect. Known and accepted vectors:</p>

<p><strong><code class="language-plaintext highlighter-rouge">--filesystem=host</code></strong>: As described above — full home directory access means no real isolation. Check your apps.</p>

<p><strong>X11 Forwarding</strong>: If an app uses X11 (with <code class="language-plaintext highlighter-rouge">--socket=x11</code> or <code class="language-plaintext highlighter-rouge">--socket=fallback-x11</code>), it can keylog other X11 apps and capture screen content. X11 has no per-app isolation. This is a fundamental X11 limitation, not a Flatpak bug. Wayland eliminates this.</p>

<p><strong><code class="language-plaintext highlighter-rouge">--socket=session-bus</code></strong>: Full access to the D-Bus session bus means the app can call any service that’s running on the bus — including other apps. This is a large permission to grant.</p>

<p><strong>PulseAudio socket</strong>: Direct access to PulseAudio means the app can potentially record audio from the system or other apps, not just play it.</p>

<p><strong>The <code class="language-plaintext highlighter-rouge">--device=all</code> permission</strong>: Grants access to all devices in <code class="language-plaintext highlighter-rouge">/dev</code>. Avoid.</p>

<p>The Flatpak team is aware of these vectors and generally treats the sandbox as “best effort for GUI desktop apps, not a security boundary for untrusted code.” The security model assumes you trust the app developer — the sandbox mostly protects against bugs and accidental data access, not malicious apps.</p>

<hr />

<h2 id="building-a-flatpak">Building a Flatpak</h2>

<p>Understanding the build system shows you why app sizes are what they are.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># org.example.MyApp.yaml (Flatpak manifest)</span>
<span class="na">app-id</span><span class="pi">:</span> <span class="s">org.example.MyApp</span>
<span class="na">runtime</span><span class="pi">:</span> <span class="s">org.gnome.Platform</span>
<span class="na">runtime-version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">45'</span>
<span class="na">sdk</span><span class="pi">:</span> <span class="s">org.gnome.Sdk</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">myapp</span>

<span class="na">finish-args</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">--share=network</span>
  <span class="pi">-</span> <span class="s">--socket=wayland</span>
  <span class="pi">-</span> <span class="s">--socket=fallback-x11</span>
  <span class="pi">-</span> <span class="s">--device=dri</span>

<span class="na">modules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">myapp</span>
    <span class="na">buildsystem</span><span class="pi">:</span> <span class="s">cmake-ninja</span>
    <span class="na">sources</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">git</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s">https://github.com/example/myapp</span>
        <span class="na">commit</span><span class="pi">:</span> <span class="s">abc123...</span>
</code></pre></div></div>

<p>The build runs inside a Flatpak SDK sandbox (isolated from your system). The app is built against the SDK’s libraries (GNOME SDK 45 = GTK4, GLib, etc.). The final image only includes the app’s unique code — runtime libraries come from the shared runtime at install time.</p>

<p>This is why Flatpak apps that use the shared runtime are often smaller than AppImages: GIMP.flatpak doesn’t bundle GTK. GTK comes from the GNOME Platform runtime shared with every other GNOME app.</p>

<hr />

<h2 id="what-happens-under-the-hood-a-full-flatpak-launch">What Happens Under the Hood: A Full Flatpak Launch</h2>

<p>Tracing the full execution path for <code class="language-plaintext highlighter-rouge">flatpak run org.gimp.GIMP</code>:</p>

<ol>
  <li><strong>flatpak CLI</strong> reads <code class="language-plaintext highlighter-rouge">~/.local/share/flatpak/app/org.gimp.GIMP/...</code> to find the app</li>
  <li>Determines the runtime: <code class="language-plaintext highlighter-rouge">org.gnome.Platform/x86_64/45</code></li>
  <li>Resolves permissions from manifest + user overrides</li>
  <li>Constructs <code class="language-plaintext highlighter-rouge">bwrap</code> arguments defining the sandbox filesystem</li>
  <li>Sets up portals: connects to <code class="language-plaintext highlighter-rouge">xdg-desktop-portal</code> on D-Bus, registers the session</li>
  <li>Sets environment variables: <code class="language-plaintext highlighter-rouge">FLATPAK_ID</code>, <code class="language-plaintext highlighter-rouge">XDG_RUNTIME_DIR</code>, <code class="language-plaintext highlighter-rouge">XDG_DATA_HOME</code>, <code class="language-plaintext highlighter-rouge">HOME</code> (pointing to <code class="language-plaintext highlighter-rouge">~/.var/app/org.gimp.GIMP</code>)</li>
  <li>Writes <code class="language-plaintext highlighter-rouge">.flatpak-info</code> to <code class="language-plaintext highlighter-rouge">/run/user/&lt;uid&gt;/flatpak/app/org.gimp.GIMP/</code></li>
  <li>Executes <code class="language-plaintext highlighter-rouge">bwrap</code> with the full argument list</li>
  <li>Inside the sandbox: mounts are set up, new namespaces created</li>
  <li><code class="language-plaintext highlighter-rouge">bwrap</code> exec()s <code class="language-plaintext highlighter-rouge">/app/bin/gimp-2.10</code></li>
  <li>GIMP starts inside its sandbox</li>
</ol>

<p>Portal calls during GIMP’s life:</p>
<ul>
  <li>File → Open: GIMP calls <code class="language-plaintext highlighter-rouge">org.freedesktop.portal.FileChooser.OpenFile()</code> → native picker shown → file descriptor returned</li>
  <li>File → Export: <code class="language-plaintext highlighter-rouge">FileChooser.SaveFile()</code> → native save dialog</li>
  <li>Help menu: <code class="language-plaintext highlighter-rouge">OpenURI</code> → your browser opens</li>
</ul>

<p>When GIMP exits, bwrap tears down the namespace and all bind mounts disappear.</p>

<hr />

<h2 id="the-oci-future-experimental-and-worth-watching">The OCI Future: Experimental and Worth Watching</h2>

<p>One development worth knowing about as of 2025–2026: Flatpak has experimental support for pulling apps and runtimes from OCI registries — standard Docker Hub-compatible image registries, accessible via Podman or any OCI-compatible toolchain.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Experimental: add an OCI-based remote</span>
flatpak remote-add <span class="nt">--from</span> oci registry.example.com/my-flatpak-repo
</code></pre></div></div>

<p>If this matures, it could meaningfully change the distribution story. OCI registries are already widely available infrastructure — every major cloud provider runs one, and self-hosted options (Harbor, Gitea’s package registry, etc.) are common. Using them as Flatpak distribution channels would reduce the need for the custom OSTree remote infrastructure, potentially making self-hosted Flatpak distribution much easier.</p>

<p>The OSTree deduplication story also looks different in an OCI world: OCI layers use content-addressable storage similar to OSTree, so the storage efficiency benefits could be preserved.</p>

<p>For now, this is experimental. The primary distribution channel for Flatpak remains OSTree-based remotes with Flathub as the de facto central repo. But it’s an interesting direction, and if you’re building internal tooling or evaluating whether to run a private Flatpak repo, it’s worth checking the current state of OCI support in your target Flatpak version.</p>

<hr />

<h2 id="what-to-read-next">What to Read Next</h2>

<ul>
  <li>Bubblewrap: <a href="https://github.com/containers/bubblewrap">github.com/containers/bubblewrap</a></li>
  <li>XDG Desktop Portal: <a href="https://flatpak.github.io/xdg-desktop-portal">flatpak.github.io/xdg-desktop-portal</a></li>
  <li>OSTree documentation: <a href="https://ostreedev.github.io/ostree">ostreedev.github.io/ostree</a></li>
  <li>Flathub submission guide: <a href="https://docs.flathub.org/docs/for-app-authors">docs.flathub.org/docs/for-app-authors</a></li>
  <li>Flatpak permissions reference: <a href="https://docs.flatpak.org/en/latest/sandbox-permissions.html">docs.flatpak.org/en/latest/sandbox-permissions.html</a></li>
</ul>

<hr />

<p><em>Next: <a href="https://denner.co/blog">Part 5 — vs Docker: When Does a Universal Package Become a Container?</a></em></p>

<p><em>Andrew Denner — denner.co — @adenner</em></p>]]></content><author><name>Andrew Denner</name></author><summary type="html"><![CDATA[Flatpak Deep Dive: Portal Architecture and the Bubblewrap Sandbox OSTree, User Namespaces, D-Bus Portals, and Why ~/.var/app Is So Large]]></summary></entry><entry><title type="html">Universal Packages vs Docker: When Does a Package Become a Container? (part 5)</title><link href="http://0.0.0.0:4000/2026/03/24/uni-vs-docker-5.html" rel="alternate" type="text/html" title="Universal Packages vs Docker: When Does a Package Become a Container? (part 5)" /><published>2026-03-24T15:35:14+00:00</published><updated>2026-03-24T15:35:14+00:00</updated><id>http://0.0.0.0:4000/2026/03/24/uni-vs-docker-5</id><content type="html" xml:base="http://0.0.0.0:4000/2026/03/24/uni-vs-docker-5.html"><![CDATA[<h1 id="universal-packages-vs-docker-when-does-a-package-become-a-container">Universal Packages vs Docker: When Does a Package Become a Container?</h1>
<h2 id="linux-namespaces-cgroups-overlayfs-and-the-oci-spec-explained">Linux Namespaces, cgroups, OverlayFS, and the OCI Spec Explained</h2>

<p><em>Andrew Denner · March 2026 · <a href="https://denner.co">denner.co</a></em>
<em>Part 5 of the <a href="https://denner.co/blog">Linux Universal Packages series</a></em></p>

<hr />

<p><em>CIALUG did a Docker internals deep-dive last month. This part is written assuming you came out of that session knowing what cgroups and namespaces are. If you didn’t — or if you want a refresher — the first half covers the kernel primitives. Then we do the comparison. If you want to skip to “how these relate to Flatpak/Snap” jump to the comparison section.</em></p>

<hr />

<h2 id="the-kernel-primitives-a-refresher">The Kernel Primitives: A Refresher</h2>

<p>Linux containers — and by extension, Snap and Flatpak sandboxes — are built from a small set of kernel primitives. Not kernel modules, not hypervisors, not virtual machines. Primitives that have been part of the mainline kernel for over a decade.</p>

<h3 id="linux-namespaces">Linux Namespaces</h3>

<p>A <strong>namespace</strong> wraps a global resource and makes it appear private to processes in that namespace. There are currently eight namespace types in Linux:</p>

<table>
  <thead>
    <tr>
      <th>Namespace</th>
      <th>Flag</th>
      <th>Wraps</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Mount</td>
      <td><code class="language-plaintext highlighter-rouge">CLONE_NEWNS</code></td>
      <td>Filesystem mount points</td>
    </tr>
    <tr>
      <td>PID</td>
      <td><code class="language-plaintext highlighter-rouge">CLONE_NEWPID</code></td>
      <td>Process IDs</td>
    </tr>
    <tr>
      <td>Network</td>
      <td><code class="language-plaintext highlighter-rouge">CLONE_NEWNET</code></td>
      <td>Network stack (interfaces, routing, sockets)</td>
    </tr>
    <tr>
      <td>UTS</td>
      <td><code class="language-plaintext highlighter-rouge">CLONE_NEWUTS</code></td>
      <td>Hostname and domain name</td>
    </tr>
    <tr>
      <td>IPC</td>
      <td><code class="language-plaintext highlighter-rouge">CLONE_NEWIPC</code></td>
      <td>SystemV IPC, POSIX message queues</td>
    </tr>
    <tr>
      <td>User</td>
      <td><code class="language-plaintext highlighter-rouge">CLONE_NEWUSER</code></td>
      <td>User/group IDs</td>
    </tr>
    <tr>
      <td>Cgroup</td>
      <td><code class="language-plaintext highlighter-rouge">CLONE_NEWCGROUP</code></td>
      <td>cgroup root view</td>
    </tr>
    <tr>
      <td>Time</td>
      <td><code class="language-plaintext highlighter-rouge">CLONE_NEWTIME</code></td>
      <td>System clocks (added in kernel 5.6)</td>
    </tr>
  </tbody>
</table>

<p>These are not new. <code class="language-plaintext highlighter-rouge">CLONE_NEWNS</code> (mount namespaces) has been in the kernel since 2.4.19 (2002). PID namespaces since 3.8. User namespaces since 3.8 (fully complete in 3.12).</p>

<p><strong>Creating a namespace is just a flag to <code class="language-plaintext highlighter-rouge">clone()</code> or <code class="language-plaintext highlighter-rouge">unshare()</code>:</strong></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Create a child process in new namespaces:</span>
<span class="n">pid_t</span> <span class="n">child</span> <span class="o">=</span> <span class="n">clone</span><span class="p">(</span><span class="n">child_function</span><span class="p">,</span> <span class="n">stack</span><span class="p">,</span>
    <span class="n">CLONE_NEWPID</span> <span class="o">|</span> <span class="n">CLONE_NEWNS</span> <span class="o">|</span> <span class="n">CLONE_NEWUSER</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>

<span class="c1">// Or: enter a new namespace from within a process:</span>
<span class="n">unshare</span><span class="p">(</span><span class="n">CLONE_NEWNS</span><span class="p">);</span>   <span class="c1">// Unshare mount namespace from parent</span>
</code></pre></div></div>

<p>You can experiment without any container tooling:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Enter a new mount namespace as an unprivileged user:</span>
unshare <span class="nt">--mount</span> <span class="nt">--pid</span> <span class="nt">--fork</span> bash

<span class="c"># Inside: a new shell with its own mount namespace</span>
<span class="c"># Mounts created here are invisible outside</span>
mount <span class="nt">--bind</span> /tmp/mydir /mnt/test  <span class="c"># Only visible in this shell</span>
<span class="nb">exit</span>  <span class="c"># Mount gone</span>

<span class="c"># Enter a new user+mount namespace and see what you can do:</span>
unshare <span class="nt">--user</span> <span class="nt">--map-root-user</span> <span class="nt">--mount</span> <span class="nt">--pid</span> <span class="nt">--fork</span> bash
<span class="nb">whoami</span>  <span class="c"># Shows "root" inside the namespace</span>
<span class="nb">id</span>      <span class="c"># But actual UID is still yours</span>
</code></pre></div></div>

<h3 id="cgroups-resource-accounting-and-limits">cgroups: Resource Accounting and Limits</h3>

<p><strong>cgroups</strong> (control groups) attach resource accounting and limits to groups of processes. Where namespaces control <em>visibility</em>, cgroups control <em>resource consumption</em>.</p>

<p>cgroups v1 (legacy, still widely used) and cgroups v2 (unified hierarchy, the current standard) both expose themselves through a virtual filesystem at <code class="language-plaintext highlighter-rouge">/sys/fs/cgroup/</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># See cgroup v2 hierarchy:</span>
<span class="nb">ls</span> /sys/fs/cgroup/
<span class="c"># cgroup.controllers  cgroup.max.depth  cpuacct.usage  memory.current ...</span>

<span class="c"># Your user session has a cgroup slice:</span>
<span class="nb">cat</span> /proc/self/cgroup
<span class="c"># 0::/user.slice/user-1000.slice/session-2.scope</span>

<span class="c"># The cgroup files for memory limits:</span>
<span class="nb">ls</span> /sys/fs/cgroup/user.slice/user-1000.slice/
<span class="c"># memory.current     # Current memory usage</span>
<span class="c"># memory.max         # Hard limit (kill if exceeded)</span>
<span class="c"># memory.high        # Soft limit (throttle before OOM)</span>
<span class="c"># cpu.max            # CPU bandwidth limit: "100000 100000" = 100% of one core</span>
</code></pre></div></div>

<p><strong>What cgroups let you do:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create a cgroup and limit its memory:</span>
<span class="nb">mkdir</span> /sys/fs/cgroup/myapp
<span class="nb">echo</span> <span class="nv">$PID</span> <span class="o">&gt;</span> /sys/fs/cgroup/myapp/cgroup.procs  <span class="c"># Add process to cgroup</span>
<span class="nb">echo</span> <span class="s2">"100M"</span> <span class="o">&gt;</span> /sys/fs/cgroup/myapp/memory.max  <span class="c"># 100MB memory limit</span>
<span class="nb">echo</span> <span class="s2">"50000 100000"</span> <span class="o">&gt;</span> /sys/fs/cgroup/myapp/cpu.max  <span class="c"># 50% CPU limit</span>
</code></pre></div></div>

<p>cgroups v2 reorganizes this into a unified tree where every cgroup inherits from its parent. A process can only be in one leaf cgroup (vs v1 where it could be in different cgroups in different hierarchies). This unified model is cleaner but required updates in systemd, docker, and container runtimes — the migration took years.</p>

<p><strong>cgroups in the context of packaging:</strong></p>
<ul>
  <li>Docker uses cgroups extensively: every container gets its own cgroup with configurable resource limits</li>
  <li>Snap uses systemd slices to assign each snap to a cgroup (for accounting, not typically hard limits on desktop)</li>
  <li>Flatpak creates systemd scopes for tracked apps but doesn’t typically apply resource limits for desktop apps</li>
  <li>AppImage: no cgroup involvement at all</li>
</ul>

<h3 id="overlayfs-layered-filesystems">OverlayFS: Layered Filesystems</h3>

<p>OverlayFS merges multiple directories into a single unified view. This is the mechanism that makes Docker image layers work, and it’s the same mechanism used in various forms across all container technologies.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># OverlayFS basic structure:</span>
<span class="c"># lowerdir: Read-only base layer(s)</span>
<span class="c"># upperdir: Read-write layer (receives all writes)</span>
<span class="c"># workdir:  Working directory (must be same filesystem as upperdir)</span>
<span class="c"># merged:   The unified view presented to the process</span>

<span class="nb">mkdir</span> /tmp/lower /tmp/upper /tmp/work /tmp/merged
<span class="nb">echo</span> <span class="s2">"from lower"</span> <span class="o">&gt;</span> /tmp/lower/existing-file.txt

mount <span class="nt">-t</span> overlay overlay <span class="nt">-o</span> <span class="se">\</span>
  <span class="nv">lowerdir</span><span class="o">=</span>/tmp/lower,<span class="se">\</span>
  <span class="nv">upperdir</span><span class="o">=</span>/tmp/upper,<span class="se">\</span>
  <span class="nv">workdir</span><span class="o">=</span>/tmp/work <span class="se">\</span>
  /tmp/merged

<span class="c"># Inside /tmp/merged:</span>
<span class="nb">ls</span> /tmp/merged        <span class="c"># Shows existing-file.txt from lower</span>
<span class="nb">cat</span> /tmp/merged/existing-file.txt   <span class="c"># "from lower"</span>

<span class="c"># Write a new file:</span>
<span class="nb">echo</span> <span class="s2">"new content"</span> <span class="o">&gt;</span> /tmp/merged/new-file.txt
<span class="c"># Goes to upper layer: /tmp/upper/new-file.txt</span>
<span class="c"># Lower layer is unchanged</span>

<span class="c"># Modify an existing file:</span>
<span class="nb">echo</span> <span class="s2">"modified"</span> <span class="o">&gt;</span> /tmp/merged/existing-file.txt
<span class="c"># Creates a copy in upper layer (copy-on-write)</span>
<span class="c"># /tmp/upper/existing-file.txt now exists with new content</span>
<span class="c"># /tmp/lower/existing-file.txt is unchanged</span>
</code></pre></div></div>

<p><strong>Copy-on-write (CoW)</strong>: When you modify a file that exists in the lower layer, OverlayFS creates a copy in the upper layer. The lower layer is never modified. This is how Docker containers have writable filesystems without copying the entire base image on creation.</p>

<p><strong>Deletion (whiteout files)</strong>: Deleting a file in the lower layer creates a <strong>whiteout</strong> entry in the upper layer — a character device with major/minor 0/0 at the same path. The overlay driver hides the lower layer’s file when it sees a whiteout.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># After deleting /tmp/merged/existing-file.txt:</span>
<span class="nb">ls</span> <span class="nt">-la</span> /tmp/upper/existing-file.txt
<span class="c"># crw------- 1 root root 0, 0 ... existing-file.txt</span>
<span class="c"># That's a whiteout device node — not a regular file</span>
</code></pre></div></div>

<hr />

<h2 id="how-docker-puts-it-together">How Docker Puts It Together</h2>

<p>Docker’s container model uses all these primitives together. Understanding the stack:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Container Process
      │
      │ runs in
      ▼
Linux Namespaces (mnt, pid, net, user, uts, ipc)
      │
      │ isolated filesystem via
      ▼
OverlayFS (image layers + writable upper layer)
      │
      │ resource-limited by
      ▼
cgroups (memory, CPU, block I/O limits)
      │
      │ defined by
      ▼
OCI Runtime (runc/crun)
      │
      │ images stored as
      ▼
OCI Image Spec (layers: tarballs with content-addressable storage)
</code></pre></div></div>

<h3 id="the-oci-specifications">The OCI Specifications</h3>

<p>The <strong>Open Container Initiative</strong> (OCI) defined two specs:</p>

<p><strong>OCI Image Spec</strong>: Defines how container images are structured and stored. An OCI image is:</p>
<ul>
  <li>A manifest (JSON listing layers and config)</li>
  <li>An image config (environment, entrypoint, exposed ports, etc.)</li>
  <li>Layer blobs (gzipped tarballs of filesystem deltas)</li>
</ul>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Simplified</span><span class="w"> </span><span class="err">OCI</span><span class="w"> </span><span class="err">manifest:</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"schemaVersion"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"digest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sha256:abc..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"mediaType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/vnd.oci.image.config.v1+json"</span><span class="w"> </span><span class="p">},</span><span class="w">
  </span><span class="nl">"layers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"digest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sha256:def..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"mediaType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/vnd.oci.image.layer.v1.tar+gzip"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"digest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sha256:ghi..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"mediaType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/vnd.oci.image.layer.v1.tar+gzip"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>OCI Runtime Spec</strong>: Defines what a container runtime must do with an unpacked image. Specifies the <code class="language-plaintext highlighter-rouge">config.json</code> format that describes namespaces to create, mounts to set up, entrypoint to execute, cgroup configuration, and security constraints (AppArmor, seccomp).</p>

<p><code class="language-plaintext highlighter-rouge">runc</code> is the reference OCI runtime — it reads <code class="language-plaintext highlighter-rouge">config.json</code> and produces a running container. Docker, Podman, containerd, and CRI-O all use runc (or crun, a compatible alternative) under the hood.</p>

<h3 id="what-docker-adds-on-top">What Docker Adds on Top</h3>

<p>runc gives you a running container from a config.json. Docker adds:</p>
<ul>
  <li>An image build system (<code class="language-plaintext highlighter-rouge">Dockerfile</code> / <code class="language-plaintext highlighter-rouge">docker build</code>)</li>
  <li>An image registry protocol (Docker Hub and compatible registries)</li>
  <li>A daemon (<code class="language-plaintext highlighter-rouge">dockerd</code>) that manages container lifecycle</li>
  <li>Networking (bridge networks, port mapping, overlay networks for multi-host)</li>
  <li>Volume management</li>
  <li>Docker Compose for multi-container applications</li>
  <li>A container registry client (pull/push)</li>
</ul>

<p>Most of what developers think of as “Docker” is the tooling layer, not the kernel primitives. Those primitives are also available through Podman (daemonless), containerd, or direct runc usage.</p>

<hr />

<h2 id="the-comparison-universal-packages-vs-containers">The Comparison: Universal Packages vs Containers</h2>

<p>Now the interesting part. How do AppImage, Snap, and Flatpak compare to Docker containers when examined through the same lens?</p>

<h3 id="namespace-usage">Namespace Usage</h3>

<table>
  <thead>
    <tr>
      <th>Technology</th>
      <th>Mount NS</th>
      <th>PID NS</th>
      <th>Network NS</th>
      <th>User NS</th>
      <th>cgroup NS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AppImage</td>
      <td>None</td>
      <td>None</td>
      <td>None</td>
      <td>None</td>
      <td>None</td>
    </tr>
    <tr>
      <td>Snap</td>
      <td>Yes</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
    </tr>
    <tr>
      <td>Flatpak</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>No</td>
      <td>Yes</td>
      <td>No</td>
    </tr>
    <tr>
      <td>Docker</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>Optional</td>
      <td>Yes</td>
    </tr>
  </tbody>
</table>

<p><strong>AppImage</strong>: No namespace isolation of any kind. The app runs in your full user session. Complete visibility of all processes, all network interfaces, all mounts. The “isolation” is purely by convention — it can’t touch the squashfs (read-only), but that’s not kernel-enforced isolation.</p>

<p><strong>Snap</strong>: Mount namespace isolation (the app has its own view of the filesystem). No PID, network, or user namespace. The app can see all processes (<code class="language-plaintext highlighter-rouge">ps aux</code> from a Snap process shows the full system), can use all network interfaces, and runs as your real UID. Snap’s security comes from AppArmor and seccomp, not namespace isolation.</p>

<p><strong>Flatpak</strong>: Mount + PID + User namespaces. The app has a private PID space (can’t directly signal processes outside), filesystem isolation via bind mounts, and a remapped UID. <strong>No network namespace</strong> — Flatpak apps with network permission access your real network stack. The <code class="language-plaintext highlighter-rouge">--share=network</code> flag doesn’t create a new namespace, it allows access to the host network. No network isolation between Flatpak apps.</p>

<p><strong>Docker</strong>: All namespaces except user (user namespaces in Docker are supported but opt-in due to compatibility concerns with bind mounts and file permissions). Full network isolation by default — each container gets its own network stack, own loopback, own IP address.</p>

<h3 id="what-this-means-in-practice">What This Means in Practice</h3>

<p>Flatpak and Snap provide what security researchers call <strong>reduced attack surface</strong> isolation, not the kind of isolation you’d rely on for running untrusted code. If a Flatpak app is compromised, it might not be able to read your files (if filesystem access isn’t granted), but it can still:</p>
<ul>
  <li>Make arbitrary network connections (if <code class="language-plaintext highlighter-rouge">--share=network</code>)</li>
  <li>See other running processes by PID (via <code class="language-plaintext highlighter-rouge">/proc</code> in Snap, slightly restricted in Flatpak)</li>
  <li>Communicate via D-Bus with other services</li>
</ul>

<p>Docker provides genuine network isolation, PID isolation, and resource limits. A compromised Docker container can’t directly reach your host network or see host processes (absent specific configurations or kernel vulnerabilities).</p>

<p>The intended use cases differ: Flatpak/Snap are for applications that need to run as <em>your user</em> with <em>your data</em>. Containers are for running services that shouldn’t have <em>any</em> access to the host.</p>

<h3 id="storage-layer-comparison">Storage Layer Comparison</h3>

<table>
  <thead>
    <tr>
      <th>Technology</th>
      <th>Storage Format</th>
      <th>Deduplication</th>
      <th>Write Layer</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AppImage</td>
      <td>squashfs (single file)</td>
      <td>None</td>
      <td>N/A (read-only)</td>
    </tr>
    <tr>
      <td>Snap</td>
      <td>squashfs (loop-mounted)</td>
      <td>None between apps</td>
      <td><code class="language-plaintext highlighter-rouge">/var/snap/&lt;app&gt;/</code></td>
    </tr>
    <tr>
      <td>Flatpak</td>
      <td>OSTree (hardlinked objects)</td>
      <td>Yes (between runtimes/versions)</td>
      <td><code class="language-plaintext highlighter-rouge">~/.var/app/&lt;app&gt;/</code></td>
    </tr>
    <tr>
      <td>Docker</td>
      <td>OverlayFS layers</td>
      <td>Yes (between images sharing layers)</td>
      <td>Container layer (tmpfs or volume)</td>
    </tr>
  </tbody>
</table>

<p>Flatpak and Docker both use content-addressable storage with deduplication. AppImage and Snap don’t deduplicate between app instances. If you have 10 snaps that all bundle GTK4, you have 10 copies of GTK4. If you have 10 Flatpaks that all use GNOME Platform 45, you have one shared copy of GNOME Platform 45 (via OSTree hardlinks).</p>

<p>Docker goes further — if two images share the same base layers (e.g., both use the same Ubuntu 22.04 base), those layers are stored once. A 100-container deployment might have only a few GB of actual storage if the images share base layers.</p>

<h3 id="security-model-comparison">Security Model Comparison</h3>

<table>
  <thead>
    <tr>
      <th>Technology</th>
      <th>Isolation Mechanism</th>
      <th>Strength</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AppImage</td>
      <td>None</td>
      <td>None</td>
      <td>Trust-based; runs as your user</td>
    </tr>
    <tr>
      <td>Snap</td>
      <td>AppArmor + seccomp</td>
      <td>Medium-High</td>
      <td>Strong MAC, no namespace isolation</td>
    </tr>
    <tr>
      <td>Flatpak</td>
      <td>Namespaces + portals</td>
      <td>Medium</td>
      <td>Good isolation, weaker than Docker</td>
    </tr>
    <tr>
      <td>Docker</td>
      <td>Namespaces + seccomp + capabilities</td>
      <td>High</td>
      <td>Near-VM isolation for most purposes</td>
    </tr>
  </tbody>
</table>

<p>“Stronger” doesn’t mean “better for all use cases.” AppImage’s lack of isolation is a feature when you want the app to have the same access as a native binary. Snap’s AppArmor model is actually more expressive than Docker’s isolation in some ways — you can define per-interface permissions at the D-Bus method level.</p>

<h3 id="the-cgroups-difference">The cgroups Difference</h3>

<p>This is where containers diverge most clearly from universal packages.</p>

<p>Docker (and Kubernetes, Podman, etc.) use cgroups to provide hard resource limits: a container can be configured to use no more than 2GB of RAM and 1 CPU core, regardless of what the code inside tries to do. The kernel enforces this.</p>

<p>Universal packages don’t typically set hard resource limits. A misbehaving Flatpak app can consume all available RAM. A runaway Snap process can spin a CPU core to 100%. The cgroup integration that Snap and Flatpak have (via systemd slices) is for accounting and throttling, not hard limits.</p>

<p>If you need hard resource limits on a desktop app — and sometimes you do, for background sync services, document processors, anything that could go rogue — containers are the right tool.</p>

<hr />

<h2 id="when-these-technologies-actually-overlap">When These Technologies Actually Overlap</h2>

<p>The comparison so far treats them as different categories, but there are real overlaps:</p>

<h3 id="flatpak-on-the-server">Flatpak on the Server?</h3>

<p>You can run Flatpak apps headless — <code class="language-plaintext highlighter-rouge">flatpak run</code> doesn’t require a display. For build environments, reproducible testing, or distributing server-side GUI tools to heterogeneous Linux systems, Flatpak works. It doesn’t have network isolation, so it’s not a drop-in for containers, but for “reproducible builds that run on any distro” it’s a reasonable choice.</p>

<h3 id="snaps-as-service-containers">Snaps as Service Containers</h3>

<p>On Ubuntu Core, snap daemons are effectively lightweight service containers. They have filesystem isolation, AppArmor confinement, automatic updates, and rollback. For IoT devices where you want managed services without the overhead of Docker, Snap fills a real niche.</p>

<h3 id="containers-for-desktop">Containers for Desktop?</h3>

<p>Distrobox and toolbox (Fedora’s tool) run Flatpak-style home directory sharing with Docker/Podman containers to give you a mutable, different-distro environment on your desktop. Your GUI apps run in a container that has full access to <code class="language-plaintext highlighter-rouge">~</code> and the display server — deliberately collapsing the container’s isolation to get cross-distro compatibility. This is the same tradeoff as Flatpak’s <code class="language-plaintext highlighter-rouge">--filesystem=home</code>.</p>

<h3 id="the-ociflatpak-convergence-experiments">The OCI/Flatpak Convergence Experiments</h3>

<p>There have been experiments combining the OCI image format with Flatpak’s portal/permission system. The <code class="language-plaintext highlighter-rouge">flatpak-oci</code> bundle format is one — it uses OCI image layers for distribution but Flatpak’s portal infrastructure for runtime. This hasn’t become mainstream but points to the technologies converging at the edges.</p>

<hr />

<h2 id="the-mental-model-a-decision-tree">The Mental Model: A Decision Tree</h2>

<p>When should you use which technology?</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Is this a desktop GUI app?
  Yes → Flatpak (sandboxed) or apt/dnf (native)
  No → continue

Does it need to run on distros without knowing the package manager?
  Yes → AppImage (portable binary) or Docker (if services)
  No → continue

Is it a long-running service?
  Yes → Docker/Podman/Snap (depending on platform and isolation needs)
  No → continue

Does it need hard resource limits?
  Yes → Docker/Podman container
  No → Universal package or native

Does it run on Ubuntu and need auto-patching on a server?
  Yes → Snap
  No → Docker

Is it on a headless embedded system (RPi, IoT)?
  Yes → Snap (Ubuntu Core) or Docker
  No → Use your distro's native package manager first
</code></pre></div></div>

<p>The answer is almost always “native package manager first.” These tools exist for the cases where native doesn’t work.</p>

<hr />

<h2 id="the-shared-ancestry-its-all-clone-and-mount">The Shared Ancestry: It’s All <code class="language-plaintext highlighter-rouge">clone()</code> and <code class="language-plaintext highlighter-rouge">mount()</code></h2>

<p>The insight that ties this together: AppImage, Snap, Flatpak, Docker, LXC, systemd-nspawn — they’re all wrappers around the same small set of Linux syscalls.</p>

<p>Every isolation mechanism here ultimately comes down to:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">clone()</code> with namespace flags</li>
  <li><code class="language-plaintext highlighter-rouge">mount()</code> with bind/overlay options</li>
  <li><code class="language-plaintext highlighter-rouge">write()</code> to <code class="language-plaintext highlighter-rouge">/proc/self/uid_map</code> for user namespace remapping</li>
  <li><code class="language-plaintext highlighter-rouge">prctl(PR_SET_SECCOMP, ...)</code> for syscall filtering</li>
  <li><code class="language-plaintext highlighter-rouge">aa_change_profile()</code> for AppArmor profile transitions</li>
  <li>Writes to <code class="language-plaintext highlighter-rouge">/sys/fs/cgroup/</code> for resource limits</li>
</ul>

<p>The differences are which of these syscalls get called, in what combination, with what policies. Docker calls all of them. Flatpak calls most of them. Snap calls some of them plus AppArmor extensively. AppImage calls none of them.</p>

<p>Understanding this shared foundation explains why:</p>
<ul>
  <li>Container vulnerabilities (like user namespace escapes) affect multiple technologies simultaneously</li>
  <li>Security improvements in the kernel (improved cgroup v2 support, better seccomp audit) benefit all these tools</li>
  <li>The tools can interoperate (Distrobox, Flatpak OCI experiments)</li>
</ul>

<p>The packaging format and the security model are separable concerns. squashfs is just a filesystem. The security happens in the wrapping layer.</p>

<hr />

<h2 id="practical-summary">Practical Summary</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>AppImage</th>
      <th>Snap</th>
      <th>Flatpak</th>
      <th>Docker</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Isolation</strong></td>
      <td>None</td>
      <td>AppArmor+seccomp</td>
      <td>Namespaces+portals</td>
      <td>Full namespaces</td>
    </tr>
    <tr>
      <td><strong>Network isolation</strong></td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td><strong>Resource limits</strong></td>
      <td>No</td>
      <td>Soft (systemd)</td>
      <td>Soft (systemd)</td>
      <td>Hard (cgroups)</td>
    </tr>
    <tr>
      <td><strong>Auto-update</strong></td>
      <td>Optional</td>
      <td>Yes (can hold)</td>
      <td>Yes (can hold)</td>
      <td>You manage</td>
    </tr>
    <tr>
      <td><strong>Root required</strong></td>
      <td>No</td>
      <td>Install: yes</td>
      <td>No</td>
      <td>Daemon: yes</td>
    </tr>
    <tr>
      <td><strong>Storage dedup</strong></td>
      <td>No</td>
      <td>No</td>
      <td>Yes (OSTree)</td>
      <td>Yes (OverlayFS)</td>
    </tr>
    <tr>
      <td><strong>Startup overhead</strong></td>
      <td>Low (FUSE)</td>
      <td>Medium (squashfuse)</td>
      <td>Low (bind mount)</td>
      <td>Low (namespace setup)</td>
    </tr>
    <tr>
      <td><strong>Best for</strong></td>
      <td>Portable single binaries</td>
      <td>Ubuntu server daemons</td>
      <td>Desktop GUI apps</td>
      <td>Services, CI, dev envs</td>
    </tr>
  </tbody>
</table>

<p>The most important row: <strong>isolation</strong>. Choose based on how much isolation you actually need for your threat model, not based on what’s fashionable.</p>

<hr />

<h2 id="resources">Resources</h2>

<ul>
  <li>Linux namespaces deep dive: <code class="language-plaintext highlighter-rouge">man 7 namespaces</code></li>
  <li>cgroups v2 guide: <a href="https://kernel.org/doc/Documentation/admin-guide/cgroup-v2.rst">kernel.org/doc/Documentation/admin-guide/cgroup-v2.rst</a></li>
  <li>OCI specs: <a href="https://opencontainers.org">opencontainers.org</a></li>
  <li>runc source: <a href="https://github.com/opencontainers/runc">github.com/opencontainers/runc</a></li>
  <li>Bubblewrap (Flatpak sandbox): <a href="https://github.com/containers/bubblewrap">github.com/containers/bubblewrap</a></li>
  <li>OverlayFS: <code class="language-plaintext highlighter-rouge">man 8 mount</code> — see overlay filesystem section</li>
  <li>Distrobox: <a href="https://distrobox.it">distrobox.it</a></li>
  <li><code class="language-plaintext highlighter-rouge">unshare(1)</code>: start experimenting with namespaces without writing C</li>
</ul>

<hr />

<p><em>This concludes the Linux Universal Packages series. Full series index at <a href="https://denner.co/blog">denner.co/blog</a>. Talk materials at <a href="https://denner.co/talks">denner.co/talks</a>.</em></p>

<p><em>Andrew Denner — denner.co — @adenner</em></p>]]></content><author><name>Andrew Denner</name></author><summary type="html"><![CDATA[Universal Packages vs Docker: When Does a Package Become a Container? Linux Namespaces, cgroups, OverlayFS, and the OCI Spec Explained]]></summary></entry><entry><title type="html">Build Your Own: Packaging Your App as AppImage, Snap, and Flatpak (part 6)</title><link href="http://0.0.0.0:4000/2026/03/24/build-your-own-6.html" rel="alternate" type="text/html" title="Build Your Own: Packaging Your App as AppImage, Snap, and Flatpak (part 6)" /><published>2026-03-24T15:35:14+00:00</published><updated>2026-03-24T15:35:14+00:00</updated><id>http://0.0.0.0:4000/2026/03/24/build-your-own-6</id><content type="html" xml:base="http://0.0.0.0:4000/2026/03/24/build-your-own-6.html"><![CDATA[<h1 id="build-your-own-packaging-your-app-as-appimage-snap-and-flatpak">Build Your Own: Packaging Your App as AppImage, Snap, and Flatpak</h1>
<h2 id="the-complete-developer-guide--from-zero-to-shipped">The Complete Developer Guide — From Zero to Shipped</h2>

<p><em>Andrew Denner · March 2026 · <a href="https://denner.co">denner.co</a></em>
<em>Part 6 of the <a href="https://denner.co/blog">Linux Universal Packages series</a></em></p>

<hr />

<p><em>This is the post I wish existed when I first tried to package something. It covers the full workflow for all three formats — not just the happy path, but the real path: the gotchas, the debugging loops, the “why does this work on my machine but not on the build server” moments. The talk slides have the abbreviated versions; this is the extended cut.</em></p>

<hr />

<h2 id="before-you-start-pick-your-format">Before You Start: Pick Your Format</h2>

<p>Not everything needs to be packaged in all three formats. A quick decision guide:</p>

<p><strong>Build an AppImage if:</strong></p>
<ul>
  <li>Your app has no deep system integration (no D-Bus services, no systemd units)</li>
  <li>Your users are on diverse distros or locked-down systems without package managers</li>
  <li>You want maximum compatibility with minimum infrastructure</li>
  <li>You just want a single file to hand people</li>
</ul>

<p><strong>Build a Snap if:</strong></p>
<ul>
  <li>Your primary audience is Ubuntu users</li>
  <li>Your app is a long-running service or daemon</li>
  <li>You want Canonical’s review process and store infrastructure</li>
  <li>You’re targeting Ubuntu Core / IoT</li>
</ul>

<p><strong>Build a Flatpak if:</strong></p>
<ul>
  <li>It’s a desktop GUI application</li>
  <li>You want to reach Fedora, Mint, Arch, and Ubuntu users with one package</li>
  <li>You care about sandbox transparency (Flatseal support)</li>
  <li>You want to be on Flathub — the de facto app store for non-Ubuntu Linux desktops</li>
</ul>

<p>If your app is CLI-only: use your distro’s package manager first. None of these formats are designed for CLI tools and the UX will fight you.</p>

<hr />

<h2 id="building-appimages">Building AppImages</h2>

<h3 id="prerequisites">Prerequisites</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install build tools</span>
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
<span class="nb">chmod</span> +x linuxdeploy-x86_64.AppImage

<span class="c"># Optional: plugin for Qt apps</span>
wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
<span class="nb">chmod</span> +x linuxdeploy-plugin-qt-x86_64.AppImage

<span class="c"># For Python apps</span>
wget https://github.com/niess/linuxdeploy-plugin-python/releases/download/continuous/linuxdeploy-plugin-python-x86_64.AppImage
<span class="nb">chmod</span> +x linuxdeploy-plugin-python-x86_64.AppImage

<span class="c"># appimagetool (if you want manual control)</span>
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
<span class="nb">chmod</span> +x appimagetool-x86_64.AppImage
</code></pre></div></div>

<h3 id="the-manual-method-understanding-the-structure">The Manual Method (Understanding the Structure)</h3>

<p>Start here before using linuxdeploy — it teaches you what linuxdeploy is doing:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Create the AppDir</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> MyApp.AppDir/usr/<span class="o">{</span>bin,lib,share/myapp<span class="o">}</span>

<span class="c"># 2. Copy your binary</span>
<span class="nb">cp </span>build/myapp MyApp.AppDir/usr/bin/

<span class="c"># 3. Create the .desktop file (required)</span>
<span class="nb">cat</span> <span class="o">&gt;</span> MyApp.AppDir/myapp.desktop <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
[Desktop Entry]
Name=MyApp
Exec=myapp
Icon=myapp
Type=Application
Categories=Utility;
Comment=My awesome application
</span><span class="no">EOF

</span><span class="c"># 4. Add an icon (required — 256x256 PNG minimum)</span>
<span class="nb">cp </span>assets/myapp.png MyApp.AppDir/myapp.png
<span class="nb">cp </span>assets/myapp.png MyApp.AppDir/usr/share/myapp/

<span class="c"># 5. Create AppRun (the entry point)</span>
<span class="nb">cat</span> <span class="o">&gt;</span> MyApp.AppDir/AppRun <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">APPRUN</span><span class="sh">'
#!/bin/bash
set -e
SELF=</span><span class="si">$(</span><span class="nb">readlink</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$0</span><span class="s2">"</span><span class="si">)</span><span class="sh">
HERE=</span><span class="k">${</span><span class="nv">SELF</span><span class="p">%/*</span><span class="k">}</span><span class="sh">
export PATH="</span><span class="k">${</span><span class="nv">HERE</span><span class="k">}</span><span class="sh">/usr/bin/:</span><span class="k">${</span><span class="nv">PATH</span><span class="k">}</span><span class="sh">"
export LD_LIBRARY_PATH="</span><span class="k">${</span><span class="nv">HERE</span><span class="k">}</span><span class="sh">/usr/lib/:</span><span class="k">${</span><span class="nv">LD_LIBRARY_PATH</span><span class="k">}</span><span class="sh">"
export XDG_DATA_DIRS="</span><span class="k">${</span><span class="nv">HERE</span><span class="k">}</span><span class="sh">/usr/share/:</span><span class="k">${</span><span class="nv">XDG_DATA_DIRS</span><span class="k">}</span><span class="sh">"
exec "</span><span class="k">${</span><span class="nv">HERE</span><span class="k">}</span><span class="sh">/usr/bin/myapp" "</span><span class="nv">$@</span><span class="sh">"
</span><span class="no">APPRUN
</span><span class="nb">chmod</span> +x MyApp.AppDir/AppRun

<span class="c"># 6. Bundle shared libraries (manual approach)</span>
<span class="c"># Find what your binary needs:</span>
ldd MyApp.AppDir/usr/bin/myapp

<span class="c"># Copy each .so that isn't a basic system library:</span>
<span class="c"># Skip: libm, libc, libpthread, libdl, ld-linux (these are always present)</span>
<span class="c"># Copy: everything else</span>
<span class="nb">cp</span> /usr/lib/x86_64-linux-gnu/libssl.so.1.1 MyApp.AppDir/usr/lib/
<span class="nb">cp</span> /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 MyApp.AppDir/usr/lib/
<span class="c"># ... etc</span>

<span class="c"># 7. Build the AppImage</span>
./appimagetool-x86_64.AppImage MyApp.AppDir MyApp-1.0-x86_64.AppImage
</code></pre></div></div>

<h3 id="the-linuxdeploy-method-production-workflow">The linuxdeploy Method (Production Workflow)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># For a compiled app (C/C++, Rust, Go binary):</span>
./linuxdeploy-x86_64.AppImage <span class="se">\</span>
  <span class="nt">--appdir</span> MyApp.AppDir <span class="se">\</span>
  <span class="nt">--executable</span> build/myapp <span class="se">\</span>
  <span class="nt">--desktop-file</span> myapp.desktop <span class="se">\</span>
  <span class="nt">--icon-file</span> assets/myapp.png <span class="se">\</span>
  <span class="nt">--output</span> appimage

<span class="c"># For a Qt app:</span>
<span class="nb">export </span><span class="nv">QMAKE</span><span class="o">=</span>/usr/lib/qt5/bin/qmake  <span class="c"># Point to your Qt installation</span>
./linuxdeploy-x86_64.AppImage <span class="se">\</span>
  <span class="nt">--appdir</span> MyApp.AppDir <span class="se">\</span>
  <span class="nt">--executable</span> build/myapp <span class="se">\</span>
  <span class="nt">--desktop-file</span> myapp.desktop <span class="se">\</span>
  <span class="nt">--icon-file</span> assets/myapp.png <span class="se">\</span>
  <span class="nt">--plugin</span> qt <span class="se">\</span>
  <span class="nt">--output</span> appimage

<span class="c"># For a Python app:</span>
./linuxdeploy-x86_64.AppImage <span class="se">\</span>
  <span class="nt">--appdir</span> MyApp.AppDir <span class="se">\</span>
  <span class="nt">--executable</span> /usr/bin/python3 <span class="se">\</span>
  <span class="nt">--desktop-file</span> myapp.desktop <span class="se">\</span>
  <span class="nt">--icon-file</span> assets/myapp.png <span class="se">\</span>
  <span class="nt">--plugin</span> python <span class="se">\</span>
  <span class="nt">--output</span> appimage
</code></pre></div></div>

<p>linuxdeploy automates:</p>
<ul>
  <li>Running <code class="language-plaintext highlighter-rouge">ldd</code> to find all dependencies</li>
  <li>Copying <code class="language-plaintext highlighter-rouge">.so</code> files into <code class="language-plaintext highlighter-rouge">usr/lib/</code></li>
  <li>Patching ELF <code class="language-plaintext highlighter-rouge">rpath</code> so the bundled libs are found at runtime</li>
  <li>Handling Qt plugins (<code class="language-plaintext highlighter-rouge">.so</code> files for image formats, SQL drivers, etc.)</li>
  <li>Generating the squashfs and attaching the runtime</li>
</ul>

<h3 id="the-most-important-rule-build-on-old-linux">The Most Important Rule: Build on Old Linux</h3>

<p>The glibc symbol version problem is real and will bite you:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check which glibc symbols your binary needs:</span>
objdump <span class="nt">-T</span> build/myapp | <span class="nb">grep </span>GLIBC | <span class="nb">sort</span> <span class="nt">-t</span>@ <span class="nt">-k2</span> | <span class="nb">tail</span>

<span class="c"># Output example:</span>
<span class="c"># 0000...  GLIBC_2.33  memmove</span>
<span class="c"># 0000...  GLIBC_2.34  pthread_create</span>
</code></pre></div></div>

<p>If you build on Ubuntu 24.04 (glibc 2.38) and someone tries to run your AppImage on Ubuntu 20.04 (glibc 2.31), they get:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./MyApp.AppImage: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found
</code></pre></div></div>

<p><strong>The fix</strong>: Build on the oldest distro you want to support, or use Docker to target a specific old distro:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Build inside Ubuntu 20.04 container:</span>
docker run <span class="nt">--rm</span> <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>:/build ubuntu:20.04 bash <span class="nt">-c</span> <span class="s2">"
  apt-get update -qq
  apt-get install -y build-essential cmake ...
  cd /build
  cmake -B builddir &amp;&amp; cmake --build builddir
"</span>
<span class="c"># Now run linuxdeploy on the output</span>
</code></pre></div></div>

<p>The AppImageKit wiki recommends building on CentOS 7 (glibc 2.17) for maximum compatibility. In practice, Ubuntu 20.04 is the most pragmatic baseline for 2025+.</p>

<h3 id="adding-update-support">Adding Update Support</h3>

<p>To enable <code class="language-plaintext highlighter-rouge">appimageupdate</code> integration, add the update URL to the AppImage at build time:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># For GitHub releases:</span>
<span class="nv">UPDATE_INFO</span><span class="o">=</span><span class="s2">"gh-releases-zsync|YourOrg|YourRepo|latest|MyApp-*-x86_64.AppImage.zsync"</span>

./linuxdeploy-x86_64.AppImage <span class="se">\</span>
  <span class="nt">--appdir</span> MyApp.AppDir <span class="se">\</span>
  <span class="nt">--executable</span> build/myapp <span class="se">\</span>
  <span class="nt">--desktop-file</span> myapp.desktop <span class="se">\</span>
  <span class="nt">--icon-file</span> assets/myapp.png <span class="se">\</span>
  <span class="nt">--updateinformation</span> <span class="s2">"</span><span class="nv">$UPDATE_INFO</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">--output</span> appimage
</code></pre></div></div>

<p>This embeds the update URL in the AppImage. Users with AppImageLauncher get automatic update notifications. Users with <code class="language-plaintext highlighter-rouge">appimageupdate</code> CLI can run delta updates.</p>

<h3 id="adding-gpg-signing">Adding GPG Signing</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Sign the AppImage with your GPG key:</span>
./appimagetool-x86_64.AppImage <span class="se">\</span>
  <span class="nt">--sign</span> <span class="se">\</span>
  <span class="nt">--sign-key</span> YOUR_GPG_FINGERPRINT <span class="se">\</span>
  MyApp.AppDir <span class="se">\</span>
  MyApp-1.0-x86_64.AppImage

<span class="c"># This embeds the signature in an ELF section and creates MyApp-1.0-x86_64.AppImage.digest</span>
</code></pre></div></div>

<h3 id="debugging-appimages">Debugging AppImages</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># What's inside?</span>
./MyApp.AppImage <span class="nt">--appimage-extract</span>
<span class="nb">ls </span>squashfs-root/

<span class="c"># Mount it for inspection (no extraction):</span>
./MyApp.AppImage <span class="nt">--appimage-mount</span>
<span class="c"># Prints the mountpoint path, Ctrl+C to unmount</span>

<span class="c"># Run the extracted binary directly (bypass FUSE):</span>
squashfs-root/AppRun

<span class="c"># Extract and debug with strace:</span>
./MyApp.AppImage <span class="nt">--appimage-extract</span>
strace <span class="nt">-e</span> openat squashfs-root/AppRun 2&gt;&amp;1 | <span class="nb">grep</span> <span class="s2">"No such file"</span>
<span class="c"># Shows every file it's trying to open that doesn't exist</span>

<span class="c"># Check offset (useful for debugging self-extraction):</span>
./MyApp.AppImage <span class="nt">--appimage-offset</span>

<span class="c"># Run without FUSE (useful when FUSE isn't available):</span>
<span class="nv">APPIMAGE_EXTRACT_AND_RUN</span><span class="o">=</span>1 ./MyApp.AppImage
</code></pre></div></div>

<h3 id="github-actions-ci-for-appimages">GitHub Actions CI for AppImages</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/appimage.yml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Build AppImage</span>

<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">,</span> <span class="nv">pull_request</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-20.04</span>  <span class="c1"># Use older Ubuntu for glibc compatibility!</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install dependencies</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">sudo apt-get install -y cmake build-essential libssl-dev</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">cmake -B build &amp;&amp; cmake --build build</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Download linuxdeploy</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage</span>
          <span class="s">chmod +x linuxdeploy-x86_64.AppImage</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build AppImage</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">./linuxdeploy-x86_64.AppImage \</span>
            <span class="s">--appdir MyApp.AppDir \</span>
            <span class="s">--executable build/myapp \</span>
            <span class="s">--desktop-file myapp.desktop \</span>
            <span class="s">--icon-file assets/myapp.png \</span>
            <span class="s">--output appimage</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload artifact</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-artifact@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">name</span><span class="pi">:</span> <span class="s">appimage</span>
          <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*.AppImage"</span>
</code></pre></div></div>

<hr />

<h2 id="building-snaps">Building Snaps</h2>

<h3 id="prerequisites-1">Prerequisites</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install snapcraft</span>
<span class="nb">sudo </span>snap <span class="nb">install </span>snapcraft <span class="nt">--classic</span>

<span class="c"># Initialize LXD (snapcraft uses it for clean builds)</span>
<span class="nb">sudo </span>snap <span class="nb">install </span>lxd
<span class="nb">sudo </span>lxd init <span class="nt">--minimal</span>
<span class="nb">sudo </span>usermod <span class="nt">-aG</span> lxd <span class="nv">$USER</span>
newgrp lxd
</code></pre></div></div>

<h3 id="the-snapcraftyaml">The snapcraft.yaml</h3>

<p>Snapcraft builds from a single <code class="language-plaintext highlighter-rouge">snapcraft.yaml</code> in your project root (or in <code class="language-plaintext highlighter-rouge">snap/snapcraft.yaml</code>). Here’s a complete example:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">myapp</span>
<span class="na">base</span><span class="pi">:</span> <span class="s">core22</span>          <span class="c1"># Ubuntu 22.04 LTS runtime</span>
<span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1.0.0'</span>
<span class="na">summary</span><span class="pi">:</span> <span class="s">My App — one line description</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
  <span class="s">Extended description of what your app does.</span>
  <span class="s">Can be multiple paragraphs.</span>

<span class="na">grade</span><span class="pi">:</span> <span class="s">stable</span>         <span class="c1"># or 'devel' for non-release</span>
<span class="na">confinement</span><span class="pi">:</span> <span class="s">strict</span>   <span class="c1"># 'devmode' for development, 'classic' for full system access</span>

<span class="na">apps</span><span class="pi">:</span>
  <span class="na">myapp</span><span class="pi">:</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">bin/myapp</span>
    <span class="na">plugs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">home</span>           <span class="c1"># Access ~/</span>
      <span class="pi">-</span> <span class="s">network</span>        <span class="c1"># Network access</span>
      <span class="pi">-</span> <span class="s">wayland</span>        <span class="c1"># Wayland display</span>
      <span class="pi">-</span> <span class="s">x11</span>            <span class="c1"># X11 fallback</span>
      <span class="pi">-</span> <span class="s">desktop</span>        <span class="c1"># Desktop environment integration</span>
      <span class="pi">-</span> <span class="s">audio-playback</span> <span class="c1"># PulseAudio/PipeWire (for apps with sound)</span>

<span class="na">parts</span><span class="pi">:</span>
  <span class="na">myapp</span><span class="pi">:</span>
    <span class="na">plugin</span><span class="pi">:</span> <span class="s">cmake</span>
    <span class="na">source</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">cmake-parameters</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">-DCMAKE_INSTALL_PREFIX=/</span>
      <span class="pi">-</span> <span class="s">-DCMAKE_BUILD_TYPE=Release</span>
    <span class="na">build-packages</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">libssl-dev</span>
      <span class="pi">-</span> <span class="s">pkg-config</span>
    <span class="na">stage-packages</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">libssl3</span>         <span class="c1"># Runtime libraries (from Ubuntu 22.04 repos)</span>
</code></pre></div></div>

<h3 id="build-commands">Build Commands</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Build the snap (downloads base, builds in LXD VM):</span>
snapcraft

<span class="c"># Output: myapp_1.0.0_amd64.snap</span>

<span class="c"># Install locally for testing:</span>
<span class="nb">sudo </span>snap <span class="nb">install </span>myapp_1.0.0_amd64.snap <span class="nt">--dangerous</span>

<span class="c"># Test it:</span>
snap run myapp

<span class="c"># Check what's blocked (AppArmor denials):</span>
snap logs myapp

<span class="c"># Rebuild just a specific part (faster during development):</span>
snapcraft clean myapp
snapcraft build myapp

<span class="c"># Debug shell inside the build environment:</span>
snapcraft prime <span class="nt">--debug</span>  <span class="c"># drops you to a shell before packaging</span>
</code></pre></div></div>

<h3 id="confinement-debugging">Confinement Debugging</h3>

<p>The most common snap development loop:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Start with devmode (no confinement):</span>
<span class="c"># In snapcraft.yaml: confinement: devmode</span>

<span class="c"># 2. Install and test:</span>
<span class="nb">sudo </span>snap <span class="nb">install </span>myapp_1.0.0_amd64.snap <span class="nt">--dangerous</span> <span class="nt">--devmode</span>

<span class="c"># 3. Watch AppArmor denials:</span>
<span class="nb">sudo </span>journalctl <span class="nt">-f</span> | <span class="nb">grep </span>DENIED

<span class="c"># 4. Map denials to interfaces</span>
<span class="c"># If you see: /run/user/1000/wayland-0 denied → need 'wayland' plug</span>
<span class="c"># If you see: /home/user/.config/ denied → need 'home' plug</span>
<span class="c"># If you see: network denied → need 'network' plug</span>

<span class="c"># 5. Add plugs to snapcraft.yaml, rebuild:</span>
snapcraft

<span class="c"># 6. Install in strict mode:</span>
<span class="nb">sudo </span>snap <span class="nb">install </span>myapp_1.0.0_amd64.snap <span class="nt">--dangerous</span>
snap run myapp

<span class="c"># 7. Repeat until it works, then set confinement: strict</span>
</code></pre></div></div>

<h3 id="the-classic-confinement-option">The Classic Confinement Option</h3>

<p>For developer tools that genuinely need full filesystem access (editors, IDEs, compilers):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">confinement</span><span class="pi">:</span> <span class="s">classic</span>
</code></pre></div></div>

<p>Classic snaps bypass AppArmor confinement entirely — they can access the full filesystem. Publishing a classic snap to the Snap Store requires manual review and justification. Not a rubber stamp; Canonical actually reviews the request.</p>

<p>Examples of legitimate classic snaps: VS Code, JetBrains IDEs, the various language SDKs (Go, .NET, Node).</p>

<h3 id="multi-app-snaps-daemons--cli">Multi-App Snaps (Daemons + CLI)</h3>

<p>Snaps can bundle multiple apps — useful for a service with a management CLI:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apps</span><span class="pi">:</span>
  <span class="na">myapp-daemon</span><span class="pi">:</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">bin/myappd</span>
    <span class="na">daemon</span><span class="pi">:</span> <span class="s">simple</span>        <span class="c1"># or 'forking', 'oneshot', 'notify'</span>
    <span class="na">restart-condition</span><span class="pi">:</span> <span class="s">on-failure</span>
    <span class="na">plugs</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">network</span><span class="pi">]</span>

  <span class="na">myapp-cli</span><span class="pi">:</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">bin/myapp-cli</span>
    <span class="na">plugs</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">home</span><span class="pi">,</span> <span class="nv">network</span><span class="pi">]</span>
</code></pre></div></div>

<p>The daemon app runs as a systemd service. <code class="language-plaintext highlighter-rouge">snap start myapp.myapp-daemon</code> and <code class="language-plaintext highlighter-rouge">snap stop myapp.myapp-daemon</code> control it.</p>

<h3 id="publishing-to-the-snap-store">Publishing to the Snap Store</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Register your app name (one-time):</span>
snapcraft register myapp

<span class="c"># Login to the store:</span>
snapcraft login

<span class="c"># Upload:</span>
snapcraft upload myapp_1.0.0_amd64.snap <span class="nt">--release</span> stable

<span class="c"># Or release a specific revision to a channel:</span>
snapcraft release myapp 1 stable      <span class="c"># revision 1 → stable</span>
snapcraft release myapp 1 candidate   <span class="c"># same revision to candidate</span>

<span class="c"># Check status:</span>
snapcraft status myapp
</code></pre></div></div>

<p><strong>Snap Store review process</strong>: Automated checks run on upload. Strict snaps with common interfaces auto-approve. Snaps using sensitive interfaces (browser-support, classic confinement, etc.) queue for manual review. Typically 1-5 business days.</p>

<h3 id="snapcraftyaml-patterns">snapcraft.yaml Patterns</h3>

<p><strong>Python application</strong>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parts</span><span class="pi">:</span>
  <span class="na">myapp</span><span class="pi">:</span>
    <span class="na">plugin</span><span class="pi">:</span> <span class="s">python</span>
    <span class="na">source</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">python-requirements</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">requirements.txt</span>
</code></pre></div></div>

<p><strong>Electron application</strong>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parts</span><span class="pi">:</span>
  <span class="na">myapp</span><span class="pi">:</span>
    <span class="na">plugin</span><span class="pi">:</span> <span class="s">npm</span>
    <span class="na">source</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">npm-include-node</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">npm-node-version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">20.0.0'</span>
</code></pre></div></div>

<p><strong>Go application</strong>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parts</span><span class="pi">:</span>
  <span class="na">myapp</span><span class="pi">:</span>
    <span class="na">plugin</span><span class="pi">:</span> <span class="s">go</span>
    <span class="na">source</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">build-snaps</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">go</span><span class="pi">]</span>
</code></pre></div></div>

<hr />

<h2 id="building-flatpaks">Building Flatpaks</h2>

<h3 id="prerequisites-2">Prerequisites</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install flatpak and flatpak-builder</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>flatpak flatpak-builder    <span class="c"># Debian/Ubuntu</span>
<span class="nb">sudo </span>dnf <span class="nb">install </span>flatpak flatpak-builder    <span class="c"># Fedora/RHEL</span>

<span class="c"># Add Flathub (if not already added)</span>
flatpak remote-add <span class="nt">--if-not-exists</span> flathub https://flathub.org/repo/flathub.flatpakrepo

<span class="c"># Install the GNOME SDK (or whichever runtime you'll use):</span>
flatpak <span class="nb">install </span>org.gnome.Sdk//47
flatpak <span class="nb">install </span>org.gnome.Platform//47

<span class="c"># For KDE apps:</span>
flatpak <span class="nb">install </span>org.kde.Sdk//6.8
flatpak <span class="nb">install </span>org.kde.Platform//6.8
</code></pre></div></div>

<h3 id="the-manifest">The Manifest</h3>

<p>Flatpak manifests can be JSON or YAML. YAML is easier to read and maintain:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># com.example.MyApp.yaml</span>
<span class="na">app-id</span><span class="pi">:</span> <span class="s">com.example.MyApp</span>

<span class="c1"># The runtime provides the base libraries (GTK, Qt, etc.)</span>
<span class="na">runtime</span><span class="pi">:</span> <span class="s">org.gnome.Platform</span>
<span class="na">runtime-version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">47'</span>

<span class="c1"># The SDK is the build-time equivalent of the runtime</span>
<span class="na">sdk</span><span class="pi">:</span> <span class="s">org.gnome.Sdk</span>

<span class="c1"># Entry point after installation</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">myapp</span>

<span class="c1"># Sandbox permissions</span>
<span class="na">finish-args</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">--share=network</span>          <span class="c1"># Allow network access</span>
  <span class="pi">-</span> <span class="s">--socket=wayland</span>         <span class="c1"># Wayland display protocol</span>
  <span class="pi">-</span> <span class="s">--socket=fallback-x11</span>    <span class="c1"># X11 fallback for non-Wayland compositors</span>
  <span class="pi">-</span> <span class="s">--device=dri</span>             <span class="c1"># GPU access (hardware rendering)</span>
  <span class="pi">-</span> <span class="s">--share=ipc</span>              <span class="c1"># Shared memory (required for X11)</span>
  <span class="pi">-</span> <span class="s">--filesystem=home</span>        <span class="c1"># Access home directory (use sparingly)</span>

<span class="c1"># Modules: your app and its non-runtime dependencies</span>
<span class="na">modules</span><span class="pi">:</span>
  <span class="c1"># Optional: build a library dep that isn't in the runtime</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">libfoo</span>
    <span class="na">buildsystem</span><span class="pi">:</span> <span class="s">cmake-ninja</span>
    <span class="na">sources</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">git</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s">https://github.com/example/libfoo.git</span>
        <span class="na">tag</span><span class="pi">:</span> <span class="s">v2.1.0</span>
        <span class="na">commit</span><span class="pi">:</span> <span class="s">abc123def456...</span>   <span class="c1"># Always pin a commit, not just a tag</span>

  <span class="c1"># Your main application</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">myapp</span>
    <span class="na">buildsystem</span><span class="pi">:</span> <span class="s">cmake-ninja</span>
    <span class="na">sources</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">git</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s">https://github.com/yourorg/myapp.git</span>
        <span class="na">tag</span><span class="pi">:</span> <span class="s">v1.0.0</span>
        <span class="na">commit</span><span class="pi">:</span> <span class="s">def456abc123...</span>
</code></pre></div></div>

<h3 id="build-and-test-cycle">Build and Test Cycle</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Build and install locally (--user = user installation, no root needed):</span>
flatpak-builder <span class="nt">--install</span> <span class="nt">--user</span> <span class="nt">--force-clean</span> builddir com.example.MyApp.yaml

<span class="c"># Run it:</span>
flatpak run com.example.MyApp

<span class="c"># Debug: drop into a shell inside the sandbox</span>
flatpak run <span class="nt">--command</span><span class="o">=</span>bash com.example.MyApp

<span class="c"># Debug: inspect the sandbox from inside</span>
<span class="nb">cat</span> /run/user/<span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span>/flatpak/app/com.example.MyApp/.flatpak-info

<span class="c"># Rebuild just to check the manifest without installing:</span>
flatpak-builder <span class="nt">--force-clean</span> builddir com.example.MyApp.yaml

<span class="c"># Check logs when something goes wrong:</span>
journalctl <span class="nt">-f</span> <span class="nt">--user</span>  <span class="c"># In another terminal while running the app</span>
</code></pre></div></div>

<h3 id="handling-dependencies-not-in-the-runtime">Handling Dependencies Not in the Runtime</h3>

<p>The tricky part: any library your app needs that isn’t in the GNOME Platform (or KDE Frameworks) must be included as a module in your manifest.</p>

<p><strong>No network during build</strong>: flatpak-builder enforces this. All sources must be declared with checksums.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">modules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">libsodium</span>
    <span class="na">buildsystem</span><span class="pi">:</span> <span class="s">autotools</span>
    <span class="na">sources</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">archive</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s">https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz</span>
        <span class="na">sha256</span><span class="pi">:</span> <span class="s">6f504490b342a4f8a4c4a02fc9b866cbef8622d5df4e5452b46be121e46636c1</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">libzmq</span>
    <span class="na">buildsystem</span><span class="pi">:</span> <span class="s">cmake-ninja</span>
    <span class="na">sources</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">archive</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s">https://github.com/zeromq/libzmq/releases/download/v4.3.4/zeromq-4.3.4.tar.gz</span>
        <span class="na">sha256</span><span class="pi">:</span> <span class="s">c593001a89f5a85dd2ddf564805deb860e02471171b3f204944857336295c3e5</span>
</code></pre></div></div>

<p><strong>The flatpak-builder-tools helpers</strong> save enormous time for language ecosystems:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># For Python apps (pip dependencies):</span>
git clone https://github.com/flatpak/flatpak-builder-tools
<span class="nb">cd </span>flatpak-builder-tools/pip

python3 flatpak-pip-generator requests pillow numpy <span class="se">\</span>
  <span class="nt">--output</span> python-deps.json

<span class="c"># Add to manifest:</span>
<span class="c"># modules:</span>
<span class="c">#   - python-deps.json</span>
<span class="c">#   - name: myapp</span>
<span class="c">#     ...</span>
</code></pre></div></div>

<p>Similar tools exist for npm, cargo (Rust), and Maven/Gradle (Java).</p>

<h3 id="choosing-the-right-permissions">Choosing the Right Permissions</h3>

<p>The hardest part of Flatpak packaging is getting permissions right. Start restrictive and add only what’s needed:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">finish-args</span><span class="pi">:</span>
  <span class="c1"># Start with these for a GUI app:</span>
  <span class="pi">-</span> <span class="s">--socket=wayland</span>
  <span class="pi">-</span> <span class="s">--socket=fallback-x11</span>
  <span class="pi">-</span> <span class="s">--share=ipc</span>
  <span class="pi">-</span> <span class="s">--device=dri</span>

  <span class="c1"># Add as needed — test before adding:</span>
  <span class="pi">-</span> <span class="s">--share=network</span>        <span class="c1"># Does the app need the internet?</span>
  <span class="pi">-</span> <span class="s">--filesystem=home</span>      <span class="c1"># Does it need to open arbitrary files?</span>
  <span class="pi">-</span> <span class="s">--filesystem=xdg-documents</span>  <span class="c1"># Better: scope to specific dirs</span>
  <span class="pi">-</span> <span class="s">--socket=pulseaudio</span>    <span class="c1"># Does it play audio?</span>
  <span class="pi">-</span> <span class="s">--device=all</span>           <span class="c1"># Only for hardware-specific apps</span>

  <span class="c1"># D-Bus access (be specific):</span>
  <span class="pi">-</span> <span class="s">--talk-name=org.freedesktop.Notifications</span>  <span class="c1"># System notifications</span>
  <span class="pi">-</span> <span class="s">--talk-name=org.gnome.keyring.SystemPrompter</span>  <span class="c1"># Keyring access</span>
  <span class="pi">-</span> <span class="s">--own-name=com.example.MyApp</span>   <span class="c1"># If the app exports a D-Bus service</span>
</code></pre></div></div>

<p>Common mistakes:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">--filesystem=host</code> — grants access to the entire filesystem. Acceptable for file managers, unacceptable for most apps. Flathub reviewers will ask you to justify this.</li>
  <li><code class="language-plaintext highlighter-rouge">--socket=session-bus</code> — grants access to the entire D-Bus session bus. Only use for apps that legitimately need broad D-Bus access.</li>
  <li><code class="language-plaintext highlighter-rouge">--share=ipc</code> — required for X11 apps but not for Wayland-only apps. Include it in the fallback case.</li>
</ul>

<h3 id="the-app-id-and-reverse-domain-convention">The App ID and Reverse Domain Convention</h3>

<p>Flatpak uses reverse-domain app IDs. The rules:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>com.YourDomain.AppName        → If you own yourdomain.com
org.YourOrg.AppName           → For non-profits/open-source orgs
io.github.youruser.AppName    → For GitHub-hosted projects without custom domain
</code></pre></div></div>

<p>Flathub verifies domain ownership for published apps. For <code class="language-plaintext highlighter-rouge">io.github.*</code> IDs, Flathub verifies your GitHub username via OAuth during submission.</p>

<p>Why it matters: the app ID becomes the Flatpak directory structure, the D-Bus service name (if any), and the identity in Flatseal’s permission UI. Changing it later requires users to reinstall.</p>

<h3 id="publishing-to-flathub">Publishing to Flathub</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Test your manifest builds cleanly with no network errors:</span>
flatpak-builder <span class="nt">--force-clean</span> <span class="nt">--sandbox</span> builddir com.example.MyApp.yaml

<span class="c"># 2. Fork github.com/flathub/flathub</span>

<span class="c"># 3. Create com.example.MyApp/com.example.MyApp.yaml in your fork</span>
<span class="c">#    (or com.example.MyApp/com.example.MyApp.json for JSON)</span>

<span class="c"># 4. The Flathub CI will run automatically on your PR:</span>
<span class="c">#    - Linting your manifest</span>
<span class="c">#    - Building the app</span>
<span class="c">#    - Running basic smoke tests</span>
<span class="c">#    - Checking for common issues (network access during build, etc.)</span>

<span class="c"># 5. Human reviewers check:</span>
<span class="c">#    - App ID is appropriate</span>
<span class="c">#    - Permissions are reasonable</span>
<span class="c">#    - Sources are verifiable (no bare HTTP, commit hashes required)</span>
<span class="c">#    - No proprietary bundled binaries for open-source apps</span>

<span class="c"># 6. After approval, you get a dedicated repo under github.com/flathub/</span>
<span class="c">#    Future updates: push to your app's repo, Flathub CI builds and publishes</span>
</code></pre></div></div>

<h3 id="cicd-for-flatpaks">CI/CD for Flatpaks</h3>

<p>Flathub handles CI for Flathub-published apps. For testing in your own repo:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/flatpak.yml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Flatpak CI</span>

<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">,</span> <span class="nv">pull_request</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">container</span><span class="pi">:</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">bilelmoussaoui/flatpak-github-actions:gnome-47</span>
      <span class="na">options</span><span class="pi">:</span> <span class="s">--privileged</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build Flatpak</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">flatpak/flatpak-github-actions/flatpak-builder@v6</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">bundle</span><span class="pi">:</span> <span class="s">MyApp.flatpak</span>
          <span class="na">manifest-path</span><span class="pi">:</span> <span class="s">com.example.MyApp.yaml</span>
          <span class="na">cache-key</span><span class="pi">:</span> <span class="s">flatpak-builder-$</span>
</code></pre></div></div>

<h3 id="updating-your-app-after-flathub-publication">Updating Your App After Flathub Publication</h3>

<p>After initial approval, you maintain a GitHub repo under <code class="language-plaintext highlighter-rouge">github.com/flathub/com.example.MyApp</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># To update: edit the manifest in your Flathub repo</span>
<span class="c1"># Change the commit hash (and url/tag if applicable):</span>
<span class="na">modules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">myapp</span>
    <span class="na">sources</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">git</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s">https://github.com/yourorg/myapp.git</span>
        <span class="na">tag</span><span class="pi">:</span> <span class="s">v1.1.0</span>
        <span class="na">commit</span><span class="pi">:</span> <span class="s">newcommit123...</span>  <span class="c1"># Update this</span>

<span class="c1"># Open a PR → CI runs → merges → Flathub builds and publishes</span>
<span class="c1"># No per-update manual review once you're established</span>
</code></pre></div></div>

<hr />

<h2 id="side-by-side-comparison-the-developer-experience">Side-by-Side Comparison: The Developer Experience</h2>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>AppImage</th>
      <th>Snap</th>
      <th>Flatpak</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Setup time</strong></td>
      <td>30 min</td>
      <td>1-2 hours</td>
      <td>1-3 hours</td>
    </tr>
    <tr>
      <td><strong>Build tool</strong></td>
      <td>linuxdeploy</td>
      <td>snapcraft</td>
      <td>flatpak-builder</td>
    </tr>
    <tr>
      <td><strong>Build isolation</strong></td>
      <td>None (build on host)</td>
      <td>LXD/Multipass VM</td>
      <td>Sandbox (strict)</td>
    </tr>
    <tr>
      <td><strong>Dependency strategy</strong></td>
      <td>Bundle everything</td>
      <td>Bundle + staged debs/rpms</td>
      <td>Runtime + modules</td>
    </tr>
    <tr>
      <td><strong>CI complexity</strong></td>
      <td>Low</td>
      <td>Medium</td>
      <td>Medium-High</td>
    </tr>
    <tr>
      <td><strong>Publishing</strong></td>
      <td>Self-hosted / GitHub releases</td>
      <td>Snap Store (Canonical)</td>
      <td>Flathub (community)</td>
    </tr>
    <tr>
      <td><strong>Review required</strong></td>
      <td>No</td>
      <td>Yes (Snap Store)</td>
      <td>Yes (Flathub)</td>
    </tr>
    <tr>
      <td><strong>Store fee</strong></td>
      <td>Free</td>
      <td>Free (open source)</td>
      <td>Free</td>
    </tr>
    <tr>
      <td><strong>Update mechanism</strong></td>
      <td>Manual/zsync</td>
      <td>Auto (snapd)</td>
      <td>Auto/manual</td>
    </tr>
    <tr>
      <td><strong>glibc concerns</strong></td>
      <td>Yes (build on old distro)</td>
      <td>No (base snap handles it)</td>
      <td>No (runtime handles it)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="the-hardest-part-of-each-format">The Hardest Part of Each Format</h2>

<p><strong>AppImage</strong>: The glibc floor problem and bundling the right — but not too many — shared libraries. Under-bundling causes crashes on other distros. Over-bundling (bundling things like libGL that must match the host GPU driver) also causes crashes. Finding the right set is a process of test, fail, adjust.</p>

<p><strong>Snap</strong>: Getting confinement right without either (a) opening everything up with classic, or (b) spending days mapping AppArmor denials to interface names. The interface reference documentation is good; reading it is mandatory. The other hard part: LXD setup is fiddly, and if your dev machine is itself a VM you’re running nested virtualization.</p>

<p><strong>Flatpak</strong>: The no-network-during-build constraint is the real pain. When your app has 47 Python dependencies, 15 npm packages, and 3 compiled C libraries, generating and maintaining the source manifest for all of them is significant overhead. The <code class="language-plaintext highlighter-rouge">flatpak-builder-tools</code> scripts help but require maintenance. The payoff is reproducible, auditable builds — but you earn it.</p>

<hr />

<h2 id="resources">Resources</h2>

<p><strong>AppImage:</strong></p>
<ul>
  <li>linuxdeploy: <a href="https://github.com/linuxdeploy/linuxdeploy">github.com/linuxdeploy/linuxdeploy</a></li>
  <li>AppImageKit: <a href="https://github.com/AppImage/AppImageKit">github.com/AppImage/AppImageKit</a></li>
  <li>AppImageSpec: <a href="https://github.com/AppImage/AppImageSpec">github.com/AppImage/AppImageSpec</a></li>
  <li>AppImage packaging guide: <a href="https://docs.appimage.org/packaging-guide">docs.appimage.org/packaging-guide</a></li>
</ul>

<p><strong>Snap:</strong></p>
<ul>
  <li>snapcraft docs: <a href="https://snapcraft.io/docs">snapcraft.io/docs</a></li>
  <li>Interface reference: <a href="https://snapcraft.io/docs/supported-interfaces">snapcraft.io/docs/supported-interfaces</a></li>
  <li>snapcraft.yaml reference: <a href="https://snapcraft.io/docs/snapcraft-yaml-reference">snapcraft.io/docs/snapcraft-yaml-reference</a></li>
</ul>

<p><strong>Flatpak:</strong></p>
<ul>
  <li>flatpak-builder docs: <a href="https://docs.flatpak.org">docs.flatpak.org</a></li>
  <li>Flathub submission guide: <a href="https://docs.flathub.org/docs/for-app-authors">docs.flathub.org/docs/for-app-authors</a></li>
  <li>flatpak-builder-tools: <a href="https://github.com/flatpak/flatpak-builder-tools">github.com/flatpak/flatpak-builder-tools</a></li>
  <li>Manifest reference: <a href="https://docs.flatpak.org/en/latest/flatpak-builder-command-reference.html">docs.flatpak.org/en/latest/flatpak-builder-command-reference.html</a></li>
</ul>

<hr />

<p><em>This concludes the Linux Universal Packages series. Full series index at <a href="https://denner.co/blog">denner.co/blog</a>. Talk materials at <a href="https://denner.co/talks">denner.co/talks</a>.</em></p>

<p><em>Andrew Denner — denner.co — @adenner</em></p>]]></content><author><name>Andrew Denner</name></author><summary type="html"><![CDATA[Build Your Own: Packaging Your App as AppImage, Snap, and Flatpak The Complete Developer Guide — From Zero to Shipped]]></summary></entry><entry><title type="html">Snap Deep Dive: Anatomy of Canonical’s Distribution System (part 3)</title><link href="http://0.0.0.0:4000/2026/03/22/Snap3.html" rel="alternate" type="text/html" title="Snap Deep Dive: Anatomy of Canonical’s Distribution System (part 3)" /><published>2026-03-22T15:35:14+00:00</published><updated>2026-03-22T15:35:14+00:00</updated><id>http://0.0.0.0:4000/2026/03/22/Snap3</id><content type="html" xml:base="http://0.0.0.0:4000/2026/03/22/Snap3.html"><![CDATA[<h1 id="snap-deep-dive-anatomy-of-canonicals-distribution-system">Snap Deep Dive: Anatomy of Canonical’s Distribution System</h1>
<h2 id="the-full-picture--snapd-apparmor-seccomp-and-the-machine-it-built">The Full Picture — snapd, AppArmor, Seccomp, and the Machine It Built</h2>

<p><em>Andrew Denner · March 2026 · <a href="https://denner.co">denner.co</a></em>
<em>Part 3 of the <a href="https://denner.co/blog">Linux Universal Packages series</a></em></p>

<hr />

<p><em>I rage-deleted snapd twice. I’m going to explain exactly what I was rage-deleting. The more you understand about Snap’s architecture, the more you either appreciate the engineering or become more annoyed at the design decisions — sometimes both at once. Let’s go.</em></p>

<hr />

<h2 id="snapd-the-daemon-that-runs-forever">snapd: The Daemon That Runs Forever</h2>

<p>Snap is not just a package format. It’s a daemon (<code class="language-plaintext highlighter-rouge">snapd</code>), a store protocol, a confinement framework, a distribution channel system, and a set of Linux kernel primitives wired together in Go. Understanding Snap means understanding <code class="language-plaintext highlighter-rouge">snapd</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># snapd is always running on Ubuntu:</span>
systemctl status snapd
<span class="c"># Active: active (running)</span>

<span class="c"># It exposes a REST API via Unix socket:</span>
<span class="nb">ls</span> <span class="nt">-la</span> /run/snapd.socket
<span class="c"># srw-rw---- 1 root snapd ... /run/snapd.socket</span>

<span class="c"># The snap CLI just calls this REST API:</span>
<span class="nb">sudo </span>snap <span class="nb">install </span>vlc
<span class="c"># Under the hood: POST /v2/snaps/vlc {"action":"install"}</span>
</code></pre></div></div>

<p>The entire Snap CLI is a thin wrapper over the snapd REST API. Every <code class="language-plaintext highlighter-rouge">snap</code> command — install, remove, refresh, connect, disconnect — is an HTTP call to <code class="language-plaintext highlighter-rouge">/run/snapd.socket</code>. This design means:</p>
<ul>
  <li>snapd must be running for any snap operation to work</li>
  <li>Third-party tools can use the API directly</li>
  <li>The daemon maintains all state and performs all privileged operations (the CLI itself isn’t setuid)</li>
</ul>

<h3 id="snapds-state-machine">snapd’s State Machine</h3>

<p>snapd maintains its state in <code class="language-plaintext highlighter-rouge">/var/lib/snapd/state.json</code>. This JSON file is snapd’s database — it records every installed snap, every connection between snaps, every pending operation, and the complete revision history.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>python3 <span class="nt">-c</span> <span class="s2">"import json,sys; d=json.load(open('/var/lib/snapd/state.json')); print(list(d.keys()))"</span>
<span class="c"># ['data', 'changes', 'tasks', 'last-task-id', 'last-change-id', 'last-notice-id', 'notices']</span>
</code></pre></div></div>

<p>Every operation in snapd is a <strong>change</strong> composed of <strong>tasks</strong>. When you run <code class="language-plaintext highlighter-rouge">snap install vlc</code>, snapd creates a change with tasks like <code class="language-plaintext highlighter-rouge">download-snap</code>, <code class="language-plaintext highlighter-rouge">validate-snap</code>, <code class="language-plaintext highlighter-rouge">mount-snap</code>, <code class="language-plaintext highlighter-rouge">copy-snap-data</code>, <code class="language-plaintext highlighter-rouge">link-snap</code>. If the operation fails halfway, snapd can resume or roll back from the last known-good state. This is the reliability story — snapd never leaves your system in a partial state.</p>

<hr />

<h2 id="snap-types-not-all-snaps-are-equal">Snap Types: Not All Snaps Are Equal</h2>

<p>The word “snap” refers to several distinct things in Canonical’s ecosystem:</p>

<p><strong>App snaps</strong>: What most people mean. User-installed applications. <code class="language-plaintext highlighter-rouge">snap install vlc</code></p>

<p><strong>Base snaps</strong>: Minimal root filesystems that other snaps run on top of. <code class="language-plaintext highlighter-rouge">core22</code> is the Ubuntu 22.04 base. <code class="language-plaintext highlighter-rouge">core20</code>, <code class="language-plaintext highlighter-rouge">core18</code> for older bases. When you install VLC, snapd also installs <code class="language-plaintext highlighter-rouge">core22</code> if not present — VLC’s sandbox uses <code class="language-plaintext highlighter-rouge">core22</code> as its filesystem root.</p>

<p><strong>Kernel snaps</strong>: On Ubuntu Core (IoT/embedded), the Linux kernel itself is a snap. This allows over-the-air kernel updates with the same rollback mechanism as app snaps.</p>

<p><strong>Gadget snaps</strong>: Also Ubuntu Core. Defines the device’s boot configuration, disk layout, and hardware-specific initialization.</p>

<p><strong>Content snaps</strong>: Share data (fonts, icons, themes, SDKs) between snaps via the <code class="language-plaintext highlighter-rouge">content</code> interface. The GNOME Platform snap is a content snap that provides GTK runtime files, reducing duplication between apps.</p>

<p>For desktop Linux, you’re dealing with app snaps and base snaps. But understanding base snaps explains why <code class="language-plaintext highlighter-rouge">snap install</code> sometimes downloads 70MB before it installs your 5MB app — it’s downloading the base runtime if you don’t have it.</p>

<hr />

<h2 id="the-disk-layout-what-gets-mounted-where">The Disk Layout: What Gets Mounted Where</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/snap/
├── vlc/                     # App namespace
│   ├── 3221/                # Revision 3221 (squashfs mounted here)
│   ├── 3198/                # Revision 3198 (previous, kept for rollback)
│   └── current -&gt; 3221/     # Symlink to active revision
├── core22/                  # Base snap
│   ├── 607/
│   └── current -&gt; 607/
├── snapd/                   # snapd itself (as a snap)
│   └── ...
└── bin/                     # Symlinks: snap, snapctl, etc.

/var/lib/snapd/
├── snaps/                   # The actual .snap squashfs files
│   ├── vlc_3221.snap        # squashfs file (read-only source)
│   └── vlc_3198.snap        # Previous revision (kept until manually removed)
├── state.json               # snapd's database
├── assertions/              # Cryptographic assertions (more on this below)
└── mount/                   # Mount namespace configuration files

/var/snap/
└── vlc/
    ├── 3221/                # Revision-specific data (wiped on update)
    ├── current -&gt; 3221/     # Symlink
    └── common/              # Persistent data (survives updates)
</code></pre></div></div>

<p>The squashfs file in <code class="language-plaintext highlighter-rouge">/var/lib/snapd/snaps/vlc_3221.snap</code> is mounted read-only at <code class="language-plaintext highlighter-rouge">/snap/vlc/3221/</code> via the kernel loop device. There’s no FUSE involved — snapd uses the kernel’s native loop device and squashfs support:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mount | <span class="nb">grep </span>snap
<span class="c"># /dev/loop4 on /snap/vlc/3221 type squashfs (ro,nodev,relatime,errors=continue,threads=single)</span>
</code></pre></div></div>

<p>This is why Snap’s performance is generally better than AppImage’s FUSE approach — the kernel reads squashfs directly without the userspace round-trip. The latency penalty comes from the AppArmor and seccomp machinery that wraps execution, not the filesystem access itself.</p>

<hr />

<h2 id="snap-confine-the-jail-that-runs-your-app">snap-confine: The Jail That Runs Your App</h2>

<p>When you launch a Snap application, you don’t directly exec the binary at <code class="language-plaintext highlighter-rouge">/snap/vlc/3221/usr/bin/vlc</code>. You go through <code class="language-plaintext highlighter-rouge">snap-confine</code>, a small C binary with <code class="language-plaintext highlighter-rouge">setuid root</code> permissions that sets up the execution environment:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>user launches vlc
  → /snap/bin/vlc (shell wrapper)
  → /usr/lib/snapd/snap-exec
  → /usr/lib/snapd/snap-confine
  → [namespace setup, apparmor transition, seccomp load]
  → /snap/vlc/3221/usr/bin/vlc (confined process)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">snap-confine</code> does the following in sequence:</p>

<ol>
  <li><strong>Mount namespace creation</strong>: Creates a new mount namespace (<code class="language-plaintext highlighter-rouge">CLONE_NEWNS</code>). Inside this namespace, the snap sees:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/snap/vlc/3221/</code> mounted at <code class="language-plaintext highlighter-rouge">/</code> (the app’s root filesystem via bind mounts)</li>
      <li>Base snap content merged in (provides glibc, GTK, etc.)</li>
      <li>Real <code class="language-plaintext highlighter-rouge">/home</code>, <code class="language-plaintext highlighter-rouge">/tmp</code>, specific <code class="language-plaintext highlighter-rouge">/run/user</code> paths (as permitted by interfaces)</li>
      <li>Tmpfs mounts for <code class="language-plaintext highlighter-rouge">/tmp</code>, <code class="language-plaintext highlighter-rouge">/dev/shm</code> (isolated per snap)</li>
    </ul>
  </li>
  <li>
    <p><strong>AppArmor profile transition</strong>: Calls <code class="language-plaintext highlighter-rouge">aa_change_profile()</code> to transition the process into the snap’s AppArmor profile. After this point, all actions are subject to AppArmor MAC.</p>
  </li>
  <li>
    <p><strong>seccomp filter load</strong>: Loads a seccomp BPF filter that restricts system calls. The filter is generated from the snap’s declarations.</p>
  </li>
  <li><strong>UID/GID</strong>: The snap process runs as the invoking user (no privilege escalation). The <code class="language-plaintext highlighter-rouge">setuid root</code> on <code class="language-plaintext highlighter-rouge">snap-confine</code> itself is only used to set up the namespace — the app process drops root before exec.</li>
</ol>

<hr />

<h2 id="apparmor-the-mac-enforcement-layer">AppArmor: The MAC Enforcement Layer</h2>

<p>AppArmor (Mandatory Access Control) enforces policy regardless of what the process tries to do with POSIX capabilities. A process confined by AppArmor cannot access resources not listed in its profile, even if the user running it has permission.</p>

<p>Snap generates AppArmor profiles automatically from the snap’s <code class="language-plaintext highlighter-rouge">meta/snap.yaml</code>. The profile is generated by snapd and loaded into the kernel when the snap is installed.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># See the generated AppArmor profile for a snap:</span>
<span class="nb">cat</span> /var/lib/snapd/apparmor/profiles/snap.vlc.vlc

<span class="c"># The profile controls:</span>
<span class="c"># - File access (read/write/execute paths)</span>
<span class="c"># - Network access</span>
<span class="c"># - D-Bus access (what services it can call, what methods)</span>
<span class="c"># - Signal sending</span>
<span class="c"># - Ptrace</span>
<span class="c"># - Mount/unmount operations</span>
<span class="c"># - Capability acquisition</span>
</code></pre></div></div>

<p>A snippet of what a typical snap AppArmor profile looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Allow reading snap content
/snap/vlc/** r,
/snap/vlc/**/**.so* mr,

# Home directory (controlled by home interface)
@{HOME}/ r,
@{HOME}/** rwk,  # Only if home interface connected

# Deny dangerous capabilities
deny /proc/sys/kernel/sysrq w,
deny /sys/kernel/security/** rwklx,
deny @{PROC}/@{pid}/mem rwklx,
</code></pre></div></div>

<p>The granularity is real. If VLC has the <code class="language-plaintext highlighter-rouge">home</code> interface connected, it can read/write your home directory. If you disconnect it via <code class="language-plaintext highlighter-rouge">snap disconnect vlc:home</code>, the AppArmor profile is regenerated and VLC can no longer access <code class="language-plaintext highlighter-rouge">~</code> even if it tries.</p>

<h3 id="interface-driven-profiles">Interface-Driven Profiles</h3>

<p>The key to Snap’s security model is that <strong>interfaces</strong> map to <strong>AppArmor policy fragments</strong>. Each interface has a policy file in <code class="language-plaintext highlighter-rouge">/usr/share/snappy/interfaces/</code>. When you connect an interface, snapd regenerates the AppArmor profile to include the corresponding policy fragment and reloads it.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># See all interfaces and their connection state for a snap:</span>
snap connections vlc

<span class="c"># Interface           Plug                Slot              Notes</span>
<span class="c"># audio-playback      vlc:audio-playback  :audio-playback   -</span>
<span class="c"># home                vlc:home            :home             -</span>
<span class="c"># network             vlc:network         :network          -</span>
<span class="c"># removable-media     vlc:removable-media -                 -   # NOT connected</span>

<span class="c"># Connect removable-media to allow /media/* access:</span>
snap connect vlc:removable-media

<span class="c"># Disconnect home to prevent home directory access:</span>
snap disconnect vlc:home
</code></pre></div></div>

<p>The interface system is the most technically sophisticated part of Snap. There are ~100 interfaces covering everything from <code class="language-plaintext highlighter-rouge">audio-playback</code> to <code class="language-plaintext highlighter-rouge">bluetooth-control</code> to <code class="language-plaintext highlighter-rouge">docker</code> to <code class="language-plaintext highlighter-rouge">ssh-keys</code>.</p>

<hr />

<h2 id="seccomp-syscall-level-filtering">seccomp: Syscall-Level Filtering</h2>

<p>AppArmor controls access to resources. seccomp (secure computing mode) controls which <strong>system calls</strong> the process can make. These are two separate but complementary confinement mechanisms.</p>

<p>Snap uses seccomp in <strong>filter mode</strong> (BPF program that inspects syscall numbers and arguments). The seccomp profile is generated by <code class="language-plaintext highlighter-rouge">snapd-seccomp</code> from the snap’s declarations.</p>

<p>A typical snap seccomp profile allows common syscalls while blocking dangerous ones:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Allowed (representative subset):
read write open close stat fstat lstat poll
select mmap mprotect munmap brk
socket connect accept bind listen
execve exit_group

# Blocked (not listed = SIGSYS):
# kexec_load       (load new kernel)
# create_module    (load kernel module)
# init_module      (insert kernel module)
# perf_event_open  (performance monitoring, can be used for side channels)
# ptrace           (unless debugging interface connected)
# bpf              (eBPF programs)
</code></pre></div></div>

<p>seccomp filtering happens at the kernel level. If a confined process tries to call a blocked syscall, it receives <code class="language-plaintext highlighter-rouge">SIGSYS</code> (bad system call) immediately — the syscall is never executed.</p>

<p>This is meaningfully different from AppArmor. AppArmor can restrict what files you can open, but it can’t stop a process from making a particular syscall. seccomp stops syscalls before they reach the kernel. Together, they provide defense-in-depth: AppArmor denies access to resources, seccomp denies the ability to make certain system calls at all.</p>

<hr />

<h2 id="the-update-system-channels-tracks-and-delta-downloads">The Update System: Channels, Tracks, and Delta Downloads</h2>

<p>Snap’s update system is more sophisticated than most users realize.</p>

<h3 id="channels">Channels</h3>

<p>Every snap in the Snap Store exists on a <strong>channel</strong> with a risk level:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>stable         # What most users want
candidate      # Almost ready, needs testing
beta           # Active development, may break
edge           # Latest commit, CI builds
</code></pre></div></div>

<p>You can install a specific channel:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>snap <span class="nb">install </span>vlc <span class="nt">--channel</span><span class="o">=</span>beta
snap switch vlc <span class="nt">--channel</span><span class="o">=</span>stable  <span class="c"># Change channel after install</span>
snap <span class="nb">install </span>code <span class="nt">--channel</span><span class="o">=</span>latest/stable  <span class="c"># Explicit track/risk</span>
</code></pre></div></div>

<p><strong>Tracks</strong> are for major version branching:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>snap <span class="nb">install </span>chromium <span class="nt">--channel</span><span class="o">=</span>stable    <span class="c"># "latest" track implicitly</span>
snap <span class="nb">install </span>chromium <span class="nt">--channel</span><span class="o">=</span>107/stable <span class="c"># Pin to Chromium 107</span>
</code></pre></div></div>

<p>This matters for security: a CVE affecting Chromium 110 doesn’t affect snaps pinned to older tracks. This is the <code class="language-plaintext highlighter-rouge">snap hold</code> equivalent for staying on a specific major version.</p>

<h3 id="delta-updates">Delta Updates</h3>

<p>When snapd refreshes a snap, it requests delta packages from the Snap Store:</p>

<ol>
  <li>snapd tells the store its current revision of VLC</li>
  <li>The store generates or retrieves a binary delta (xdelta3-based) from old→new squashfs</li>
  <li>snapd downloads the delta (might be 20MB instead of 200MB)</li>
  <li>snapd applies the delta to produce the new squashfs</li>
  <li>The new squashfs is verified against the store’s assertions</li>
  <li><code class="language-plaintext highlighter-rouge">/snap/vlc/current</code> symlink is updated atomically</li>
</ol>

<p>The delta mechanism can reduce update sizes by 80-90% for minor updates. Major updates (new bundled libraries) still download most of the image.</p>

<h3 id="assertions-the-trust-chain">Assertions: The Trust Chain</h3>

<p>Every snap in the Snap Store is cryptographically signed. snapd verifies the full chain before mounting a snap:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Assertions are stored here:</span>
<span class="nb">ls</span> /var/lib/snapd/assertions/

<span class="c"># The chain:</span>
<span class="c"># Snap Store Root Key (Canonical-controlled)</span>
<span class="c">#   → Account Key (developer's key)</span>
<span class="c">#     → snap-declaration (metadata: interfaces used, slots, plugs)</span>
<span class="c">#       → snap-revision (specific version: sha3-384 hash of squashfs)</span>
</code></pre></div></div>

<p>When snapd downloads a snap, it verifies the <code class="language-plaintext highlighter-rouge">snap-revision</code> assertion’s hash matches the downloaded squashfs. This prevents MITM attacks — you can’t intercept and replace a snap in transit because you can’t forge the assertion signatures.</p>

<p>This is a security strength Snap has over AppImage. AppImages have no mandatory signing — assertions in AppImageKit are optional and rarely used. Snap downloads are always verified against Canonical’s key infrastructure.</p>

<hr />

<h2 id="startup-latency-the-full-picture">Startup Latency: The Full Picture</h2>

<p>The notorious Snap startup latency deserves a proper explanation.</p>

<p>On a cold start (first launch, no caches warmed):</p>

<ol>
  <li>
    <p><strong>squashfs mount</strong>: The <code class="language-plaintext highlighter-rouge">/snap/vlc/3221/</code> mount is persistent — it’s mounted at boot. So this is not actually a cold-start cost anymore in recent snapd versions (older versions unmounted between uses).</p>
  </li>
  <li>
    <p><strong>AppArmor profile load</strong>: A cache of compiled AppArmor profiles is maintained. If the cache is valid, this is fast (~1ms). If the profile needs recompilation (after an update), this can take 100-500ms.</p>
  </li>
  <li>
    <p><strong>snap-confine overhead</strong>: Creating the mount namespace, loading seccomp filter, executing the snap-exec chain: ~50-200ms depending on complexity.</p>
  </li>
  <li>
    <p><strong>squashfs block decompression</strong>: The first read of each block incurs decompression overhead. For an app like Firefox that reads thousands of files on startup, this adds up.</p>
  </li>
</ol>

<p>Benchmark example on typical SSD hardware:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Firefox snap cold start</span>
<span class="nb">time </span>snap run firefox
<span class="c"># real: 8.3s</span>

<span class="c"># Firefox .deb (apt-installed) cold start</span>
<span class="nb">time </span>firefox
<span class="c"># real: 2.1s</span>

<span class="c"># Firefox snap warm start (second launch)</span>
<span class="nb">time </span>snap run firefox
<span class="c"># real: 3.1s</span>
</code></pre></div></div>

<p>The 6-second cold-start penalty is almost entirely squashfs decompression. The warm-start gap (1 second) is snap-confine overhead.</p>

<p>On NVMe SSDs with fast decompression (Ryzen processors have hardware-assisted lz4), the gap narrows. On spinning disks, it widens. This is a hardware problem masquerading as a software problem.</p>

<hr />

<h2 id="the-chromium-situation-what-actually-happened">The Chromium Situation: What Actually Happened</h2>

<p>This deserves documentation because it’s still misunderstood.</p>

<p>In 2019, Canonical deprecated the <code class="language-plaintext highlighter-rouge">chromium-browser</code> Debian package. They stopped publishing security updates to the .deb. Instead, they maintained the Chromium snap and made <code class="language-plaintext highlighter-rouge">apt install chromium-browser</code> on Ubuntu install the snap instead.</p>

<p>The mechanism: Ubuntu patches <code class="language-plaintext highlighter-rouge">apt</code> to recognize the <code class="language-plaintext highlighter-rouge">chromium-browser</code> package name as a snap trigger. When you <code class="language-plaintext highlighter-rouge">apt install chromium-browser</code>, Ubuntu’s package manager calls <code class="language-plaintext highlighter-rouge">snap install chromium</code> instead of downloading a .deb.</p>

<p>This was documented in Ubuntu’s release notes. It was also, let’s be honest, surprising and somewhat jarring to experienced Linux users who expected <code class="language-plaintext highlighter-rouge">apt install</code> to install a .deb.</p>

<p>The reasoning from Canonical’s side is valid: maintaining Chromium packages for each Ubuntu LTS is genuinely difficult because Chromium’s build system evolves faster than LTS timelines. The snap is better maintained. The security updates arrive faster.</p>

<p>The frustration from users is also valid: intercepting <code class="language-plaintext highlighter-rouge">apt install</code> and doing something different from what was asked breaks user expectations and violates the principle of least surprise.</p>

<p>Both things are true.</p>

<h3 id="the-20252026-update">The 2025–2026 Update</h3>

<p>By 2025–2026, the sharpest edges of this situation have been sanded down. Mozilla now publishes a first-party Firefox .deb via their own PPA, which many users prefer over the snap. Google Chrome has always been a .deb from Google directly. Ungoogled Chromium has active community PPA maintenance. The dominant use case for the Chromium snap — “I just want Chromium on Ubuntu and I don’t want to manage a PPA” — still applies, but the claim that users <em>have</em> to use the snap for a maintained browser is no longer true.</p>

<p>Snap’s center of gravity has shifted clearly toward server/IoT: Ubuntu Core devices, Microk8s, Juju charms, and background services on Ubuntu Server. That’s where Canonical’s investment is concentrated, and it’s where Snap’s architectural decisions make the most sense. The desktop snap wars have largely cooled.</p>

<hr />

<h2 id="what-snapd-stores-on-your-system">What snapd Stores on Your System</h2>

<p>A complete accounting of the Snap footprint:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Squashfs mount points (one per revision of each snap):</span>
mount | <span class="nb">grep </span>squashfs | <span class="nb">wc</span> <span class="nt">-l</span>
<span class="c"># On a typical Ubuntu desktop: 10-20 loop devices</span>

<span class="c"># Actual disk usage:</span>
<span class="nb">du</span> <span class="nt">-sh</span> /var/lib/snapd/snaps/    <span class="c"># squashfs files</span>
<span class="nb">du</span> <span class="nt">-sh</span> /var/snap/               <span class="c"># App data (user data here is yours, don't delete)</span>
<span class="nb">du</span> <span class="nt">-sh</span> /snap/                   <span class="c"># Mount points (no real disk usage, just mounts)</span>

<span class="c"># AppArmor profile cache:</span>
<span class="nb">du</span> <span class="nt">-sh</span> /var/cache/apparmor/

<span class="c"># On a moderate Ubuntu desktop install:</span>
<span class="c"># /var/lib/snapd/snaps/ ≈ 1-4 GB depending on what's installed</span>
<span class="c"># /var/snap/ ≈ 200MB-2GB (mostly application data you want to keep)</span>
</code></pre></div></div>

<p>The multiple revisions issue: by default, snapd keeps the current and one previous revision of each snap. That means two copies of every squashfs on disk.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Show disk usage per snap including all revisions:</span>
snap list <span class="nt">--all</span>

<span class="c"># Remove all old revisions:</span>
snap list <span class="nt">--all</span> | <span class="nb">awk</span> <span class="s1">'/disabled/{print $1, $3}'</span> | <span class="k">while </span><span class="nb">read </span>name rev<span class="p">;</span> <span class="k">do
  </span>snap remove <span class="s2">"</span><span class="nv">$name</span><span class="s2">"</span> <span class="nt">--revision</span><span class="o">=</span><span class="s2">"</span><span class="nv">$rev</span><span class="s2">"</span>
<span class="k">done</span>
</code></pre></div></div>

<hr />

<h2 id="removing-snapd-and-living-with-the-consequences">Removing snapd (And Living With the Consequences)</h2>

<p>If you’ve decided snapd is not for you on a non-Ubuntu distro, or you want it gone on Ubuntu:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Remove all snaps first (in reverse dependency order):</span>
<span class="c"># snap-store, gnome-3-38, gtk-common-themes, firefox, ...</span>
snap list  <span class="c"># Check what's installed</span>

<span class="k">for </span>snap <span class="k">in</span> <span class="si">$(</span>snap list | <span class="nb">awk</span> <span class="s1">'NR&gt;1{print $1}'</span><span class="si">)</span><span class="p">;</span> <span class="k">do
  </span><span class="nb">sudo </span>snap remove <span class="nt">--purge</span> <span class="s2">"</span><span class="nv">$snap</span><span class="s2">"</span>
<span class="k">done</span>

<span class="c"># Remove snapd itself:</span>
<span class="nb">sudo </span>apt purge snapd
<span class="nb">sudo </span>apt-mark hold snapd  <span class="c"># Prevent reinstallation by apt</span>

<span class="c"># Clean up:</span>
<span class="nb">rm</span> <span class="nt">-rf</span> ~/snap              <span class="c"># User app data (~/snap/vlc/, ~/snap/firefox/, etc.)</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /snap          <span class="c"># Kernel loop-mounted squashfs images (system-wide mount point)</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /var/snap      <span class="c"># Per-snap revision data</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /var/lib/snapd <span class="c"># snapd database, downloaded .snap files, assertions</span>

<span class="c"># Optionally: prevent Ubuntu from reinstalling snap packages via apt</span>
<span class="c"># Create a preference file:</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">' | sudo tee /etc/apt/preferences.d/no-snaps.pref
Package: snapd
Pin: release a=*
Pin-Priority: -10
</span><span class="no">EOF
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">apt-mark hold</code> is important — some Ubuntu packages will try to pull snapd back in as a dependency (notably <code class="language-plaintext highlighter-rouge">ubuntu-desktop</code>). The <code class="language-plaintext highlighter-rouge">no-snaps.pref</code> file is the nuclear option that prevents any snap from being installed via apt.</p>

<p>On an Ubuntu machine where snap-delivered apps are part of the default setup (Firefox, Thunderbird), removing snapd means finding .deb alternatives or Flatpak equivalents. The Firefox PPA maintains a .deb:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>add-apt-repository ppa:mozillateam/ppa
<span class="nb">echo</span> <span class="s1">'Package: *
Pin: release o=LP-PPA-mozillateam
Pin-Priority: 1001'</span> | <span class="nb">sudo tee</span> /etc/apt/preferences.d/mozilla-firefox
<span class="nb">sudo </span>apt <span class="nb">install </span>firefox
</code></pre></div></div>

<hr />

<h2 id="the-genuine-strengths">The Genuine Strengths</h2>

<p>I’ve been hard on Snap. Let me be fair about where it excels.</p>

<p><strong>Server and IoT use cases</strong>: On Ubuntu Core, snaps are excellent. The atomic update and rollback mechanism, combined with the confined execution model, makes remote management of headless devices genuinely reliable. When a snap update fails on a Raspberry Pi you can’t physically access, the automatic rollback is not a nice-to-have — it’s the difference between a bricked device and a working one.</p>

<p><strong>The interface system</strong>: The depth and granularity of Snap’s interface system is impressive. The ability to see and control exactly what each app can access — and have that control enforced at the kernel level — is a real security feature. <code class="language-plaintext highlighter-rouge">snap connections</code> is one of the most useful security introspection tools on Linux.</p>

<p><strong>The update security model</strong>: Assertions + signing + delta updates is a well-designed system. The cryptographic chain from Canonical’s root key to individual snap revisions is stronger than most distribution systems.</p>

<hr />

<h2 id="what-to-read-next">What to Read Next</h2>

<ul>
  <li>snapd architecture documentation: <a href="https://github.com/snapcore/snapd">github.com/snapcore/snapd</a></li>
  <li>Interface reference: <a href="https://snapcraft.io/docs/interfaces">snapcraft.io/docs/interfaces</a></li>
  <li>AppArmor reference: <a href="https://ubuntu.com/server/docs/security-apparmor">ubuntu.com/server/docs/security-apparmor</a></li>
  <li>snap-confine source: <a href="https://github.com/snapcore/snapd/tree/master/cmd/snap-confine">github.com/snapcore/snapd/tree/master/cmd/snap-confine</a></li>
</ul>

<hr />

<p><em>Next: <a href="https://denner.co/blog">Part 4 — Flatpak: Portal Architecture and the Bubblewrap Sandbox</a></em></p>

<p><em>Andrew Denner — denner.co — @adenner</em></p>]]></content><author><name>Andrew Denner</name></author><summary type="html"><![CDATA[Snap Deep Dive: Anatomy of Canonical’s Distribution System The Full Picture — snapd, AppArmor, Seccomp, and the Machine It Built]]></summary></entry><entry><title type="html">AppImage Deep Dive: One File, No Illusions (part 2)</title><link href="http://0.0.0.0:4000/2026/03/21/AppImg-2.html" rel="alternate" type="text/html" title="AppImage Deep Dive: One File, No Illusions (part 2)" /><published>2026-03-21T15:35:14+00:00</published><updated>2026-03-21T15:35:14+00:00</updated><id>http://0.0.0.0:4000/2026/03/21/AppImg-2</id><content type="html" xml:base="http://0.0.0.0:4000/2026/03/21/AppImg-2.html"><![CDATA[<h1 id="appimage-deep-dive-one-file-no-illusions">AppImage Deep Dive: One File, No Illusions</h1>
<h2 id="the-complete-technical-picture">The Complete Technical Picture</h2>

<p><em>Part 2 of the <a href="https://denner.co/blog">Linux Universal Packages series</a></em></p>

<hr />

<p><em>Part 1 of this series covers the overview, decision matrix, and talk highlights. This part goes further than any conference talk has time for. We’re going into the binary format, FUSE internals, the update spec, and the libfuse2/libfuse3 incident that broke half the AppImages on Ubuntu 22.04. Grab coffee.</em></p>

<hr />

<h2 id="what-an-appimage-actually-is-at-the-byte-level">What an AppImage Actually Is at the Byte Level</h2>

<p>The executive summary: an AppImage is a concatenated ELF binary and squashfs filesystem. That’s it. Understanding those two pieces explains almost everything about how they work and why they behave the way they do.</p>

<h3 id="the-runtime-binary">The Runtime Binary</h3>

<p>Every AppImage starts with a <strong>runtime</strong> — a small C binary typically called <code class="language-plaintext highlighter-rouge">runtime.c</code> from the AppImageKit project. When your kernel executes an AppImage, it executes this runtime just like any other ELF binary. The runtime’s job is to:</p>

<ol>
  <li>Find its own file path (<code class="language-plaintext highlighter-rouge">/proc/self/exe</code>)</li>
  <li>Mount the squashfs payload that follows it in the same file</li>
  <li>Find and execute the AppRun script inside that squashfs</li>
  <li>Wait for AppRun to exit, then unmount and clean up</li>
</ol>

<p>The squashfs payload starts at a specific byte offset immediately after the runtime binary. The runtime finds this offset because it knows the size of itself — it’s compiled with its own size embedded as a constant. In AppImageKit, this is the <code class="language-plaintext highlighter-rouge">sfs_offset</code> variable. When the runtime binary is built, the build system appends this size at a well-known location so the runtime can find where the filesystem begins.</p>

<p>You can inspect this yourself:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Find the squashfs magic bytes — 0x73717368 ("sqsh") or 0x68737173 ("hsqs")</span>
xxd MyApp.AppImage | <span class="nb">grep</span> <span class="nt">-m1</span> <span class="s2">"7371 7368</span><span class="se">\|</span><span class="s2">6873 7173"</span>
</code></pre></div></div>

<p>The offset you find is where the squashfs filesystem starts. Everything before it is the runtime ELF.</p>

<h3 id="elf-header-inspection">ELF Header Inspection</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Verify the ELF header (first 4 bytes: 7f 45 4c 46 = ELF magic)</span>
xxd <span class="nt">-l</span> 16 MyApp.AppImage

<span class="c"># Check the embedded section that tells file(1) this is an AppImage</span>
file MyApp.AppImage
<span class="c"># Output: ELF 64-bit LSB executable ... (AppImage)</span>

<span class="c"># The AppImage type is embedded in ELF section .note.ABI-tag</span>
<span class="c"># Type 1 = AppImage format 1 (libarchive-based, older)</span>
<span class="c"># Type 2 = AppImage format 2 (squashfs, current)</span>
readelf <span class="nt">-n</span> MyApp.AppImage | <span class="nb">grep</span> <span class="nt">-A2</span> AppImage
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">.note.ABI-tag</code> ELF section contains a note of type <code class="language-plaintext highlighter-rouge">AI\x02\x00</code> (AppImage format 2). This is how tools like AppImageLauncher identify AppImages without relying on file extension — though the <code class="language-plaintext highlighter-rouge">.AppImage</code> extension is conventional, not required.</p>

<hr />

<h2 id="the-squashfs-filesystem">The squashfs Filesystem</h2>

<p>squashfs is a read-only compressed filesystem that’s been part of the Linux kernel since 2.6.29. It’s the same filesystem format used in live CDs, embedded systems (OpenWRT, among many others), and Snap/Flatpak runtimes. AppImage is not special in using squashfs — it’s just the right tool for a read-only bundled filesystem.</p>

<p>Key properties:</p>
<ul>
  <li><strong>Read-only</strong>: squashfs is always compressed and always read-only. No exceptions.</li>
  <li><strong>Block-compressed</strong>: data is compressed in blocks (typically 128KB). Random access to compressed data requires decompressing the block containing your data. This is why squashfs-based formats have slightly higher latency on first access.</li>
  <li><strong>Supported compression</strong>: gzip, lzma, lzo, xz, zstd. AppImages typically use gzip or xz. xz gives better compression at the cost of slower decompression.</li>
  <li><strong>Directory structure</strong>: standard POSIX filesystem with inodes, directory entries, symlinks, device nodes.</li>
</ul>

<p>You can inspect the squashfs directly:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Extract squashfs at the known offset (replace OFFSET with actual value)</span>
<span class="nb">dd </span><span class="k">if</span><span class="o">=</span>MyApp.AppImage <span class="nv">bs</span><span class="o">=</span>1 <span class="nv">skip</span><span class="o">=</span>OFFSET <span class="nv">of</span><span class="o">=</span>/tmp/myapp.squashfs

<span class="c"># Or let unsquashfs figure it out</span>
unsquashfs <span class="nt">-l</span> MyApp.AppImage | <span class="nb">head</span> <span class="nt">-40</span>
<span class="c"># Shows the directory tree inside the AppImage</span>
</code></pre></div></div>

<h3 id="the-appdir-layout">The AppDir Layout</h3>

<p>Inside the squashfs, AppImages follow the <strong>AppDir spec</strong>. This is a semi-formal specification that defines what an application directory should contain to be portable.</p>

<p>A minimal AppDir:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MyApp.AppDir/
├── AppRun              # Executable or symlink; entry point
├── myapp.desktop       # Standard .desktop file
├── myapp.png           # Icon (various sizes ideally)
├── usr/
│   ├── bin/
│   │   └── myapp       # The actual binary
│   ├── lib/
│   │   ├── libssl.so.1.1    # Bundled libraries
│   │   └── libcrypto.so.1.1
│   └── share/
│       └── myapp/      # Data files, assets
└── .DirIcon            # Optional: icon for file managers
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">AppRun</code> file is the entry point. For simple apps it might just be:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">exec</span> <span class="s2">"</span><span class="nv">$APPDIR</span><span class="s2">/usr/bin/myapp"</span> <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
</code></pre></div></div>

<p>But more complex AppRuns set up <code class="language-plaintext highlighter-rouge">LD_LIBRARY_PATH</code> to point to bundled libraries, set <code class="language-plaintext highlighter-rouge">XDG_DATA_DIRS</code> to include bundled data, and handle architecture-specific paths.</p>

<p>The <code class="language-plaintext highlighter-rouge">.desktop</code> file must follow the <a href="https://specifications.freedesktop.org/desktop-entry-spec/">Desktop Entry Specification</a>. AppImageLauncher uses it to create system integration — the <code class="language-plaintext highlighter-rouge">Name</code>, <code class="language-plaintext highlighter-rouge">Exec</code>, <code class="language-plaintext highlighter-rouge">Icon</code>, and <code class="language-plaintext highlighter-rouge">Categories</code> fields become the application menu entry.</p>

<hr />

<h2 id="fuse-mounting-what-actually-happens-at-runtime">FUSE Mounting: What Actually Happens at Runtime</h2>

<p>When you execute an AppImage, the runtime uses FUSE (Filesystem in Userspace) to mount the squashfs payload. Here’s the sequence:</p>

<ol>
  <li>Runtime binary starts execution</li>
  <li>It calls <code class="language-plaintext highlighter-rouge">fuse_main()</code> (or equivalent) with the squashfs offset as the source</li>
  <li>The kernel FUSE driver creates a virtual mountpoint at <code class="language-plaintext highlighter-rouge">/tmp/.mount_&lt;appname&gt;&lt;random&gt;/</code></li>
  <li>The squashfs is accessible at that path</li>
  <li>Runtime <code class="language-plaintext highlighter-rouge">exec()</code>s <code class="language-plaintext highlighter-rouge">$MOUNTPOINT/AppRun</code></li>
  <li>AppRun runs (the application executes)</li>
  <li>When AppRun exits, FUSE unmounts and cleans up</li>
</ol>

<p>You can observe this while an AppImage is running:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># In one terminal, run an AppImage</span>
./MyApp.AppImage &amp;

<span class="c"># In another terminal:</span>
mount | <span class="nb">grep </span>AppImage
<span class="c"># Shows: /tmp/.mount_MyApp&lt;hash&gt; type fuse.squashfuse ...</span>

<span class="nb">ls</span> /tmp/.mount_MyApp<span class="k">*</span>/
<span class="c"># Shows the AppDir contents live</span>
</code></pre></div></div>

<p>This FUSE mount is why AppImages don’t need root — all mounting happens in userspace. The kernel FUSE driver brokers access between the runtime’s FUSE filesystem server and any processes trying to read files through it.</p>

<h3 id="why-appimage-doesnt-need-root-and-what-that-costs">Why AppImage Doesn’t Need Root (and What That Costs)</h3>

<p>Traditional <code class="language-plaintext highlighter-rouge">mount</code> requires <code class="language-plaintext highlighter-rouge">CAP_SYS_ADMIN</code>. FUSE gets around this because:</p>
<ol>
  <li>The FUSE kernel module exposes <code class="language-plaintext highlighter-rouge">/dev/fuse</code></li>
  <li>Any user can open <code class="language-plaintext highlighter-rouge">/dev/fuse</code> and register a FUSE filesystem server</li>
  <li>The kernel routes filesystem operations to that server via the <code class="language-plaintext highlighter-rouge">/dev/fuse</code> file descriptor</li>
</ol>

<p>The userspace FUSE server (squashfuse) reads compressed blocks from the AppImage file and hands them to the kernel. Every read operation goes: kernel → FUSE driver → squashfuse userspace process → kernel buffer → calling process.</p>

<p>This is why AppImage I/O has slightly higher overhead than directly reading a filesystem — there’s an extra userspace hop for every file access. In practice this is negligible for most apps.</p>

<hr />

<h2 id="the-libfuse2libfuse3-incident">The libfuse2/libfuse3 Incident</h2>

<p>This is the section of AppImage history that’s still causing production headaches in 2026.</p>

<h3 id="background">Background</h3>

<p>AppImageKit’s FUSE runtime historically used <strong>libfuse2</strong> (the 2.x API). This was the dominant FUSE library for years. Around 2021-2022, libfuse3 became the standard — it’s not backward-compatible at the API level, though the kernel protocol is the same.</p>

<p>Ubuntu 22.04 LTS (Jammy) shipped <strong>without libfuse2 installed by default</strong>. Only libfuse3 was included. The AppImage runtime links against libfuse2 at build time, so:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># On Ubuntu 22.04 with a pre-2022 AppImage:</span>
./OldApp.AppImage
<span class="c"># Error: cannot open shared object file: libfuse.so.2: No such file or directory</span>
<span class="c"># OR just: Segmentation fault</span>
</code></pre></div></div>

<p>The fix:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>libfuse2
</code></pre></div></div>

<p>That’s it. One package install. But it breaks the “AppImages just work without installing anything” story, and it surprised a lot of users who didn’t know what FUSE was.</p>

<h3 id="the-longer-term-fix">The Longer-Term Fix</h3>

<p>By 2025-2026, most actively maintained AppImages have migrated to the FUSE 3 runtime or added a fuse3 fallback. The cases still requiring <code class="language-plaintext highlighter-rouge">libfuse2</code> tend to be older or unmaintained AppImages, and some legacy scientific computing tools that haven’t been rebuilt since the transition. If you’re building new AppImages, use current AppImageKit tooling — it handles FUSE 3 correctly. If you encounter an older AppImage that still needs libfuse2, the install command is just <code class="language-plaintext highlighter-rouge">sudo apt install libfuse2</code>.</p>

<p>Some AppImages also include a <code class="language-plaintext highlighter-rouge">--appimage-extract-and-run</code> fallback that extracts the squashfs to a temporary directory and runs without FUSE at all (slower, but works on systems without any FUSE support).</p>

<p>You can use this workaround for any AppImage:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./MyApp.AppImage <span class="nt">--appimage-extract-and-run</span>
<span class="c"># Or set the environment variable permanently:</span>
<span class="nb">export </span><span class="nv">APPIMAGE_EXTRACT_AND_RUN</span><span class="o">=</span>1
</code></pre></div></div>

<hr />

<h2 id="appimage-update-protocol-zsync">AppImage Update Protocol: zsync</h2>

<p>The AppImage spec includes an optional update mechanism based on <strong>zsync</strong> — a partial file download algorithm that applies binary diffs over HTTP. This is how AppImages can self-update without downloading the entire file each time.</p>

<h3 id="how-zsync-works">How zsync Works</h3>

<p>zsync is essentially rsync-over-HTTP. The basic idea:</p>
<ol>
  <li>The new version of the file is available at a URL</li>
  <li>A <code class="language-plaintext highlighter-rouge">.zsync</code> file at a known URL describes the new file as a series of blocks with checksums</li>
  <li>The client reads its existing file and computes which blocks are already correct</li>
  <li>Only the changed blocks are downloaded</li>
  <li>The client reassembles the new file from old blocks + downloaded new blocks</li>
</ol>

<p>For large AppImages (200MB+), this can reduce update downloads to a few megabytes for minor version bumps that only changed code, not bundled libraries.</p>

<h3 id="the-update-url-embedded-in-the-appimage">The Update URL Embedded in the AppImage</h3>

<p>AppImages that support updating embed an <code class="language-plaintext highlighter-rouge">update_information</code> field in their <code class="language-plaintext highlighter-rouge">.desktop</code> file or in the ELF’s custom section. It looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh-releases-zsync|owner|repo|latest|MyApp-*.AppImage.zsync
</code></pre></div></div>

<p>This tells AppImageUpdate:</p>
<ul>
  <li>The update mechanism (<code class="language-plaintext highlighter-rouge">gh-releases-zsync</code> = GitHub releases with zsync)</li>
  <li>Where to find the latest release</li>
  <li>Which file pattern matches the update</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use AppImageUpdate to check for updates</span>
appimageupdate <span class="nt">--check</span> MyApp.AppImage
<span class="c"># Output: [INFO] update available, new version: 1.2.3</span>

<span class="c"># Apply the update (downloads diff, creates new version)</span>
appimageupdate MyApp.AppImage
</code></pre></div></div>

<p>Most AppImages don’t include this. The AppImageUpdate tool is optional and rarely used. The official AppImageHub catalog tracks update info for listed apps. AppImageLauncher integrates with this to show update notifications in the tray — when you double-click an AppImage in AppImageLauncher, it registers it and periodically polls for updates.</p>

<hr />

<h2 id="building-appimages-appimagetool">Building AppImages: appimagetool</h2>

<p>Understanding how AppImages are built gives you the full picture of what you’re running.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Minimal AppImage build process:</span>

<span class="c"># 1. Create your AppDir structure</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> MyApp.AppDir/usr/bin
<span class="nb">cp </span>myapp MyApp.AppDir/usr/bin/
<span class="nb">cat</span> <span class="o">&gt;</span> MyApp.AppDir/myapp.desktop <span class="o">&lt;&lt;</span> <span class="no">EOF</span><span class="sh">
[Desktop Entry]
Name=MyApp
Exec=myapp
Icon=myapp
Type=Application
Categories=Utility;
</span><span class="no">EOF
</span><span class="nb">cp </span>myapp.png MyApp.AppDir/

<span class="c"># 2. Create AppRun</span>
<span class="nb">cat</span> <span class="o">&gt;</span> MyApp.AppDir/AppRun <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
#!/bin/bash
exec "</span><span class="nv">$APPDIR</span><span class="sh">/usr/bin/myapp" "</span><span class="nv">$@</span><span class="sh">"
</span><span class="no">EOF
</span><span class="nb">chmod</span> +x MyApp.AppDir/AppRun

<span class="c"># 3. Bundle dependencies (the painful part)</span>
<span class="c"># linuxdeploy handles this automatically:</span>
linuxdeploy <span class="nt">--appdir</span> MyApp.AppDir <span class="se">\</span>
  <span class="nt">--executable</span> usr/bin/myapp <span class="se">\</span>
  <span class="nt">--desktop-file</span> MyApp.AppDir/myapp.desktop <span class="se">\</span>
  <span class="nt">--icon-file</span> myapp.png <span class="se">\</span>
  <span class="nt">--output</span> appimage

<span class="c"># This produces: MyApp-x86_64.AppImage</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">linuxdeploy</code> traces the app’s dynamic library dependencies (<code class="language-plaintext highlighter-rouge">ldd</code>), copies them into the AppDir, and patches the ELF <code class="language-plaintext highlighter-rouge">rpath</code> so the bundled libraries are found. It also handles Qt and GTK plugin deployment via separate plugins.</p>

<p>This is where the “bundle everything” tradeoff becomes concrete: linuxdeploy might find 40+ shared libraries to bundle, adding 50-100MB to the AppImage before compression.</p>

<hr />

<h2 id="security-the-actual-risk-surface">Security: The Actual Risk Surface</h2>

<p>Let’s be honest about what the AppImage security model is and isn’t.</p>

<h3 id="what-appimage-does-not-provide">What AppImage Does NOT Provide</h3>

<ul>
  <li><strong>No sandbox</strong>: AppImages run with your full user permissions. <code class="language-plaintext highlighter-rouge">~/.ssh</code>, <code class="language-plaintext highlighter-rouge">~/.gnupg</code>, your entire home directory — all accessible.</li>
  <li><strong>No code signing verification</strong>: The ELF + squashfs structure has no built-in signature verification. AppImageKit supports GPG signatures embedded in the image, but checking them is not enforced and most apps don’t implement it.</li>
  <li><strong>No update integrity checking</strong>: zsync downloads use checksums for file integrity, but there’s no signing of the release itself beyond whatever the hosting platform provides (GitHub releases, for example, doesn’t sign artifacts by default).</li>
  <li><strong>No AppArmor/seccomp</strong>: Unlike Snap, there’s no mandatory access control applied to AppImage processes.</li>
</ul>

<h3 id="what-appimage-does-provide">What AppImage DOES Provide</h3>

<ul>
  <li><strong>Isolation by convention</strong>: The app runs from a read-only squashfs. It can’t modify its own code at runtime. This isn’t security — the app can still write anywhere in your home directory — but it does mean the app files themselves aren’t modified.</li>
  <li><strong>System isolation</strong>: The app’s files don’t mix with your system’s files. There’s no risk of an AppImage install breaking system packages.</li>
  <li><strong>Reproducibility</strong>: You know exactly what version you’re running and it will never change unless you download a new file. This is the opposite of Snap’s auto-update, and for certain security contexts it’s actually preferable.</li>
</ul>

<h3 id="the-trust-model">The Trust Model</h3>

<p>When you download an AppImage, you are trusting:</p>
<ol>
  <li>The developer who built it</li>
  <li>The infrastructure that distributed it (GitHub releases page, developer’s website, etc.)</li>
  <li>Your connection to that infrastructure (HTTPS protects against in-transit modification)</li>
</ol>

<p>This is roughly equivalent to downloading and running a binary from a developer’s website — which is what a lot of users did before universal packages existed. It’s not inherently worse than that baseline, but it’s significantly weaker than a distro package or Flatpak where there’s review infrastructure.</p>

<p><strong>Practical guidance:</strong></p>
<ul>
  <li>Download from the project’s official GitHub releases page or official website only</li>
  <li>Verify SHA256/SHA512 checksums when provided (many projects publish them)</li>
  <li>Prefer projects that embed GPG signatures in their AppImages (<code class="language-plaintext highlighter-rouge">appimagetool --sign</code>)</li>
  <li>Check the <a href="https://appimage.github.io">AppImageHub catalog</a> for community-vetted apps</li>
</ul>

<hr />

<h2 id="the---appimage--cli-flags">The <code class="language-plaintext highlighter-rouge">--appimage-*</code> CLI Flags</h2>

<p>Every AppImage built with AppImageKit supports these built-in flags:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Extract the squashfs to a directory (no FUSE needed)</span>
./MyApp.AppImage <span class="nt">--appimage-extract</span>
<span class="c"># Creates: squashfs-root/</span>

<span class="c"># Mount the squashfs and show mount path (for debugging)</span>
./MyApp.AppImage <span class="nt">--appimage-mount</span>
<span class="c"># Prints: /tmp/.mount_MyApp&lt;hash&gt;</span>
<span class="c"># Ctrl+C to unmount</span>

<span class="c"># Show embedded update info</span>
./MyApp.AppImage <span class="nt">--appimage-updateinfo</span>

<span class="c"># Show version of AppImageKit runtime</span>
./MyApp.AppImage <span class="nt">--appimage-version</span>

<span class="c"># Run without FUSE (extract to temp dir and exec)</span>
./MyApp.AppImage <span class="nt">--appimage-extract-and-run</span>

<span class="c"># Show offset where squashfs starts</span>
./MyApp.AppImage <span class="nt">--appimage-offset</span>
</code></pre></div></div>

<p>These are invaluable for debugging why an AppImage isn’t working. If <code class="language-plaintext highlighter-rouge">--appimage-mount</code> fails, you have a FUSE problem. If it succeeds, the issue is in AppRun. If <code class="language-plaintext highlighter-rouge">--appimage-extract</code> succeeds but running the extracted binary fails, you have a missing dependency.</p>

<hr />

<h2 id="appimagelauncher-the-missing-piece">AppImageLauncher: The Missing Piece</h2>

<p>AppImageLauncher is what makes AppImages usable as a daily driver. Without it, you’re managing raw files. With it, you get:</p>

<ol>
  <li>
    <p><strong>Automatic integration</strong>: Double-click an AppImage → dialog asks if you want to integrate it into the system. Integrating copies it to <code class="language-plaintext highlighter-rouge">~/Applications/</code> and creates a <code class="language-plaintext highlighter-rouge">.desktop</code> file in <code class="language-plaintext highlighter-rouge">~/.local/share/applications/</code>.</p>
  </li>
  <li>
    <p><strong>Update checking</strong>: Integrated apps are periodically checked for updates using the embedded update URL. The tray icon notifies you.</p>
  </li>
  <li>
    <p><strong>Removal</strong>: Right-click → Remove from the app menu unintegrates the app and removes the file.</p>
  </li>
  <li>
    <p><strong>binfmt_misc registration</strong>: AppImageLauncher registers an interpreter via <code class="language-plaintext highlighter-rouge">/proc/sys/fs/binfmt_misc</code> so AppImages execute directly without needing <code class="language-plaintext highlighter-rouge">chmod +x</code>. The kernel recognizes the AppImage ELF magic and routes execution through AppImageLauncher.</p>
  </li>
</ol>

<p>The binfmt_misc trick is the clever part:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># AppImageLauncher registers something like:</span>
<span class="c"># :AppImage:M:0:\x7fELF...\x41\x49::/usr/lib/AppImageLauncher/runtime:</span>
<span class="c"># This tells the kernel: if ELF file contains AI magic bytes, exec it via the runtime</span>
<span class="nb">cat</span> /proc/sys/fs/binfmt_misc/appimage-type2
</code></pre></div></div>

<p>This is why AppImages “just work” without <code class="language-plaintext highlighter-rouge">chmod +x</code> when AppImageLauncher is installed — the binfmt_misc interpreter handles execution before the execute permission check.</p>

<hr />

<h2 id="performance-characteristics">Performance Characteristics</h2>

<p>A few numbers worth knowing:</p>

<p><strong>Cold start overhead</strong>: 200-800ms additional latency vs a native binary, depending on squashfs compression and how many files need to be accessed. This is FUSE overhead + decompression. After first launch, the OS page cache stores the decompressed blocks and subsequent launches are faster.</p>

<p><strong>Warm start</strong>: Near-native performance once the squashfs blocks are cached in RAM.</p>

<p><strong>Memory overhead</strong>: The squashfuse process adds ~2-5MB RSS. Negligible.</p>

<p><strong>Disk I/O</strong>: Higher than native binaries for file reads from the AppImage filesystem. For I/O-heavy operations, consider whether the app should be running from an AppImage at all.</p>

<p><strong>Compression ratio</strong>: Typical AppImages achieve 2-4x compression on the squashfs payload. A 200MB installed app might be a 80-100MB AppImage.</p>

<hr />

<h2 id="what-to-read-next">What to Read Next</h2>

<ul>
  <li>AppImageKit source: <a href="https://github.com/AppImage/AppImageKit">github.com/AppImage/AppImageKit</a></li>
  <li>AppDir spec: <a href="https://github.com/AppImage/AppImageSpec">github.com/AppImage/AppImageSpec</a></li>
  <li>linuxdeploy: <a href="https://github.com/linuxdeploy/linuxdeploy">github.com/linuxdeploy/linuxdeploy</a></li>
  <li>AppImageUpdate: <a href="https://github.com/AppImage/AppImageUpdate">github.com/AppImage/AppImageUpdate</a></li>
  <li>squashfs-tools: <code class="language-plaintext highlighter-rouge">apt install squashfs-tools</code> — provides <code class="language-plaintext highlighter-rouge">unsquashfs</code></li>
</ul>

<hr />

<p><em>Next: <a href="https://denner.co/blog">Part 3 — Snap: Anatomy of Canonical’s Distribution System</a></em></p>

<p><em>Andrew Denner — denner.co — @adenner</em></p>]]></content><author><name>Andrew Denner</name></author><summary type="html"><![CDATA[AppImage Deep Dive: One File, No Illusions The Complete Technical Picture]]></summary></entry><entry><title type="html">AppImage, Snap, and Flatpak — The Honest Review</title><link href="http://0.0.0.0:4000/2026/03/20/Snap-1.html" rel="alternate" type="text/html" title="AppImage, Snap, and Flatpak — The Honest Review" /><published>2026-03-20T15:35:14+00:00</published><updated>2026-03-20T15:35:14+00:00</updated><id>http://0.0.0.0:4000/2026/03/20/Snap-1</id><content type="html" xml:base="http://0.0.0.0:4000/2026/03/20/Snap-1.html"><![CDATA[<p>I gave this talk at CIALUG on March 18, 2026. This is the long-form version — the full opinionated breakdown of all three universal Linux package formats, informed by years of using each of them, occasionally loving them, and at least twice nuking them from orbit.</p>

<p>Fair warning: I have opinions. The facts are accurate to the best of my knowledge. The takes are mine.</p>

<hr />

<h2 id="why-these-formats-exist-and-why-its-not-your-distros-fault">Why These Formats Exist (And Why It’s Not Your Distro’s Fault)</h2>

<p>Let me start with the thesis, because I’m tired of people framing this wrong.</p>

<p>Linux does not have an app problem. Linux has a <strong>distribution contract problem</strong>.</p>

<p>Your distro’s package maintainers have a job: make sure every package in the repo works together, doesn’t break your server at 2am, and doesn’t introduce security vulnerabilities by surprise. They optimize for stability, security, and policy. This is a good job and they do it well.</p>

<p>App developers have a different job: ship software that uses the latest version of every library they depend on, tested against their own development environment, on a timeline that doesn’t involve waiting for Debian to unfreeze. They optimize for shipping fast and not answering bug reports about Ubuntu 18.04.</p>

<p>These goals are fundamentally incompatible, and no amount of goodwill makes them compatible. Universal packages are the peace treaty: the app developer bundles everything their app needs into one isolated unit, and your distro doesn’t have to care what’s in it.</p>

<p>The catch — and I want you to internalize this before we go further — is that you’ve <strong>moved the problem, not solved it</strong>. You didn’t eliminate dependency hell. You moved it into a zip file. The zip file now weighs 400 megabytes. Some people think this is progress.</p>

<hr />

<h2 id="appimage-the-cowboy-method">AppImage: The Cowboy Method</h2>

<p>AppImage is the oldest of the three formats and, in some ways, the most honest. It makes no promises about security, integration, or management. It’s a file. You run it. It works.</p>

<p>Technically, an AppImage is an ELF binary that contains a squashfs filesystem. When you execute it, the binary mounts the squashfs, finds the app entrypoint, and runs it. The entire app — binary, libraries, assets — is in that one file. No install required.</p>

<p>The install process is:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod</span> +x MyApp.AppImage <span class="o">&amp;&amp;</span> ./MyApp.AppImage
</code></pre></div></div>

<p>That’s it. That’s the whole thing.</p>

<h3 id="the-good-stuff">The Good Stuff</h3>

<p>The portability story is genuinely compelling. Copy an AppImage to a USB stick and run it on any modern Linux machine without touching the package manager. This is not theoretical — it actually works. I’ve used this for distributing internal tools to machines I don’t control, and it’s the cleanest solution I’ve found.</p>

<p>No root required. No system changes. No daemon watching it. No database entry. Delete the file and the app is completely uninstalled. For people who’ve spent decades navigating “dpkg –purge” and leftover config files in /etc, this is surprisingly satisfying.</p>

<p>AppImages are also great for running multiple versions of the same app simultaneously, which sounds niche until you need it. I maintain three different AppImage versions of a scientific computing tool because different projects have different requirements, and they coexist without conflict.</p>

<h3 id="the-bad-and-ugly">The Bad and Ugly</h3>

<p>The security model is, diplomatically, “trust me bro.” There is no built-in sandbox. An AppImage runs with your full user permissions on your system. You are trusting that whoever compiled and uploaded the AppImage is a reasonable person who didn’t do anything sketchy.</p>

<p>Download from official project pages. Check the appimage.github.io catalog, which maintains a list of vetted AppImages. Never run an AppImage from a link someone posted in a forum. This is not theoretical advice.</p>

<p>No auto-updates by default. The AppImage spec includes an update protocol, but implementing it is optional, and most AppImages don’t. Your AppImage will never tell you there’s a newer version. You have to care about this yourself.</p>

<p>Desktop integration is also manual or nonexistent by default. No .desktop file, no app menu entry, no file associations. The solution is AppImageLauncher, which is excellent — it handles integration automatically when you double-click an AppImage — but you have to install it separately.</p>

<h3 id="the-chmod-x-tax">The chmod +x Tax</h3>

<p>I’ve given this section a name because it deserves one. Every single time I do an AppImage demo, I forget to chmod +x the file before trying to run it. Every time. I’ve been doing this for a decade. I will forget again.</p>

<p>The error is “Permission denied,” which is not the most informative error message for someone who doesn’t know what’s happening. The fix is obvious in retrospect but annoying every time.</p>

<p>This is the single most common AppImage support question. If someone tells you AppImages don’t work, check for chmod +x first.</p>

<h3 id="who-appimage-is-for">Who AppImage Is For</h3>

<p>AppImage is the right choice when portability is the primary constraint. Air-gapped systems, one-off tool deployments, situations where you need to know exactly what version of an app is running and guarantee it doesn’t change without your knowledge. Devs who want to ship to users without caring what distro they’re running.</p>

<p>It is not a replacement for your package manager. Don’t use AppImages for system tools, libraries, or anything where you want security updates to happen automatically.</p>

<hr />

<h2 id="snap-canonicals-walled-garden">Snap: Canonical’s Walled Garden</h2>

<p>Snap is Canonical’s answer to universal packaging, and it carries all the hallmarks of Canonical’s approach to Ubuntu: opinionated, integrated, centralized, and occasionally doing things without asking you first.</p>

<p>I’ve rage-deleted snapd twice. I reinstalled it once. My feelings are complicated.</p>

<h3 id="what-snap-actually-is">What Snap Actually Is</h3>

<p>At a technical level, a snap is a squashfs image wrapped in metadata, distributed through Canonical’s Snap Store (snapcraft.io). When you install a snap, snapd downloads the image, mounts it at <code class="language-plaintext highlighter-rouge">/snap/&lt;name&gt;/&lt;revision&gt;/</code>, and wraps it in an AppArmor confinement profile.</p>

<p>The confinement is real and meaningful. Each snap declares “interfaces” — named permission scopes that correspond to things like home directory access, network access, camera, microphone, USB devices. The snap only gets what it declares, and even then only what you’ve connected.</p>

<p><code class="language-plaintext highlighter-rouge">snap connections &lt;name&gt;</code> shows you exactly what a snap can access. It’s genuinely impressive. I wish more people knew this existed.</p>

<h3 id="the-ubuntu-integration-story">The Ubuntu Integration Story</h3>

<p>On Ubuntu, snapd is pre-installed and always running. <code class="language-plaintext highlighter-rouge">snap install</code> works out of the box. This is either a feature or an annoyance depending on your perspective.</p>

<p>The Chromium situation is worth explaining clearly because it surprises people the first time: on Ubuntu, <code class="language-plaintext highlighter-rouge">apt install chromium-browser</code> does not install a .deb. Ubuntu intercepts the command and installs the Chromium snap. This is documented behavior, not a bug, and Canonical’s reasoning is that the snap version is better maintained and more secure.</p>

<p>Worth noting: if you specifically want a .deb Chromium, alternatives exist — Google Chrome has always distributed a .deb directly, and Ungoogled Chromium is available through community PPAs. But the default <code class="language-plaintext highlighter-rouge">apt install chromium-browser</code> path on Ubuntu leads to snap.</p>

<p>Your reaction to all of this tells me everything about your relationship with Canonical.</p>

<h3 id="the-startup-latency-problem">The Startup Latency Problem</h3>

<p>Every snap app mounts its squashfs image on launch. This is the source of the notorious snap startup latency.</p>

<p>On an SSD with a cold cache, expect a noticeable delay — particularly on first boot or slower drives. NVMe SSDs and improved squashfs caching have narrowed the gap compared to a few years ago, but the first cold start is still measurably slower than an equivalent apt-installed binary. The latency improves after the first launch as the OS caches the squashfs blocks.</p>

<p>This matters more for some apps than others. Firefox snap’s startup time is one of the most-criticized aspects of Ubuntu’s default setup. Server apps where you don’t care about interactive startup time? The latency is irrelevant.</p>

<h3 id="the-auto-update-gotcha">The Auto-Update Gotcha</h3>

<p>Snaps auto-update. By default. Without asking you. While you’re working.</p>

<p>This is usually fine. Sometimes a snap update changes behavior in a way that breaks your workflow. And once — I’m speaking from experience — a snap refreshed during a live presentation, closing the app mid-demo with no warning.</p>

<p>Run this before any presentation, demo, or situation where an unexpected app close would be embarrassing:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>snap refresh <span class="nt">--hold</span><span class="o">=</span>48h
</code></pre></div></div>

<p>Hold a specific snap for longer if needed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>snap refresh <span class="nt">--hold</span><span class="o">=</span>168h vlc
</code></pre></div></div>

<p>This is the most practically useful Snap tip I know. I wish I’d known it earlier.</p>

<h3 id="the-20252026-reality-check">The 2025–2026 Reality Check</h3>

<p>By 2025–2026, the fiercest “Snap wars” era of desktop Linux has largely cooled. The most contentious flashpoints — Firefox and Chromium being snap-only on Ubuntu — have been resolved by well-maintained .deb alternatives: Mozilla publishes a Firefox .deb via their own PPA, Google Chrome has always been a .deb, and Ungoogled Chromium is available through community PPAs. Many experienced Ubuntu users simply pin the .deb versions over the snap equivalents for desktop apps where startup time matters.</p>

<p>Snap’s center of gravity has shifted more clearly toward what it was always best at: Ubuntu server and Ubuntu Core IoT. The Snap Store is the right distribution channel for daemons, services, and appliance software running on Canonical’s platforms. The desktop wars have cooled — which is good for everyone.</p>

<h3 id="who-snap-is-for">Who Snap Is For</h3>

<p>Snap is at its best for server-side daemons and background services on Ubuntu, browsers (the security benefits are real even if the startup is slow), and any situation where you want automatic security patching and don’t care much about the startup time.</p>

<p>Snap is at its worst for CLI tools (latency is painful), apps where you want theme integration, and non-Ubuntu systems (Snap essentially requires systemd and doesn’t work well outside the Canonical ecosystem).</p>

<hr />

<h2 id="flatpak-the-community-darling">Flatpak: The Community Darling</h2>

<p>Flatpak is the universal package format that most of the Linux community outside of Ubuntu’s gravity well has converged on. It ships by default on Fedora, Linux Mint, and elementary OS. On Ubuntu, you install it yourself — which is, incidentally, one of the things people resent.</p>

<p>I use Flatpak for most of my desktop GUI apps. I also complain about it constantly. Both of these things are true and not in conflict.</p>

<h3 id="the-technical-differences">The Technical Differences</h3>

<p>Flatpak uses Bubblewrap for sandboxing rather than AppArmor. Bubblewrap creates a minimal namespace environment for each app — separate filesystem view, separate process tree, no access to things it hasn’t been explicitly granted.</p>

<p>The portal system is Flatpak’s most interesting idea. Rather than giving apps direct access to system resources, they go through “portal” services — OS-managed brokers that handle things like file access, camera, screen capture, and notifications. When a Flatpak app opens a file dialog, it’s actually asking the portal for a file, and the portal shows the native file picker. The app never sees your filesystem directly unless you grant it access.</p>

<p>This is clever and mostly works. It’s occasionally annoying for CLI tools that expect to do file operations directly without asking permission.</p>

<p>Flatpak also supports decentralized remotes — you’re not locked into a single corporate store. Flathub is the main one and has around 2,500 apps at this point, but you can add any remote including your own.</p>

<h3 id="flatseal-is-non-negotiable">Flatseal Is Non-Negotiable</h3>

<p>Install Flatseal before any other Flatpak. This is not optional advice.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flatpak <span class="nb">install </span>flathub com.github.tchx84.Flatseal
</code></pre></div></div>

<p>Flatseal is a GUI permission manager for Flatpak apps. Without it, managing permissions requires knowing the CLI override syntax and looking up the right portal names. With it, you can see every permission each app has and toggle them with checkboxes.</p>

<p>If you’re going to show one thing to someone who’s skeptical about sandboxing, show them Flatseal. It’s the clearest demonstration of “here’s exactly what this app can access, and you can control it” I’ve seen on Linux.</p>

<h3 id="the-disk-usage-reality">The Disk Usage Reality</h3>

<p>Do <code class="language-plaintext highlighter-rouge">du -sh ~/.var/app</code> after using Flatpak for a few months. I’ll wait.</p>

<p>The shared runtime system means apps that use the same base runtime (say, GNOME Platform 45) share that runtime rather than each bundling it. This is better than every app bundling everything. It is not as good as “install GIMP and it uses the system GTK like a normal package.”</p>

<p>If you install GNOME apps and KDE apps, you’re downloading both runtimes. Each can be 500MB+. If you install apps casually and don’t prune regularly, your Flatpak storage will grow in ways that will surprise you.</p>

<p>Prune it regularly:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flatpak uninstall <span class="nt">--unused</span>          <span class="c"># remove orphaned runtimes</span>
flatpak uninstall <span class="nt">--delete-data</span>     <span class="c"># also wipe app data dirs for removed apps</span>
flatpak repair                      <span class="c"># fix corruption and deduplicate</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">--unused</code> flag is the main one to run monthly. If you’ve been casual about installing and removing apps for a while, <code class="language-plaintext highlighter-rouge">flatpak repair</code> will reclaim space from duplicate objects. Put <code class="language-plaintext highlighter-rouge">flatpak uninstall --unused</code> in a cron job — disk reclamation you never have to think about.</p>

<h3 id="cli-verbosity-the-app-id-problem">CLI Verbosity: The App ID Problem</h3>

<p>Flatpak uses reverse-domain app IDs. Installing LibreOffice is:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flatpak <span class="nb">install </span>flathub org.libreoffice.LibreOffice
</code></pre></div></div>

<p>Running it is:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flatpak run org.libreoffice.LibreOffice
</code></pre></div></div>

<p>This is verbose. Tab completion helps. Shell aliases help more. But it’s a real friction point, especially when you’re used to <code class="language-plaintext highlighter-rouge">apt install libreoffice</code>.</p>

<h3 id="who-flatpak-is-for">Who Flatpak Is For</h3>

<p>Flatpak is the right choice for desktop GUI applications where you want sandboxing with visible, controllable permissions. If you’re on a non-Ubuntu distro, it’s probably already there and is the natural choice. If you want to run apps that actually respect your desktop theme, Flatpak integrates better than Snap.</p>

<p>It is not ideal for CLI tools (portal permission prompts are annoying for things that just need to write a file), and the verbose CLI means it’s not the best choice for quick one-off installs.</p>

<hr />

<h2 id="head-to-head-the-actual-comparison">Head to Head: The Actual Comparison</h2>

<p>Let me be blunt about the decision matrix.</p>

<p><strong>Use AppImage when:</strong></p>
<ul>
  <li>Portability is the primary constraint</li>
  <li>You’re on an air-gapped system</li>
  <li>You need a specific version and don’t want it to update</li>
  <li>You want zero system changes</li>
  <li>You’re on a weird distro and just need something to run</li>
</ul>

<p><strong>Use Snap when:</strong></p>
<ul>
  <li>You’re on Ubuntu and the app is available</li>
  <li>It’s a server or daemon app where auto-updates are a feature</li>
  <li>You want the app to auto-patch and you trust Canonical’s update cadence</li>
  <li>Startup latency doesn’t matter (background services, browsers you keep open)</li>
</ul>

<p><strong>Use Flatpak when:</strong></p>
<ul>
  <li>It’s a desktop GUI app</li>
  <li>You care about sandboxing and want to see what the app can access</li>
  <li>You’re on any non-Ubuntu distro that ships Flatpak</li>
  <li>You want your apps to actually look like they belong on your desktop</li>
</ul>

<p><strong>Use apt/dnf/pacman when:</strong></p>
<ul>
  <li>The app is in your distro’s repo</li>
  <li>Performance matters</li>
  <li>You want proper system integration</li>
  <li>You want security updates handled by people who know your distro</li>
  <li>You like your sanity</li>
</ul>

<p>That last option is the one people forget. Native package managers exist, work well for most software, and are the correct default. Universal packages are the fallback, not the baseline.</p>

<hr />

<h2 id="the-ugly-truths-i-didnt-want-to-say-but-will">The Ugly Truths I Didn’t Want to Say But Will</h2>

<p>Universal packages do not replace your distro’s package manager. If you’re using Flatpak for everything on Ubuntu, you’re doing it wrong. Your distro’s maintainers package most of what you need and they package it better.</p>

<p>They all increase disk usage. Significantly. This is the price of bundling. It’s a reasonable price for some use cases and an unreasonable one for others.</p>

<p>They all shift trust boundaries. With apt, you’re trusting your distro’s security team. With universal packages, you’re trusting the developer — or the Flathub submission reviewers, or Canonical’s store team. This is not necessarily worse, but it’s different, and you should be aware of it.</p>

<p>The reason we have three competing formats is because the Linux community couldn’t agree on one. This is both a joke and a tragedy. Each format made different tradeoffs, found different communities, and entrenched. We now live in a world where you might reasonably have AppImages, Snaps, Flatpaks, and native packages all installed on the same machine.</p>

<p>I do. I complain about it. I keep all of them.</p>

<hr />

<h2 id="the-series-goes-much-deeper">The Series Goes Much Deeper</h2>

<p>This post is the overview and decision matrix. If you want the real internals, the six-part series has you covered:</p>

<p><strong><a href="https://denner.co/blog">Part 2 — AppImage Deep Dive</a></strong>: Goes byte-for-byte into the ELF + squashfs structure, how FUSE mounting works without root, the libfuse2/libfuse3 transition that broke Ubuntu 22.04, and the zsync update protocol. Includes the <code class="language-plaintext highlighter-rouge">--appimage-*</code> debugging flags nobody tells you about.</p>

<p><strong><a href="https://denner.co/blog">Part 3 — Snap Deep Dive</a></strong>: Dissects why snapd needs to run forever as a daemon, the full snap-confine → AppArmor → seccomp execution chain, the snap type hierarchy (app/base/kernel/gadget/content), and how snap assertions build the trust chain back to Canonical.</p>

<p><strong><a href="https://denner.co/blog">Part 4 — Flatpak Deep Dive</a></strong>: Bubblewrap user namespaces, OSTree content-addressable storage, and how xdg-desktop-portal lets sandboxed apps open a file picker without ever seeing your filesystem.</p>

<p><strong><a href="https://denner.co/blog">Part 5 — vs Docker</a></strong>: Compares all three formats with Docker’s full namespace stack, cgroups resource limits, and OverlayFS storage. Answers the “is a Flatpak app a container?” question properly.</p>

<p><strong><a href="https://denner.co/blog">Part 6 — Build Your Own</a></strong>: The guide I wish existed. Full walkthroughs for packaging in all three formats — including the glibc floor problem, confinement debugging, no-network-during-build constraints, and CI patterns that actually work.</p>

<hr />

<h2 id="resources">Resources</h2>

<p><strong>AppImage:</strong></p>
<ul>
  <li><a href="https://appimage.github.io">appimage.github.io</a> — curated catalog</li>
  <li><a href="https://github.com/AppImageCommunity/AppImageLauncher">AppImageLauncher</a> — get this</li>
</ul>

<p><strong>Snap:</strong></p>
<ul>
  <li><a href="https://snapcraft.io">snapcraft.io</a> — store and docs</li>
  <li><code class="language-plaintext highlighter-rouge">sudo snap refresh --hold=48h</code> — memorize this command</li>
</ul>

<p><strong>Flatpak:</strong></p>
<ul>
  <li><a href="https://flathub.org">flathub.org</a> — the app store</li>
  <li><a href="https://flathub.org/apps/com.github.tchx84.Flatseal">Flatseal</a> — install immediately</li>
  <li><code class="language-plaintext highlighter-rouge">flatpak uninstall --unused</code> — run monthly</li>
</ul>

<p><strong>Talk materials:</strong></p>
<ul>
  <li>Slides: <a href="https://denner.co/talks">denner.co/talks</a></li>
  <li>Demo scripts: <a href="https://github.com/adenner/cialug-pkg-talk">github.com/adenner/cialug-pkg-talk</a></li>
</ul>]]></content><author><name>Andrew Denner</name></author><summary type="html"><![CDATA[I gave this talk at CIALUG on March 18, 2026. This is the long-form version — the full opinionated breakdown of all three universal Linux package formats, informed by years of using each of them, occasionally loving them, and at least twice nuking them from orbit.]]></summary></entry><entry><title type="html">Denner Family Christmas Letter-2025</title><link href="http://0.0.0.0:4000/2025/12/19/Christmas.html" rel="alternate" type="text/html" title="Denner Family Christmas Letter-2025" /><published>2025-12-19T15:35:14+00:00</published><updated>2025-12-19T15:35:14+00:00</updated><id>http://0.0.0.0:4000/2025/12/19/Christmas</id><content type="html" xml:base="http://0.0.0.0:4000/2025/12/19/Christmas.html"><![CDATA[<p>December 19, 2025</p>

<p>Dear friends and family,</p>

<p>It is hard to believe that we are nearing the end of 2025. It has been a whirlwind of a year for us. The year started out rough with the passing of Jess’s youngest sister Gabby.</p>

<p>The family took a family trip to Chicago where we all enjoyed a trip to the Lincoln Park Zoo, and the Shedd aquarium. Isaac especially liked visiting all his fish friends. Navy Pier and the children’s museum.</p>

<p>Closer to home, Isaac has claimed the Blank Park Zoo as his own. With our family membership we have made multiple trips, and he knows where all his favorite animals are, including the giraffe, tiger, lion and even dinosaurs (they had animatronic dinos visiting for several months). Our last trip was just to have pizza and hot coco with Santa.</p>

<p>We also spent a long weekend in South Dakota visiting all the usual tourist hot spots including Wall Drug, Mount Rushmore, and even Bear Country. Isaac loved spotting all the animals, some even before us adults. On the way home we made a quick stop at the Bad Lands where we saw some of the locals including a large buffalo and Prairie Dogs. Unfortunately, while we were gone, our sump pump also decided to take a bit of break. Thankfully we had a remote sensor that tipped us off to the issue and were able to get help from our entire neighborhood to get the water pumped out, and basement dried out. (Especially thanks to Ron, Travis, Mike, the Colvin’s (and anyone I may have missed).</p>

<p>We continued to enjoy family camping trips in Bessie, our 1994 Pace Arrow Camper. It was a busy summer as you can see, but we were able to find a few weekends to go out in the woods. Isaac has still not found a playground he didn’t like, and it has been a joy to see his joy exploring new places.</p>

<p>It is hard to believe that Isaac is now 3 years old. He continues to thrive at Beautiful Beginnings where he is enjoying playing with his friends in the 2-year-olds room.  Also, he just passed his latest swim lesson class at Foss Swim and his next step will be starting to swim on his own with a small class supervised by an instructor.</p>

<p>This year Jess turned the big 40 and celebrated with a big birthday bash. She continues to be a Senior Quality Assurance Specialist at Kemin. Her baking efforts continue to yield with another winning year at the Iowa State Fair with 31 entries and 5 first places.</p>

<p>Andy is still doing the same job at Corteva Agriscience but with a slightly different title, he is now a Scientific Computing Scientist. He also continues to be the president of the Central Iowa Linux Users group.</p>

<p>We hope all of you have had a great year, and that you all have a Merry Christmas, and a happy holidays!</p>

<p>Jess, Andy and Isaac</p>]]></content><author><name>Andrew Denner</name></author><summary type="html"><![CDATA[December 19, 2025]]></summary></entry><entry><title type="html">More Than a Badge: Reflections on the True Meaning of Eagle Scout</title><link href="http://0.0.0.0:4000/2025/07/06/eagle.html" rel="alternate" type="text/html" title="More Than a Badge: Reflections on the True Meaning of Eagle Scout" /><published>2025-07-06T15:35:14+00:00</published><updated>2025-07-06T15:35:14+00:00</updated><id>http://0.0.0.0:4000/2025/07/06/eagle</id><content type="html" xml:base="http://0.0.0.0:4000/2025/07/06/eagle.html"><![CDATA[<p>A few days ago, I had the distinct honor of speaking at the Eagle Scout Court of Honor for a remarkable young man named Brandon. Congratulations to him and his family on this incredible achievement.</p>

<p>Leading up to the ceremony, I spent a lot of time thinking about what the Eagle Scout award truly means. It’s easy to list the famous names or the impressive statistics, but I wanted to get to the heart of it. My goal was to explore the quiet, everyday leadership that the journey instills in a person—the skills and the code that you carry with you long after you’ve folded up the sash.</p>

<p>This post is a slightly polished version of that speech. I hope it resonates with fellow Eagles, Scouters, parents, and anyone who believes in the power of service and living a life of purpose.</p>

<hr />

<p><a href="/assets/Brandon.Eagle.pdf">pdf original</a></p>

<h3 id="an-eagle-scout-court-of-honor-speech">An Eagle Scout Court of Honor Speech</h3>

<p>Welcome everyone, I understand that when they were looking for a speaker for today, they asked a great orator, someone was well liked by all, who is handsome, and amazing but unfortunately, they were unavailable… so instead they asked me. In all seriousness, I am greatly honored to be here today to celebrate with you all Brandon’s accomplishment as well as to say a little bit about the Eagle Scout award.</p>

<p>As the NESA eagle roll says:</p>

<blockquote>
  <p>“The Eagle Scout Award. It’s Scouting’s highest rank and among its most familiar icons. [Those] who have earned it count it among their most treasured possessions. Those who missed it by a whisker remember exactly which requirement they didn’t complete. Americans from all walks of life know that being an Eagle Scout is a great honor, even if they don’t know just what the badge means.”</p>

  <p>“The award is more than a badge. It’s a state of being. You are an Eagle Scout—never were. You may have received the badge as a [youth], but you earn it every day as a[n adult]. In the words of the Eagle Scout Promise, you do your best each day to make your training and example, your rank and your influence count strongly for better Scouting and for better citizenship in your troop, in your community, and in your contacts with other people. And to this you pledge your sacred honor.”</p>
</blockquote>

<p>From the first Eagle Scout, Arthur Rose Eldred in 1910, there have been over 2.75 million men and women who have earned the award. To put it in perspective, this is a little bigger than Chicago. This last Eagle class, which you are a part of, was comprised of 29 thousand people (a little bigger than Marshalltown). When you compare this to the over one million youth who participated in Scouting (or half the size of the Kansas City metro area), this means a little less than 3% of Scouts earned their Eagle Scout award.</p>

<p>While doing this, they completed roughly 2.8 million hours of service projects. This service project helps not only the Eagle but their fellow Scouts step outside their bubble, identify a problem, make a plan, and follow through on it. It helps you to think about things bigger than just your immediate self and also is a taste of leading and marshaling others, some who are far older and more experienced than you are. Brandon, in your case, your bird houses helped to make the world just a bit better.</p>

<p>So who are your fellow Eagle Scouts? They include Astronauts, politicians, businessmen, and teachers, like the first man on the moon Neil Armstrong, or Jim Lovell of the Apollo 13 mission. Bill Amend, artist of the Foxtrot Cartoons. Inventor of the first electronic television Philo Farnsworth, or Kevin Kwan Loucks, a concert pianist. It also includes less famous people like Jamie Bryant, who later became an auto mechanic, ran several Goodyear auto service centers, and gave back serving as an Automotive Maintenance Merit Badge Counselor. Or, Don Garlits, a pioneer in the sport of drag racing.</p>

<p>You don’t have to stop at famous people though. It has been my experience that you always know who the Eagle Scouts are when you meet them. They are always willing and able to help, whether at work, out in their community, or at Church. They are the ones quick to tie the right knot, able to lead by example, have first aid skills, and are able to start a good campfire.</p>

<p>There is always something about your fellow Eagle Scouts that just makes them stand out a little bit. As TV host Mike Rowe once said:</p>
<blockquote>
  <p>“I learned to lead as a Boy Scout, and I’d like to think I still do today.”</p>
</blockquote>

<p>These skills are recognized by those around you as well. They were the inspiration for Chris Evans when he starred as Captain America.</p>
<blockquote>
  <p>“There’s a kid that I grew up with named Charlie Morris. He’s the best kid I know. He was an Eagle Scout. And being an Eagle Scout is not easy — you’ve got to really do it for a long time. But he’s just such a good man, and he genuinely, genuinely puts himself last. He lives by a code. And so, when I took the role, I told Charlie, ‘Listen. I’m modeling this after you.’”</p>
</blockquote>

<p>As Dr. Randy Pausch, another Eagle Scout who was a Professor of Computer Science at Carnegie Mellon University, once said:</p>
<blockquote>
  <p>“Becoming an Eagle Scout is just about the only thing you can put on your resume at age 50 that you did at age 14 and it still impresses.”</p>
</blockquote>

<p>I know personally, it is still a major highlight of my resume.</p>

<p>Mike Rowe sends this advice to new Eagle Scouts:</p>
<blockquote>
  <p>“Don’t wait for the fold to acknowledge your accomplishments. By all means take pride in what you’ve done, but don’t let it go to your head… Fold up your sash and stow it away somewhere private with the other tokens of what you’ve done so far. Then roll up your sleeves, get out into the world and put what you’ve learned to use.”</p>
</blockquote>

<p>For me, the skills that I learned in Scouting have helped me so many times, whether it be the love of camping and the outdoors that it has instilled in me, the first aid skills that I have had to practice on myself and others (what can I say, I’m a klutz), or the taste of hobbies and life skills that I picked up in all the different merit badges, like personal finance. It has also proved incalculably helpful in the organization and leadership skills it has helped me learn, whether that be at work, serving in my church, or community organizations like the State of Iowa Science and Technology Fair Board. Brandon, I know that Scouting has provided the same for you and I challenge you to go out now into life and put your skills to use. Be that light in the world that shows others by example.</p>

<p>I will leave you with a Poem by Richelle E. Goodrich, “Today you Soar”:</p>

<blockquote>
  <p>Like the grand eagle, you spread your wings<br />
And put forth the effort to do great things…</p>

  <p>So now you’ve reached where few even try<br />
As the eagle high in a glorious sky.</p>

  <p>Not superior, but grand.<br />
Not proud, but sure.</p>

  <p>Not a cub, wolf, or bear but an eagle pure.</p>

  <p>Today [, Brandon,] you soar.</p>
</blockquote>]]></content><author><name>Andrew Denner</name></author><summary type="html"><![CDATA[A few days ago, I had the distinct honor of speaking at the Eagle Scout Court of Honor for a remarkable young man named Brandon. Congratulations to him and his family on this incredible achievement. Leading up to the ceremony, I spent a lot of time thinking about what the Eagle Scout award truly means. It’s easy to list the famous names or the impressive statistics, but I wanted to get to the heart of it. My goal was to explore the quiet, everyday leadership that the journey instills in a person—the skills and the code that you carry with you long after you’ve folded up the sash. This post is a slightly polished version of that speech. I hope it resonates with fellow Eagles, Scouters, parents, and anyone who believes in the power of service and living a life of purpose. pdf original An Eagle Scout Court of Honor Speech Welcome everyone, I understand that when they were looking for a speaker for today, they asked a great orator, someone was well liked by all, who is handsome, and amazing but unfortunately, they were unavailable… so instead they asked me. In all seriousness, I am greatly honored to be here today to celebrate with you all Brandon’s accomplishment as well as to say a little bit about the Eagle Scout award. As the NESA eagle roll says: “The Eagle Scout Award. It’s Scouting’s highest rank and among its most familiar icons. [Those] who have earned it count it among their most treasured possessions. Those who missed it by a whisker remember exactly which requirement they didn’t complete. Americans from all walks of life know that being an Eagle Scout is a great honor, even if they don’t know just what the badge means.” “The award is more than a badge. It’s a state of being. You are an Eagle Scout—never were. You may have received the badge as a [youth], but you earn it every day as a[n adult]. In the words of the Eagle Scout Promise, you do your best each day to make your training and example, your rank and your influence count strongly for better Scouting and for better citizenship in your troop, in your community, and in your contacts with other people. And to this you pledge your sacred honor.” From the first Eagle Scout, Arthur Rose Eldred in 1910, there have been over 2.75 million men and women who have earned the award. To put it in perspective, this is a little bigger than Chicago. This last Eagle class, which you are a part of, was comprised of 29 thousand people (a little bigger than Marshalltown). When you compare this to the over one million youth who participated in Scouting (or half the size of the Kansas City metro area), this means a little less than 3% of Scouts earned their Eagle Scout award. While doing this, they completed roughly 2.8 million hours of service projects. This service project helps not only the Eagle but their fellow Scouts step outside their bubble, identify a problem, make a plan, and follow through on it. It helps you to think about things bigger than just your immediate self and also is a taste of leading and marshaling others, some who are far older and more experienced than you are. Brandon, in your case, your bird houses helped to make the world just a bit better. So who are your fellow Eagle Scouts? They include Astronauts, politicians, businessmen, and teachers, like the first man on the moon Neil Armstrong, or Jim Lovell of the Apollo 13 mission. Bill Amend, artist of the Foxtrot Cartoons. Inventor of the first electronic television Philo Farnsworth, or Kevin Kwan Loucks, a concert pianist. It also includes less famous people like Jamie Bryant, who later became an auto mechanic, ran several Goodyear auto service centers, and gave back serving as an Automotive Maintenance Merit Badge Counselor. Or, Don Garlits, a pioneer in the sport of drag racing. You don’t have to stop at famous people though. It has been my experience that you always know who the Eagle Scouts are when you meet them. They are always willing and able to help, whether at work, out in their community, or at Church. They are the ones quick to tie the right knot, able to lead by example, have first aid skills, and are able to start a good campfire. There is always something about your fellow Eagle Scouts that just makes them stand out a little bit. As TV host Mike Rowe once said: “I learned to lead as a Boy Scout, and I’d like to think I still do today.” These skills are recognized by those around you as well. They were the inspiration for Chris Evans when he starred as Captain America. “There’s a kid that I grew up with named Charlie Morris. He’s the best kid I know. He was an Eagle Scout. And being an Eagle Scout is not easy — you’ve got to really do it for a long time. But he’s just such a good man, and he genuinely, genuinely puts himself last. He lives by a code. And so, when I took the role, I told Charlie, ‘Listen. I’m modeling this after you.’” As Dr. Randy Pausch, another Eagle Scout who was a Professor of Computer Science at Carnegie Mellon University, once said: “Becoming an Eagle Scout is just about the only thing you can put on your resume at age 50 that you did at age 14 and it still impresses.” I know personally, it is still a major highlight of my resume. Mike Rowe sends this advice to new Eagle Scouts: “Don’t wait for the fold to acknowledge your accomplishments. By all means take pride in what you’ve done, but don’t let it go to your head… Fold up your sash and stow it away somewhere private with the other tokens of what you’ve done so far. Then roll up your sleeves, get out into the world and put what you’ve learned to use.” For me, the skills that I learned in Scouting have helped me so many times, whether it be the love of camping and the outdoors that it has instilled in me, the first aid skills that I have had to practice on myself and others (what can I say, I’m a klutz), or the taste of hobbies and life skills that I picked up in all the different merit badges, like personal finance. It has also proved incalculably helpful in the organization and leadership skills it has helped me learn, whether that be at work, serving in my church, or community organizations like the State of Iowa Science and Technology Fair Board. Brandon, I know that Scouting has provided the same for you and I challenge you to go out now into life and put your skills to use. Be that light in the world that shows others by example. I will leave you with a Poem by Richelle E. Goodrich, “Today you Soar”: Like the grand eagle, you spread your wings And put forth the effort to do great things… So now you’ve reached where few even try As the eagle high in a glorious sky. Not superior, but grand. Not proud, but sure. Not a cub, wolf, or bear but an eagle pure. Today [, Brandon,] you soar.]]></summary></entry><entry><title type="html">Beyond the Standard Library: A Modern C++ Toolkit for Senior Engineers and Architects</title><link href="http://0.0.0.0:4000/2025/07/05/cpplib.html" rel="alternate" type="text/html" title="Beyond the Standard Library: A Modern C++ Toolkit for Senior Engineers and Architects" /><published>2025-07-05T15:35:14+00:00</published><updated>2025-07-05T15:35:14+00:00</updated><id>http://0.0.0.0:4000/2025/07/05/cpplib</id><content type="html" xml:base="http://0.0.0.0:4000/2025/07/05/cpplib.html"><![CDATA[<p>The C++ landscape has undergone a profound transformation. With the advent of modern standards from C++11 to C++23, the language has evolved to favor expressiveness, safety, and performance. While the C++ Standard Library is more powerful than ever, the ecosystem of third-party libraries remains the cornerstone of productive, high-performance software development. For senior engineers, scientific computing specialists, and software architects, navigating this ecosystem is a critical skill.</p>

<p>This guide provides a curated look at essential modern C++ libraries, moving beyond the obvious choices to highlight tools that solve complex problems with elegance and efficiency. Each section includes practical examples and expected outputs to illustrate their real-world application.</p>

<h2 id="core-foundational-libraries">Core Foundational Libraries</h2>

<p>Before diving into specialized domains, every C++ developer should be familiar with these foundational toolkits.</p>

<ul>
  <li>
    <p><strong>Boost</strong> A collection of high-quality, peer-reviewed libraries that often serve as a testing ground for features that later become part of the C++ standard. It provides robust implementations for everything from multithreading and data structures to string algorithms and pseudorandom number generation. While its size can be daunting, its quality is exceptional.</p>
  </li>
  <li>
    <p><strong>POCO (POrtable COmponents)</strong> A set of C++ libraries focused on building network-centric, portable applications for desktop, server, and embedded systems. Its modular design makes it suitable for IoT, industrial automation, and enterprise applications. POCO offers both an open-source version and a commercially licensed pro version.</p>
  </li>
</ul>

<h2 id="command-line-interface-cli-frameworks">Command-Line Interface (CLI) Frameworks</h2>

<p>Creating polished and user-friendly CLIs in C++ is streamlined by several excellent libraries.</p>

<h3 id="cli11"><strong>CLI11</strong></h3>

<p>A powerful, header-only command-line parser for C++11 and beyond that is praised for its simple, intuitive interface and rich feature set.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"CLI/CLI.hpp"</span><span class="cp">
#include</span> <span class="cpf">&lt;iostream&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span><span class="o">**</span> <span class="n">argv</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">CLI</span><span class="o">::</span><span class="n">App</span> <span class="n">app</span><span class="p">{</span><span class="s">"Log Processor"</span><span class="p">};</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">filename</span><span class="p">;</span>
    <span class="n">app</span><span class="p">.</span><span class="n">add_option</span><span class="p">(</span><span class="s">"-f,--file"</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="s">"Log file to process"</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">required</span><span class="p">();</span>
    <span class="kt">int</span> <span class="n">level</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">app</span><span class="p">.</span><span class="n">add_option</span><span class="p">(</span><span class="s">"-l,--level"</span><span class="p">,</span> <span class="n">level</span><span class="p">,</span> <span class="s">"Logging level (0-4)"</span><span class="p">);</span>
    <span class="kt">bool</span> <span class="n">verbose</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
    <span class="n">app</span><span class="p">.</span><span class="n">add_flag</span><span class="p">(</span><span class="s">"-v,--verbose"</span><span class="p">,</span> <span class="n">verbose</span><span class="p">,</span> <span class="s">"Enable verbose output"</span><span class="p">);</span>
    <span class="n">CLI11_PARSE</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">argc</span><span class="p">,</span> <span class="n">argv</span><span class="p">);</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Processing file: "</span> <span class="o">&lt;&lt;</span> <span class="n">filename</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Log level: "</span> <span class="o">&lt;&lt;</span> <span class="n">level</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">verbose</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Verbose mode enabled."</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="cli"><strong>cli</strong></h3>

<p>A header-only library for building interactive command-line interfaces, similar to a router’s console. It supports command history, nested menus, and auto-completion.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;iostream&gt;</span><span class="cp">
#include</span> <span class="cpf">"cli/cli.h"</span><span class="cp">
#include</span> <span class="cpf">"cli/clilocalsession.h"</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">auto</span> <span class="n">root_menu</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">make_unique</span><span class="o">&lt;</span><span class="n">cli</span><span class="o">::</span><span class="n">Menu</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"main"</span><span class="p">);</span>
    <span class="n">root_menu</span><span class="o">-&gt;</span><span class="n">Add</span><span class="p">(</span>
        <span class="s">"status"</span><span class="p">,</span>
        <span class="p">[](</span><span class="n">std</span><span class="o">::</span><span class="n">ostream</span><span class="o">&amp;</span> <span class="n">out</span><span class="p">){</span> <span class="n">out</span> <span class="o">&lt;&lt;</span> <span class="s">"System is OK."</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span> <span class="p">},</span>
        <span class="s">"Print system status"</span><span class="p">);</span>
    <span class="n">root_menu</span><span class="o">-&gt;</span><span class="n">Add</span><span class="p">(</span>
        <span class="s">"exit"</span><span class="p">,</span>
        <span class="p">[](</span><span class="n">std</span><span class="o">::</span><span class="n">ostream</span><span class="o">&amp;</span> <span class="n">out</span><span class="p">){</span> <span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="p">},</span>
        <span class="s">"Exit the application"</span><span class="p">);</span>

    <span class="n">cli</span><span class="o">::</span><span class="n">Cli</span> <span class="nf">cli</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">move</span><span class="p">(</span><span class="n">root_menu</span><span class="p">));</span>
    <span class="n">cli</span><span class="p">.</span><span class="n">ExitAction</span><span class="p">(</span> <span class="p">[](</span><span class="k">auto</span><span class="o">&amp;</span> <span class="n">out</span><span class="p">){</span> <span class="n">out</span> <span class="o">&lt;&lt;</span> <span class="s">"Goodbye!"</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span> <span class="p">}</span> <span class="p">);</span>

    <span class="n">cli</span><span class="o">::</span><span class="n">CliLocalSession</span> <span class="nf">session</span><span class="p">(</span><span class="n">cli</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="p">,</span> <span class="mi">200</span><span class="p">);</span>
    <span class="n">session</span><span class="p">.</span><span class="n">MainLoop</span><span class="p">();</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="scientific--high-performance-computing">Scientific &amp; High-Performance Computing</h2>

<p>C++ is the language of choice for performance-critical scientific computing, supported by a rich set of numerical libraries.</p>

<h3 id="eigen"><strong>Eigen</strong></h3>

<p>A powerful C++ template library for linear algebra, matrices, vectors, and numerical solvers. It is widely considered the de facto standard for such tasks due to its expressive API, high performance, and reliability.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;iostream&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;Eigen/Dense&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">Eigen</span><span class="o">::</span><span class="n">Matrix3f</span> <span class="n">A</span><span class="p">;</span>
    <span class="n">A</span> <span class="o">&lt;&lt;</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span>
         <span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
        <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">;</span>

    <span class="n">Eigen</span><span class="o">::</span><span class="n">Vector3f</span> <span class="n">b</span><span class="p">;</span>
    <span class="n">b</span> <span class="o">&lt;&lt;</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">;</span>

    <span class="n">Eigen</span><span class="o">::</span><span class="n">Vector3f</span> <span class="n">x</span> <span class="o">=</span> <span class="n">A</span><span class="p">.</span><span class="n">colPivHouseholderQr</span><span class="p">().</span><span class="n">solve</span><span class="p">(</span><span class="n">b</span><span class="p">);</span>

    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"The solution is:</span><span class="se">\n</span><span class="s">"</span> <span class="o">&lt;&lt;</span> <span class="n">x</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="json-for-modern-c">JSON for Modern C++</h2>

<p>Working with JSON is a common requirement, and this library makes it painless.</p>

<h3 id="nlohmannjson"><strong>nlohmann/json</strong></h3>

<p>A single-header library that provides an intuitive way to work with JSON, mimicking Python’s dictionary and list access.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;iostream&gt;</span><span class="cp">
#include</span> <span class="cpf">"nlohmann/json.hpp"</span><span class="cp">
</span>
<span class="k">using</span> <span class="n">json</span> <span class="o">=</span> <span class="n">nlohmann</span><span class="o">::</span><span class="n">json</span><span class="p">;</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// Create a JSON object</span>
    <span class="n">json</span> <span class="n">j</span><span class="p">;</span>
    <span class="n">j</span><span class="p">[</span><span class="s">"service"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"config-server"</span><span class="p">;</span>
    <span class="n">j</span><span class="p">[</span><span class="s">"port"</span><span class="p">]</span> <span class="o">=</span> <span class="mi">8080</span><span class="p">;</span>
    <span class="n">j</span><span class="p">[</span><span class="s">"active"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
    <span class="n">j</span><span class="p">[</span><span class="s">"protocols"</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="s">"http"</span><span class="p">,</span> <span class="s">"https"</span><span class="p">};</span>

    <span class="c1">// Serialize to string</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Serialized JSON: "</span> <span class="o">&lt;&lt;</span> <span class="n">j</span><span class="p">.</span><span class="n">dump</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>

    <span class="c1">// Parse from string</span>
    <span class="k">auto</span> <span class="n">parsed_json</span> <span class="o">=</span> <span class="n">json</span><span class="o">::</span><span class="n">parse</span><span class="p">(</span><span class="s">R"({"user": "admin", "id": 123})"</span><span class="p">);</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Parsed user: "</span> <span class="o">&lt;&lt;</span> <span class="n">parsed_json</span><span class="p">[</span><span class="s">"user"</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="gui-and-visualization-libraries">GUI and Visualization Libraries</h2>

<p>While C++ GUI development can be complex, several excellent libraries cater to different needs, from embedded systems to full-scale desktop applications.</p>

<h3 id="dear-imgui"><strong>Dear ImGui</strong></h3>

<p>A fast, portable, and self-contained immediate-mode graphical user interface library. It is especially popular for game development tools, 3D applications, and real-time debugging overlays due to its simplicity and high performance.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// This is a conceptual example. Full implementation requires a rendering backend (e.g., OpenGL, Vulkan).</span>
<span class="cp">#include</span> <span class="cpf">"imgui.h"</span><span class="cp">
</span>
<span class="kt">void</span> <span class="nf">RenderMyUI</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">ImGui</span><span class="o">::</span><span class="n">Begin</span><span class="p">(</span><span class="s">"System Control"</span><span class="p">);</span>
    <span class="k">static</span> <span class="kt">int</span> <span class="n">counter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">ImGui</span><span class="o">::</span><span class="n">Text</span><span class="p">(</span><span class="s">"Application average %.3f ms/frame (%.1f FPS)"</span><span class="p">,</span> <span class="mf">1000.0f</span> <span class="o">/</span> <span class="n">ImGui</span><span class="o">::</span><span class="n">GetIO</span><span class="p">().</span><span class="n">Framerate</span><span class="p">,</span> <span class="n">ImGui</span><span class="o">::</span><span class="n">GetIO</span><span class="p">().</span><span class="n">Framerate</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">ImGui</span><span class="o">::</span><span class="n">Button</span><span class="p">(</span><span class="s">"Increment Counter"</span><span class="p">))</span> <span class="p">{</span>
        <span class="n">counter</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">ImGui</span><span class="o">::</span><span class="n">SameLine</span><span class="p">();</span>
    <span class="n">ImGui</span><span class="o">::</span><span class="n">Text</span><span class="p">(</span><span class="s">"counter = %d"</span><span class="p">,</span> <span class="n">counter</span><span class="p">);</span>
    <span class="n">ImGui</span><span class="o">::</span><span class="n">End</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="other-notable-gui-frameworks"><strong>Other Notable GUI Frameworks:</strong></h3>

<ul>
  <li>
    <p><strong>Qt:</strong> A mature, comprehensive, cross-platform framework for creating applications with advanced user interfaces. It is more than just a GUI toolkit, providing extensive modules for networking, databases, and more.</p>
  </li>
  <li>
    <p><strong>wxWidgets:</strong> A library that allows developers to create applications with a truly native look and feel by using the platform’s native API rather than emulating it.</p>
  </li>
</ul>

<h2 id="specialized-and-utility-libraries">Specialized and Utility Libraries</h2>

<p>For specific problem domains, these libraries are indispensable.</p>

<ul>
  <li>
    <p><strong>OpenCV (Open Source Computer Vision Library):</strong> The premier library for real-time computer vision, machine learning, and image processing. It features thousands of optimized algorithms for tasks like facial recognition, object detection, and 3D model extraction.</p>
  </li>
  <li>
    <p><strong>GoogleTest:</strong> A powerful and widely adopted testing framework from Google. It provides an xUnit test framework and a mocking framework, making it a cornerstone of modern C++ development for ensuring code quality.</p>
  </li>
  <li>
    <p><strong>Asio:</strong> A cross-platform C++ library for network and low-level I/O programming that provides developers with a consistent asynchronous model. It is part of the Boost project but is also available as a standalone library.</p>
  </li>
</ul>

<h2 id="strategic-considerations-for-library-selection">Strategic Considerations for Library Selection</h2>

<p>For architects and senior engineers, choosing a library involves more than just its API.</p>

<ul>
  <li>
    <p><strong>Build System Integration:</strong> The C++ ecosystem relies heavily on build systems like <strong>CMake</strong> and package managers like <strong>vcpkg</strong> and <strong>Conan</strong>. A library’s compatibility with these tools is a major factor in its ease of adoption.</p>
  </li>
  <li>
    <p><strong>Modern C++ Compatibility:</strong> Ensure that a library aligns with your project’s target C++ standard (e.g., C++17, C++20) and follows modern best practices.</p>
  </li>
  <li>
    <p><strong>Header-Only vs. Compiled:</strong> Header-only libraries offer easy integration but can increase compile times. Compiled libraries require linking but can be faster to build against once compiled.</p>
  </li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Mastering modern C++ involves building a curated toolkit of high-quality libraries. The selections presented here represent robust, battle-tested solutions that empower developers to build complex, high-performance systems efficiently. By understanding the trade-offs and strengths of each library, senior engineers and architects can make informed technology decisions that accelerate development, enhance performance, and ensure long-term maintainability.</p>]]></content><author><name>Andrew Denner</name></author><summary type="html"><![CDATA[The C++ landscape has undergone a profound transformation. With the advent of modern standards from C++11 to C++23, the language has evolved to favor expressiveness, safety, and performance. While the C++ Standard Library is more powerful than ever, the ecosystem of third-party libraries remains the cornerstone of productive, high-performance software development. For senior engineers, scientific computing specialists, and software architects, navigating this ecosystem is a critical skill. This guide provides a curated look at essential modern C++ libraries, moving beyond the obvious choices to highlight tools that solve complex problems with elegance and efficiency. Each section includes practical examples and expected outputs to illustrate their real-world application. Core Foundational Libraries Before diving into specialized domains, every C++ developer should be familiar with these foundational toolkits. Boost A collection of high-quality, peer-reviewed libraries that often serve as a testing ground for features that later become part of the C++ standard. It provides robust implementations for everything from multithreading and data structures to string algorithms and pseudorandom number generation. While its size can be daunting, its quality is exceptional. POCO (POrtable COmponents) A set of C++ libraries focused on building network-centric, portable applications for desktop, server, and embedded systems. Its modular design makes it suitable for IoT, industrial automation, and enterprise applications. POCO offers both an open-source version and a commercially licensed pro version. Command-Line Interface (CLI) Frameworks Creating polished and user-friendly CLIs in C++ is streamlined by several excellent libraries. CLI11 A powerful, header-only command-line parser for C++11 and beyond that is praised for its simple, intuitive interface and rich feature set. #include "CLI/CLI.hpp" #include &lt;iostream&gt; int main(int argc, char** argv) { CLI::App app{"Log Processor"}; std::string filename; app.add_option("-f,--file", filename, "Log file to process")-&gt;required(); int level = 0; app.add_option("-l,--level", level, "Logging level (0-4)"); bool verbose = false; app.add_flag("-v,--verbose", verbose, "Enable verbose output"); CLI11_PARSE(app, argc, argv); std::cout &lt;&lt; "Processing file: " &lt;&lt; filename &lt;&lt; std::endl; std::cout &lt;&lt; "Log level: " &lt;&lt; level &lt;&lt; std::endl; if (verbose) { std::cout &lt;&lt; "Verbose mode enabled." &lt;&lt; std::endl; } return 0; } cli A header-only library for building interactive command-line interfaces, similar to a router’s console. It supports command history, nested menus, and auto-completion. #include &lt;iostream&gt; #include "cli/cli.h" #include "cli/clilocalsession.h" int main() { auto root_menu = std::make_unique&lt;cli::Menu&gt;("main"); root_menu-&gt;Add( "status", [](std::ostream&amp; out){ out &lt;&lt; "System is OK." &lt;&lt; std::endl; }, "Print system status"); root_menu-&gt;Add( "exit", [](std::ostream&amp; out){ exit(0); }, "Exit the application"); cli::Cli cli(std::move(root_menu)); cli.ExitAction( [](auto&amp; out){ out &lt;&lt; "Goodbye!" &lt;&lt; std::endl; } ); cli::CliLocalSession session(cli, std::cout, 200); session.MainLoop(); return 0; } Scientific &amp; High-Performance Computing C++ is the language of choice for performance-critical scientific computing, supported by a rich set of numerical libraries. Eigen A powerful C++ template library for linear algebra, matrices, vectors, and numerical solvers. It is widely considered the de facto standard for such tasks due to its expressive API, high performance, and reliability. #include &lt;iostream&gt; #include &lt;Eigen/Dense&gt; int main() { Eigen::Matrix3f A; A &lt;&lt; 1, 2, 1, 2, 1, 0, -1, 1, 2; Eigen::Vector3f b; b &lt;&lt; 2, 2, 3; Eigen::Vector3f x = A.colPivHouseholderQr().solve(b); std::cout &lt;&lt; "The solution is:\n" &lt;&lt; x &lt;&lt; std::endl; } JSON for Modern C++ Working with JSON is a common requirement, and this library makes it painless. nlohmann/json A single-header library that provides an intuitive way to work with JSON, mimicking Python’s dictionary and list access. #include &lt;iostream&gt; #include "nlohmann/json.hpp" using json = nlohmann::json; int main() { // Create a JSON object json j; j["service"] = "config-server"; j["port"] = 8080; j["active"] = true; j["protocols"] = {"http", "https"}; // Serialize to string std::cout &lt;&lt; "Serialized JSON: " &lt;&lt; j.dump(4) &lt;&lt; std::endl; // Parse from string auto parsed_json = json::parse(R"({"user": "admin", "id": 123})"); std::cout &lt;&lt; "Parsed user: " &lt;&lt; parsed_json["user"] &lt;&lt; std::endl; } GUI and Visualization Libraries While C++ GUI development can be complex, several excellent libraries cater to different needs, from embedded systems to full-scale desktop applications. Dear ImGui A fast, portable, and self-contained immediate-mode graphical user interface library. It is especially popular for game development tools, 3D applications, and real-time debugging overlays due to its simplicity and high performance. // This is a conceptual example. Full implementation requires a rendering backend (e.g., OpenGL, Vulkan). #include "imgui.h" void RenderMyUI() { ImGui::Begin("System Control"); static int counter = 0; ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); if (ImGui::Button("Increment Counter")) { counter++; } ImGui::SameLine(); ImGui::Text("counter = %d", counter); ImGui::End(); } Other Notable GUI Frameworks: Qt: A mature, comprehensive, cross-platform framework for creating applications with advanced user interfaces. It is more than just a GUI toolkit, providing extensive modules for networking, databases, and more. wxWidgets: A library that allows developers to create applications with a truly native look and feel by using the platform’s native API rather than emulating it. Specialized and Utility Libraries For specific problem domains, these libraries are indispensable. OpenCV (Open Source Computer Vision Library): The premier library for real-time computer vision, machine learning, and image processing. It features thousands of optimized algorithms for tasks like facial recognition, object detection, and 3D model extraction. GoogleTest: A powerful and widely adopted testing framework from Google. It provides an xUnit test framework and a mocking framework, making it a cornerstone of modern C++ development for ensuring code quality. Asio: A cross-platform C++ library for network and low-level I/O programming that provides developers with a consistent asynchronous model. It is part of the Boost project but is also available as a standalone library. Strategic Considerations for Library Selection For architects and senior engineers, choosing a library involves more than just its API. Build System Integration: The C++ ecosystem relies heavily on build systems like CMake and package managers like vcpkg and Conan. A library’s compatibility with these tools is a major factor in its ease of adoption. Modern C++ Compatibility: Ensure that a library aligns with your project’s target C++ standard (e.g., C++17, C++20) and follows modern best practices. Header-Only vs. Compiled: Header-only libraries offer easy integration but can increase compile times. Compiled libraries require linking but can be faster to build against once compiled. Conclusion Mastering modern C++ involves building a curated toolkit of high-quality libraries. The selections presented here represent robust, battle-tested solutions that empower developers to build complex, high-performance systems efficiently. By understanding the trade-offs and strengths of each library, senior engineers and architects can make informed technology decisions that accelerate development, enhance performance, and ensure long-term maintainability.]]></summary></entry></feed>