<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Ai2th</title>
    <description>The latest articles on Forem by Ai2th (@ai2th).</description>
    <link>https://forem.com/ai2th</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3807739%2Fb37c9ef5-0bb3-4436-ba5c-65d468c26e31.png</url>
      <title>Forem: Ai2th</title>
      <link>https://forem.com/ai2th</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ai2th"/>
    <language>en</language>
    <item>
      <title>Linxr | Part 4 — Test Results</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Mon, 09 Mar 2026 17:05:50 +0000</pubDate>
      <link>https://forem.com/ai2th/linxr-part-4-test-results-2fdj</link>
      <guid>https://forem.com/ai2th/linxr-part-4-test-results-2fdj</guid>
      <description>&lt;h1&gt;
  
  
  Test Results
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 4&lt;/strong&gt; of 4 — the final chapter of building Linxr, a single APK that runs Alpine Linux on non-rooted Android.&lt;br&gt;
&lt;a href="https://dev.to/ai2th/linxr-part-3-ssh-terminal-in-flutter-36oe"&gt;← Part 3: SSH Terminal in Flutter&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Download
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/AI2TH/Linxr/releases/latest" rel="noopener noreferrer"&gt;linxr-v1.apk&lt;/a&gt;&lt;/strong&gt; — v1.0 · ARM64 · 63 MB&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Requires Android 8.0+ (API 26), ARM64 device. Enable "Install from unknown sources" before sideloading.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How We Test
&lt;/h2&gt;

&lt;p&gt;Testing on a Mac with an Android emulator doesn't work — QEMU inside an emulator needs nested virtualisation, which isn't available.&lt;/p&gt;

&lt;p&gt;We use &lt;strong&gt;Firebase Test Lab&lt;/strong&gt; with an automated Robo test:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Pixel 2 (arm64)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android version&lt;/td&gt;
&lt;td&gt;11 (API 31)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test type&lt;/td&gt;
&lt;td&gt;Robo (automated UI crawler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;600 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Test Result: Pass ✅
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://ai2th.github.io/linxr.html" rel="noopener noreferrer"&gt;→ Full test results&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No crashes. Key events from the logcat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;01:29:38  VmManager: startVm()
01:29:38  VmManager: Reusing existing user.qcow2
01:29:38  VmManager: VM process launched

01:29:43  QEMU: OpenRC 0.52.1 is starting up Linux 6.6.14-0-virt (aarch64)
01:29:51  QEMU: * eth0 ... [ ok ]
01:29:52  QEMU: * Starting sshd ... [ ok ]
01:29:52  QEMU: Welcome to Alpine Linux 3.19
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From QEMU launch to sshd ready: &lt;strong&gt;~14 seconds&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Robo Test Found
&lt;/h2&gt;

&lt;p&gt;The crawler navigated all three tabs — Home, Terminal, About — and exercised the Start VM / Stop VM flow. It found and read through the full open-source component list in the About screen: dartssh2, xterm, QEMU, Alpine, OpenSSH.&lt;/p&gt;

&lt;p&gt;No crashes. No ANRs. No unexpected states.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Install APK → tap Start VM → Alpine boots in ~14 seconds&lt;/li&gt;
&lt;li&gt;✅ sshd starts automatically; built-in terminal auto-connects&lt;/li&gt;
&lt;li&gt;✅ Persistent overlay — state survives VM restarts&lt;/li&gt;
&lt;li&gt;✅ Internet access — &lt;code&gt;apk add&lt;/code&gt; works inside the VM&lt;/li&gt;
&lt;li&gt;✅ No root. No Termux. No PC required after install.&lt;/li&gt;
&lt;li&gt;✅ Foreground service keeps VM alive when app is backgrounded&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Current Limitations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ARM64 only&lt;/td&gt;
&lt;td&gt;QEMU binary targets &lt;code&gt;aarch64&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password auth&lt;/td&gt;
&lt;td&gt;SSH key setup requires manual &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No GUI&lt;/td&gt;
&lt;td&gt;Text terminal only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APK size 63 MB&lt;/td&gt;
&lt;td&gt;Alpine disk image + QEMU + 50 shared libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] SSH key management from the app UI&lt;/li&gt;
&lt;li&gt;[ ] Configurable RAM and vCPU in settings&lt;/li&gt;
&lt;li&gt;[ ] File transfer (SCP/SFTP) from the app&lt;/li&gt;
&lt;li&gt;[ ] ARM32 / x86_64 support&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Full Series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/ai2th/linxr-part-1-the-idea-and-architecture-2b48"&gt;Part 1: The Idea and Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ai2th/linxr-part-2-shipping-qemu-in-an-apk-4kij"&gt;Part 2: Shipping QEMU in an APK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ai2th/linxr-part-3-ssh-terminal-in-flutter-36oe"&gt;Part 3: SSH Terminal in Flutter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4: Test Results&lt;/strong&gt; ← you are here&lt;/li&gt;
&lt;/ol&gt;







&lt;h2&gt;
  
  
  Linxr Series — Alpine Linux on Android
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Linxr&lt;/strong&gt; = &lt;em&gt;Linux&lt;/em&gt; + &lt;em&gt;r&lt;/em&gt;. A single Android APK that runs a full Alpine Linux shell on any Android phone — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-alpine-linux-on-android-no-root-15la"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Linxr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-1-the-idea-and-architecture-2b48"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-2-shipping-qemu-in-an-apk-4kij"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Shipping QEMU in an APK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-3-ssh-terminal-in-flutter-36oe"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;SSH Terminal in Flutter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-4-test-results-2fdj"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Linxr" rel="noopener noreferrer"&gt;github.com/AI2TH/Linxr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contributor: Kalvin Nathan&lt;/em&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="mailto:skalvinnathan@gmail.com"&gt;skalvinnathan@gmail.com&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/skalvinnathan/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>linux</category>
      <category>testing</category>
      <category>firebase</category>
    </item>
    <item>
      <title>Linxr | Part 3 — SSH Terminal in Flutter</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Mon, 09 Mar 2026 17:03:29 +0000</pubDate>
      <link>https://forem.com/ai2th/linxr-part-3-ssh-terminal-in-flutter-36oe</link>
      <guid>https://forem.com/ai2th/linxr-part-3-ssh-terminal-in-flutter-36oe</guid>
      <description>&lt;h1&gt;
  
  
  SSH Terminal in Flutter
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 3&lt;/strong&gt; of 4 — building Linxr, a single APK that runs Alpine Linux on non-rooted Android.&lt;br&gt;
&lt;a href="https://dev.to/ai2th/linxr-part-2-shipping-qemu-in-an-apk-4kij"&gt;← Part 2: Shipping QEMU in an APK&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;Once Alpine boots and sshd starts, the app needs a usable terminal — without requiring an external SSH client.&lt;/p&gt;

&lt;p&gt;Two Flutter packages make this possible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pub.dev/packages/dartssh2" rel="noopener noreferrer"&gt;&lt;code&gt;dartssh2&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Pure-Dart SSH2 client — connects to the VM's sshd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pub.dev/packages/xterm" rel="noopener noreferrer"&gt;&lt;code&gt;xterm&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Terminal emulator widget — renders VT100/xterm sequences&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both are pure Dart. No platform channels, no native code, no extra Android permissions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connecting to the VM
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SSHSocket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2222&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SSHClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;username:&lt;/span&gt; &lt;span class="s"&gt;'root'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;onPasswordRequest:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;'alpine'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;pty:&lt;/span&gt; &lt;span class="n"&gt;SSHPtyConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;type:&lt;/span&gt; &lt;span class="s"&gt;'xterm-256color'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="n"&gt;_terminal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;viewWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="n"&gt;_terminal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;viewHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SSHPtyConfig&lt;/code&gt; allocates a pseudo-terminal with &lt;code&gt;xterm-256color&lt;/code&gt; — enabling colour output, cursor movement, and correct &lt;code&gt;TERM&lt;/code&gt; variable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wiring SSH to the Terminal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// VM output → terminal display&lt;/span&gt;
&lt;span class="n"&gt;_session&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="na"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_terminal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromCharCodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Keyboard input → SSH session&lt;/span&gt;
&lt;span class="n"&gt;_terminal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_session&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;stdin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;codeUnits&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Terminal resize → SSH PTY resize&lt;/span&gt;
&lt;span class="n"&gt;_terminal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onResize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_session&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;resizeTerminal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resize events keep tools like &lt;code&gt;vim&lt;/code&gt;, &lt;code&gt;htop&lt;/code&gt;, and &lt;code&gt;nano&lt;/code&gt; correctly sized.&lt;/p&gt;




&lt;h2&gt;
  
  
  Auto-Reconnect
&lt;/h2&gt;

&lt;p&gt;Alpine takes ~15 seconds to boot. The terminal handles the window where sshd isn't ready yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;_maxRetries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 24 × 5s = 2 minutes&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_retryOrError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_retryCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_retryCount&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;_maxRetries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_terminal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;$msg&lt;/span&gt;&lt;span class="s"&gt; — retrying in 5s...]&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_scheduleConnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;delaySeconds:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Gave up after &lt;/span&gt;&lt;span class="si"&gt;$_maxRetries&lt;/span&gt;&lt;span class="s"&gt; attempts.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Progress messages appear inline in the terminal buffer — no separate loading spinner.&lt;/p&gt;




&lt;h2&gt;
  
  
  Terminal Theme
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;theme:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;TerminalTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;cursor:&lt;/span&gt;     &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFF20C997&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// teal&lt;/span&gt;
    &lt;span class="nl"&gt;foreground:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFFE0E0E0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// light grey&lt;/span&gt;
    &lt;span class="nl"&gt;background:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFF0E1117&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// near-black&lt;/span&gt;
    &lt;span class="nl"&gt;green:&lt;/span&gt;      &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFF20C997&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;blue:&lt;/span&gt;       &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFF0D6EFD&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;yellow:&lt;/span&gt;     &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFFFFC107&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;red:&lt;/span&gt;        &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFFDC3545&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cursor teal (&lt;code&gt;#20C997&lt;/code&gt;) matches the "Connected" status chip.&lt;/p&gt;




&lt;h2&gt;
  
  
  VM-Not-Running Banner
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vmStatus&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;'running'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_Banner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;icon:&lt;/span&gt; &lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warning_amber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFFFFC107&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;message:&lt;/span&gt; &lt;span class="s"&gt;'VM is not running. Start it from the Home tab.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prevents confusing "Connection refused" errors when the user opens the terminal before starting the VM.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to?"&gt;Part 4 — Test Results&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Linxr" rel="noopener noreferrer"&gt;github.com/AI2TH/Linxr&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Linxr Series — Alpine Linux on Android
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Linxr&lt;/strong&gt; = &lt;em&gt;Linux&lt;/em&gt; + &lt;em&gt;r&lt;/em&gt;. A single Android APK that runs a full Alpine Linux shell on any Android phone — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-alpine-linux-on-android-no-root-15la"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Linxr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-1-the-idea-and-architecture-2b48"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-2-shipping-qemu-in-an-apk-4kij"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Shipping QEMU in an APK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-3-ssh-terminal-in-flutter-36oe"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;SSH Terminal in Flutter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-4-test-results-2fdj"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Linxr" rel="noopener noreferrer"&gt;github.com/AI2TH/Linxr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>ssh</category>
      <category>android</category>
    </item>
    <item>
      <title>Linxr | Part 2 — Shipping QEMU in an APK</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Mon, 09 Mar 2026 17:03:22 +0000</pubDate>
      <link>https://forem.com/ai2th/linxr-part-2-shipping-qemu-in-an-apk-4kij</link>
      <guid>https://forem.com/ai2th/linxr-part-2-shipping-qemu-in-an-apk-4kij</guid>
      <description>&lt;h1&gt;
  
  
  Shipping QEMU in an APK
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 2&lt;/strong&gt; of 4 — building Linxr, a single APK that runs Alpine Linux on non-rooted Android.&lt;br&gt;
&lt;a href="https://dev.to/ai2th/linxr-part-1-the-idea-and-architecture-2b48"&gt;← Part 1: The Idea and Architecture&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Same Wall as Pockr
&lt;/h2&gt;

&lt;p&gt;Getting QEMU to run on non-rooted Android is the same challenge Pockr solved first. If you've read &lt;a href="https://dev.to/ai2th/pockr-part-2-executing-binaries-on-android-3b4k"&gt;Pockr Part 2&lt;/a&gt; and &lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Part 3&lt;/a&gt;, you already know the full story. This article gives the overview as it applies to Linxr.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: SELinux Blocks execve()
&lt;/h2&gt;

&lt;p&gt;On Android 10+, files in app storage have the SELinux label &lt;code&gt;app_data_file&lt;/code&gt;. That label does not allow &lt;code&gt;execve()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot run program ".../files/qemu/qemu-system-aarch64":
error=13, Permission denied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chmod +x&lt;/code&gt; doesn't help — it's a mandatory access control policy, not a Unix permission issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Put QEMU in &lt;code&gt;jniLibs/arm64-v8a/&lt;/code&gt; as a &lt;code&gt;.so&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qemu-system-aarch64  →  jniLibs/arm64-v8a/libqemu.so
qemu-img             →  jniLibs/arm64-v8a/libqemu_img.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Android's &lt;code&gt;PackageManager&lt;/code&gt; installs these to &lt;code&gt;nativeLibraryDir&lt;/code&gt; which has &lt;code&gt;exec_type&lt;/code&gt; SELinux label — &lt;code&gt;execve()&lt;/code&gt; works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Compressed jniLibs
&lt;/h2&gt;

&lt;p&gt;AGP 3.6+ compresses native libraries inside the APK by default — they're loaded directly from the zip, not extracted to disk. QEMU needs to be on disk to execute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix in &lt;code&gt;build.gradle&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;packagingOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;jniLibs&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;useLegacyPackaging&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Problem 3: Wrong ELF Interpreter
&lt;/h2&gt;

&lt;p&gt;A self-built QEMU from a Linux desktop uses glibc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Android's &lt;code&gt;PackageManager&lt;/code&gt; only extracts &lt;code&gt;.so&lt;/code&gt; files using Android's Bionic linker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Requesting program interpreter: /system/bin/linker64]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use Termux's pre-built QEMU packages — compiled against Bionic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 4: 50 Shared Library Dependencies
&lt;/h2&gt;

&lt;p&gt;Termux QEMU dynamically links ~50 shared libraries that live at Termux's prefix — a path that doesn't exist on non-Termux devices. All 50 must be bundled in &lt;code&gt;jniLibs/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two sub-problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Termux embeds a hardcoded &lt;code&gt;RUNPATH&lt;/code&gt;&lt;/strong&gt; pointing to &lt;code&gt;/data/data/com.termux/files/usr/lib/&lt;/code&gt; — Android's linker tries it first and fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;patchelf --remove-rpath&lt;/code&gt; corrupts ELF&lt;/strong&gt; — it restructures LOAD segments, breaking Android 11's strict segment count limits&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; An in-place Python script that zeroes only the &lt;code&gt;d_val&lt;/code&gt; field of &lt;code&gt;DT_RPATH&lt;/code&gt;/&lt;code&gt;DT_RUNPATH&lt;/code&gt; ELF entries, leaving the structure intact. See &lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Pockr Part 3&lt;/a&gt; for the full implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Linxr vs Pockr: Same Libraries
&lt;/h2&gt;

&lt;p&gt;Linxr uses the same 50-library bundle as Pockr. The only difference in the QEMU launch command is the forwarded port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Linxr: SSH only&lt;/span&gt;
&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-netdev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user,id=net0,hostfwd=tcp::2222-:22"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Pockr: HTTP API&lt;/span&gt;
&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-netdev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user,id=net0,hostfwd=tcp::7080-:7080"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/ai2th/linxr-part-3-ssh-terminal-in-flutter-36oe"&gt;Part 3 — SSH Terminal in Flutter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Linxr" rel="noopener noreferrer"&gt;github.com/AI2TH/Linxr&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Linxr Series — Alpine Linux on Android
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Linxr&lt;/strong&gt; = &lt;em&gt;Linux&lt;/em&gt; + &lt;em&gt;r&lt;/em&gt;. A single Android APK that runs a full Alpine Linux shell on any Android phone — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-alpine-linux-on-android-no-root-15la"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Linxr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-1-the-idea-and-architecture-2b48"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-2-shipping-qemu-in-an-apk-4kij"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Shipping QEMU in an APK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-3-ssh-terminal-in-flutter-36oe"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;SSH Terminal in Flutter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-4-test-results-2fdj"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Linxr" rel="noopener noreferrer"&gt;github.com/AI2TH/Linxr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>linux</category>
      <category>kotlin</category>
      <category>security</category>
    </item>
    <item>
      <title>Linxr | Part 1 — The Idea and Architecture</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Mon, 09 Mar 2026 17:01:16 +0000</pubDate>
      <link>https://forem.com/ai2th/linxr-part-1-the-idea-and-architecture-2b48</link>
      <guid>https://forem.com/ai2th/linxr-part-1-the-idea-and-architecture-2b48</guid>
      <description>&lt;h1&gt;
  
  
  The Idea and Architecture
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 1&lt;/strong&gt; of 4 — building Linxr, a single APK that runs Alpine Linux on non-rooted Android.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Constraint
&lt;/h2&gt;

&lt;p&gt;Android is a locked-down Linux system. For regular (non-root) apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;execve()&lt;/code&gt; from app storage is blocked by SELinux W^X policy&lt;/li&gt;
&lt;li&gt;Native Linux tools are unavailable without Termux&lt;/li&gt;
&lt;li&gt;The kernel disables &lt;code&gt;CONFIG_USER_NS&lt;/code&gt; — Docker rootless mode is impossible&lt;/li&gt;
&lt;li&gt;No way to load kernel modules or create network namespaces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A QEMU virtual machine sidesteps all of this. The VM runs its own kernel with its own namespace — completely isolated from Android's restrictions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Alpine?
&lt;/h2&gt;

&lt;p&gt;Alpine Linux is the right guest OS for a mobile VM:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Alpine&lt;/th&gt;
&lt;th&gt;Debian/Ubuntu&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compressed disk image&lt;/td&gt;
&lt;td&gt;~35 MB&lt;/td&gt;
&lt;td&gt;~300 MB+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot time&lt;/td&gt;
&lt;td&gt;~5 s&lt;/td&gt;
&lt;td&gt;~30+ s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM at idle&lt;/td&gt;
&lt;td&gt;~30 MB&lt;/td&gt;
&lt;td&gt;~100 MB+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package manager&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Init system&lt;/td&gt;
&lt;td&gt;OpenRC&lt;/td&gt;
&lt;td&gt;systemd&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Small footprint, fast boot, full POSIX shell — ideal for embedding in an APK.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android OS (non-rooted)
└── APK (Flutter + Kotlin)
    ├── VmManager — asset extraction, QEMU lifecycle
    ├── VmService — ForegroundService (keeps QEMU alive)
    └── Flutter UI — Home, Terminal, About tabs
          └── TerminalScreen (dartssh2 + xterm)

QEMU process (libqemu.so)
└── Alpine Linux 3.19 VM
    └── OpenRC init → sshd on :22
          ↑
          SLIRP hostfwd tcp::2222-:22
          ↑
    SSH on 127.0.0.1:2222 (from Android app)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Disk Layout
&lt;/h2&gt;

&lt;p&gt;Linxr uses QCOW2 copy-on-write layering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;base.qcow2 (read-only — Alpine rootfs, openssh baked in)
  └── user.qcow2 (writable overlay — your data lives here)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;base.qcow2&lt;/code&gt; ships compressed in the APK assets. Extracted once on first install.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user.qcow2&lt;/code&gt; is created fresh on first install. Persists across VM restarts — installed packages and files survive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The overlay is only recreated when &lt;code&gt;base.qcow2&lt;/code&gt; changes (asset version marker bumps on app update).&lt;/p&gt;




&lt;h2&gt;
  
  
  SLIRP Networking
&lt;/h2&gt;

&lt;p&gt;QEMU SLIRP requires no root and no kernel modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android app
  └── TCP to 127.0.0.1:2222
        └── QEMU hostfwd: tcp::2222-:22
              └── Alpine VM eth0 (10.0.2.15):22
                    └── sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eth0:    10.0.2.15/24
gateway: 10.0.2.2
DNS:     10.0.2.3  (SLIRP built-in resolver)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The VM has full outbound internet access. &lt;code&gt;apk add&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt; all work normally.&lt;/p&gt;




&lt;h2&gt;
  
  
  Token-Free Design
&lt;/h2&gt;

&lt;p&gt;Pockr uses a UUID token injected via QEMU kernel cmdline to authenticate its HTTP API. Linxr doesn't need that — SSH handles authentication natively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Username: &lt;code&gt;root&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Password: &lt;code&gt;alpine&lt;/code&gt; (baked into the base image)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No tokens, no custom API — standard SSH.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/ai2th/linxr-part-2-shipping-qemu-in-an-apk-4kij"&gt;Part 2 — Shipping QEMU in an APK&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Linxr" rel="noopener noreferrer"&gt;github.com/AI2TH/Linxr&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Linxr Series — Alpine Linux on Android
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Linxr&lt;/strong&gt; = &lt;em&gt;Linux&lt;/em&gt; + &lt;em&gt;r&lt;/em&gt;. A single Android APK that runs a full Alpine Linux shell on any Android phone — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to%%INTRO%%"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Linxr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to%%P1%%"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-2-shipping-qemu-in-an-apk-4kij"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Shipping QEMU in an APK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-3-ssh-terminal-in-flutter-36oe"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;SSH Terminal in Flutter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-4-test-results-2fdj"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Linxr" rel="noopener noreferrer"&gt;github.com/AI2TH/Linxr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>linux</category>
      <category>qemu</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Linxr — Alpine Linux on Android (No Root)</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Mon, 09 Mar 2026 17:01:14 +0000</pubDate>
      <link>https://forem.com/ai2th/linxr-alpine-linux-on-android-no-root-15la</link>
      <guid>https://forem.com/ai2th/linxr-alpine-linux-on-android-no-root-15la</guid>
      <description>&lt;h2&gt;
  
  
  What is Linxr?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Linxr&lt;/strong&gt; = &lt;em&gt;Linux&lt;/em&gt; + &lt;em&gt;r&lt;/em&gt; (as in: run it).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Linux in your pocket.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Linxr is a single Android APK that runs a full Alpine Linux environment on any non-rooted Android phone. No Termux. No root. No PC required after install.&lt;/p&gt;

&lt;p&gt;Install it, tap &lt;strong&gt;Start VM&lt;/strong&gt;, wait ~15 seconds, and you have a live Linux shell — from the built-in terminal tab or any SSH client on the same device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Linxr" rel="noopener noreferrer"&gt;github.com/AI2TH/Linxr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Linxr/releases/latest" rel="noopener noreferrer"&gt;linxr-v1.apk&lt;/a&gt; — ARM64 · 63 MB&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Does This Exist?
&lt;/h2&gt;

&lt;p&gt;Modern Android phones are powerful Linux machines underneath — 8 cores, 8 GB RAM, a full Linux 6.x kernel. But the platform deliberately limits what regular apps can do. Running native Linux tools requires either rooting the device or a runtime like Termux.&lt;/p&gt;

&lt;p&gt;Linxr explores how far a normal, unmodified APK can go using only standard Android APIs. The answer: far enough for a full Alpine Linux VM with SSH access, persistent storage, and internet — in a 63 MB APK.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alpine Linux 3.19&lt;/td&gt;
&lt;td&gt;bash, sudo, apk package manager, openssl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in SSH terminal&lt;/td&gt;
&lt;td&gt;Auto-connects when VM starts; auto-retries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External SSH&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ssh root@localhost -p 2222&lt;/code&gt; from any SSH client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistent storage&lt;/td&gt;
&lt;td&gt;Installed packages survive VM restarts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internet access&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;apk add curl git python3&lt;/code&gt; just works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No root&lt;/td&gt;
&lt;td&gt;QEMU runs as a regular app process&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Default credentials: &lt;code&gt;root&lt;/code&gt; / &lt;code&gt;alpine&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flutter UI (Dart)
  └── MethodChannel
        └── Kotlin VmManager
              └── QEMU (libqemu.so — ships inside APK jniLibs)
                    └── Alpine Linux 3.19 VM (QCOW2 disk image)
                          └── OpenSSH sshd (port 22)
                                ↑
                    SLIRP hostfwd: Android :2222 → VM :22
                                ↑
              SSH client (dartssh2 + xterm — Flutter)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How It Compares to Pockr
&lt;/h2&gt;

&lt;p&gt;Linxr is the sibling of &lt;a href="https://dev.to/ai2th/series/36300"&gt;Pockr&lt;/a&gt; — which runs Docker on Android. Linxr strips that down to the essentials:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Linxr&lt;/th&gt;
&lt;th&gt;Pockr&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Guest runtime&lt;/td&gt;
&lt;td&gt;OpenSSH&lt;/td&gt;
&lt;td&gt;Docker daemon + FastAPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access method&lt;/td&gt;
&lt;td&gt;SSH terminal&lt;/td&gt;
&lt;td&gt;REST API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APK size&lt;/td&gt;
&lt;td&gt;63 MB&lt;/td&gt;
&lt;td&gt;163 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot time&lt;/td&gt;
&lt;td&gt;~15 s&lt;/td&gt;
&lt;td&gt;~60 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First-run setup&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;~5 min (Docker install)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you just need a Linux shell, Linxr is the right tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Name
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Linxr&lt;/strong&gt; = &lt;strong&gt;Linux&lt;/strong&gt; with a dropped vowel. Same naming pattern as Pockr — short, memorable, double meaning. The goal was always &lt;em&gt;"install this APK, get Linux"&lt;/em&gt; — no configuration, no server, no extra steps.&lt;/p&gt;







&lt;h2&gt;
  
  
  Linxr Series — Alpine Linux on Android
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Linxr&lt;/strong&gt; = &lt;em&gt;Linux&lt;/em&gt; + &lt;em&gt;r&lt;/em&gt;. A single Android APK that runs a full Alpine Linux shell on any Android phone — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to%%INTRO%%"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Linxr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to%%P1%%"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-2-shipping-qemu-in-an-apk-4kij"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Shipping QEMU in an APK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-3-ssh-terminal-in-flutter-36oe"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;SSH Terminal in Flutter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/linxr-part-4-test-results-2fdj"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Linxr" rel="noopener noreferrer"&gt;github.com/AI2TH/Linxr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Idea &amp;amp; Contributor: Kalvin Nathan&lt;/em&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="mailto:skalvinnathan@gmail.com"&gt;skalvinnathan@gmail.com&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/skalvinnathan/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>linux</category>
      <category>qemu</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Pockr | Part 6 — Test Results and What's Next</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Thu, 05 Mar 2026 13:27:14 +0000</pubDate>
      <link>https://forem.com/ai2th/pockr-part-6-test-results-and-whats-next-8m5</link>
      <guid>https://forem.com/ai2th/pockr-part-6-test-results-and-whats-next-8m5</guid>
      <description>&lt;h1&gt;
  
  
  Test Results, Firebase Test Lab, and What's Next for Pockr
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 6&lt;/strong&gt; of 6 — the final chapter of building Pockr, a single APK that runs Docker on non-rooted Android.&lt;br&gt;
&lt;a href="https://dev.to/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22"&gt;← Part 5: Debugging a VM Restart Loop&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Download
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/AI2TH/Pockr/releases/download/v1.0.0/pockr-v1.apk" rel="noopener noreferrer"&gt;pockr-v1.apk&lt;/a&gt;&lt;/strong&gt; — v1.0.0 · ARM64 · 163 MB&lt;br&gt;
&lt;a href="https://github.com/AI2TH/Pockr/releases/tag/v1.0.0" rel="noopener noreferrer"&gt;Release notes&lt;/a&gt; · &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Requires Android 9+ (API 28), ARM64 device. Enable "Install from unknown sources" before installing.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  How We Test: Firebase Test Lab
&lt;/h2&gt;

&lt;p&gt;Physical device testing on a Mac with an Android emulator is unreliable for this project — QEMU needs hardware virtualisation, and nested virtualisation in emulators doesn't work.&lt;/p&gt;

&lt;p&gt;Instead, every build gets tested on &lt;strong&gt;Firebase Test Lab&lt;/strong&gt; with a Robo test:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Pixel 2 (arm64)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android version&lt;/td&gt;
&lt;td&gt;11 (API 30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test type&lt;/td&gt;
&lt;td&gt;Robo (automated UI crawler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;600 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;ARM64 (same as real phones)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Robo crawler explores the UI systematically — tapping every button it finds, navigating every screen, filling forms. It's brutal but effective at finding race conditions and edge cases that manual testing misses.&lt;/p&gt;


&lt;h2&gt;
  
  
  Test History
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;What Changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v11–v20&lt;/td&gt;
&lt;td&gt;❌ Various crashes&lt;/td&gt;
&lt;td&gt;ELF/linker issues, asset extraction bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v22&lt;/td&gt;
&lt;td&gt;✅ Pass&lt;/td&gt;
&lt;td&gt;VmManager as Application singleton — QEMU survives Activity relaunch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v23&lt;/td&gt;
&lt;td&gt;✅ Pass&lt;/td&gt;
&lt;td&gt;DNS fix (&lt;code&gt;use-vc&lt;/code&gt; + 8.8.8.8 fallback) — Docker Hub reachable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v24&lt;/td&gt;
&lt;td&gt;✅ Pass&lt;/td&gt;
&lt;td&gt;Flutter &lt;code&gt;ScaffoldMessenger&lt;/code&gt; context fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v25&lt;/td&gt;
&lt;td&gt;✅ Pass&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;First container pull + run&lt;/strong&gt; — busybox and nginx from Docker Hub (~78s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v28&lt;/td&gt;
&lt;td&gt;✅ Pass&lt;/td&gt;
&lt;td&gt;Release APK + Pockr branding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v30&lt;/td&gt;
&lt;td&gt;✅ Pass&lt;/td&gt;
&lt;td&gt;Corrected APK filename in test script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v33&lt;/td&gt;
&lt;td&gt;✅ Pass&lt;/td&gt;
&lt;td&gt;Race condition fix in &lt;code&gt;VmManager.getStatus()&lt;/code&gt; — no more restart loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v34&lt;/td&gt;
&lt;td&gt;❌ Fail&lt;/td&gt;
&lt;td&gt;Double-start found — Robo tapped Start during &lt;code&gt;starting→running&lt;/code&gt; transition, launching a second QEMU instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v35&lt;/td&gt;
&lt;td&gt;✅ Pass&lt;/td&gt;
&lt;td&gt;Double-start guard in &lt;code&gt;VmState.startVm()&lt;/code&gt; — confirmed no double-start, containers running&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://ai2th.github.io/pockr.html" rel="noopener noreferrer"&gt;→ Full test results&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What a Passing Test Looks Like
&lt;/h2&gt;

&lt;p&gt;From the v35 logcat (second boot — Docker image cache already warm):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;03:38:02  VmManager: Starting VM...
03:38:02  VmManager: Reusing existing user.qcow2 (Docker image cache preserved)
03:38:02  VmManager: VM process launched

         ... Alpine second boot: ~50 seconds ...
         ... API server comes up ...

03:38:53  VmApiClient: Container started: alpine_1772710722409  ✅
03:39:28  VmApiClient: Container started: test_1772710762640    ✅
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From cold install (first boot, Docker + Python setup):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;03:35:33  VmManager: Starting VM...
03:35:33  VmManager: Extracted vm/base.qcow2 (fresh install)
03:35:33  VmManager: VM process launched

         ... Alpine first boot: ~2–5 minutes ...
         ... Docker installs, API server bootstraps ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From cold install to running container: &lt;strong&gt;~5 minutes&lt;/strong&gt;.&lt;br&gt;
Second boot (Docker image cache warm): &lt;strong&gt;~50 seconds&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Works Today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Install APK → tap Start → QEMU boots Alpine Linux&lt;/li&gt;
&lt;li&gt;✅ Docker pulls images from Docker Hub over SLIRP networking&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;busybox&lt;/code&gt;, &lt;code&gt;alpine&lt;/code&gt;, &lt;code&gt;nginx&lt;/code&gt; confirmed running on real hardware (ARM64, Android 11)&lt;/li&gt;
&lt;li&gt;✅ Persistent overlay disk — pulled images survive VM restarts&lt;/li&gt;
&lt;li&gt;✅ No root. No Termux. No PC required after install.&lt;/li&gt;
&lt;li&gt;✅ Foreground service keeps VM alive when app is backgrounded&lt;/li&gt;
&lt;li&gt;✅ Auto-start option to launch VM on app open&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Current Limitations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First boot ~5 min&lt;/td&gt;
&lt;td&gt;Docker + Python bootstrap on Alpine; subsequent boots ~50s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARM64 only&lt;/td&gt;
&lt;td&gt;QEMU binary targets &lt;code&gt;aarch64&lt;/code&gt; only (no ARM32/x86)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage driver: &lt;code&gt;vfs&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Less efficient than &lt;code&gt;overlay2&lt;/code&gt;, but works without kernel modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--network host&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;No isolated container networks without bridge module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APK size ~163 MB&lt;/td&gt;
&lt;td&gt;Includes full Alpine disk image + QEMU binary + 50 libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Stop container / view logs in app UI&lt;/li&gt;
&lt;li&gt;[ ] ARM32 device support&lt;/li&gt;
&lt;li&gt;[ ] Reduce first-boot time&lt;/li&gt;
&lt;li&gt;[ ] Custom container image support from UI&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Full Series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/ai2th/pockr-part-1-the-idea-and-architecture-h80"&gt;Part 1: The Idea and Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ai2th/pockr-part-2-executing-binaries-on-android-3b4k"&gt;Part 2: Executing Binaries on Android — The SELinux Problem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Part 3: Bundling 50 Native Libraries Without Breaking the Android Linker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ai2th/pockr-part-4-docker-without-kernel-modules-232m"&gt;Part 4: Making Docker Run Without iptables, bridge, or overlay2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22"&gt;Part 5: Debugging a VM Restart Loop — A Race Condition in Kotlin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 6: Test Results and What's Next&lt;/strong&gt; ← you are here&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Pockr Series — Docker in Your Pocket
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pockr&lt;/strong&gt; = &lt;em&gt;Pocket&lt;/em&gt; + &lt;em&gt;Docker&lt;/em&gt;. A single Android APK that runs real Docker containers in your pocket — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-running-docker-on-a-non-rooted-android-phone-4g01"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Pockr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-1-the-idea-and-architecture-h80"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-2-executing-binaries-on-android-3b4k"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Executing Binaries — The SELinux Problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bundling 50 Native Libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-4-docker-without-kernel-modules-232m"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Docker Without Kernel Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22"&gt;Part 5&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Debugging the VM Restart Loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-6-test-results-and-whats-next-8m5"&gt;Part 6&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results and What's Next&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Contributor: Kalvin Nathan&lt;/em&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="mailto:skalvinnathan@gmail.com"&gt;skalvinnathan@gmail.com&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/skalvinnathan/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>docker</category>
      <category>testing</category>
      <category>firebase</category>
    </item>
    <item>
      <title>Pockr | Part 5 — Debugging the VM Restart Loop</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Thu, 05 Mar 2026 13:26:35 +0000</pubDate>
      <link>https://forem.com/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22</link>
      <guid>https://forem.com/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22</guid>
      <description>&lt;h1&gt;
  
  
  A Race Condition in Kotlin
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 5&lt;/strong&gt; of 6 — building Pockr, a single APK that runs Docker on non-rooted Android.&lt;br&gt;
&lt;a href="https://dev.to/ai2th/pockr-part-4-docker-without-kernel-modules-232m"&gt;← Part 4: Making Docker Run Without Kernel Modules&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Symptom
&lt;/h2&gt;

&lt;p&gt;Alpine Linux takes ~5 minutes on first boot to set up Docker, install Python packages, and start the API server. During that time, the app shows a loading state.&lt;/p&gt;

&lt;p&gt;On real device testing (Firebase Test Lab), we kept seeing a cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VM starts → health check fails at ~35s → VM restarts → repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alpine was never given enough time to finish first-boot setup. Docker Hub pulls never happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading the Logcat
&lt;/h2&gt;

&lt;p&gt;The logcat told the story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VmManager: Starting VM...
VmManager: VM process launched
VmApiClient: Health check failed: timeout          ← 35s in
VmManager: Starting VM...                          ← restarted!
VmManager: Stopping existing VM before restart     ← killed Alpine mid-boot
VmManager: VM process launched
VmApiClient: Health check failed: timeout          ← again
VmManager: Starting VM...                          ← restarted again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The VM was restarting every ~35 seconds — exactly the health check timeout. But why? Nobody was tapping Stop.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Race Condition
&lt;/h2&gt;

&lt;p&gt;Here's &lt;code&gt;getStatus()&lt;/code&gt; before the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"stopped"&lt;/span&gt;   &lt;span class="c1"&gt;// ← THE BUG&lt;/span&gt;
    &lt;span class="n"&gt;vmProcess&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exitValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;isRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
            &lt;span class="s"&gt;"stopped"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IllegalThreadStateException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"running"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"stopped"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;startVm()&lt;/code&gt; does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Synchronized&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;startVm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Stop existing VM before restart&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunning&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;vmProcess&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;stopVm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// kills QEMU&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... launches new QEMU&lt;/span&gt;
    &lt;span class="n"&gt;isRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do you see the window? Inside &lt;code&gt;startVm()&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;stopVm()&lt;/code&gt; is called → sets &lt;code&gt;isRunning = false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;New QEMU not yet launched → &lt;code&gt;vmProcess = null&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;During this window&lt;/strong&gt;, &lt;code&gt;getStatus()&lt;/code&gt; is called by the Flutter health poll&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;!isRunning&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; → returns &lt;code&gt;"stopped"&lt;/code&gt; — even though we're mid-restart&lt;/li&gt;
&lt;li&gt;Flutter sets &lt;code&gt;_status = "stopped"&lt;/code&gt; → Start Engine button re-enables&lt;/li&gt;
&lt;li&gt;User or Robo taps Start Engine → calls &lt;code&gt;startVm()&lt;/code&gt; AGAIN → kills Alpine mid-boot&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Check the &lt;strong&gt;live process&lt;/strong&gt; first, not the boolean flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Check the process directly first — isRunning can be briefly false&lt;/span&gt;
    &lt;span class="c1"&gt;// during the startVm() window between stopVm() and isRunning=true&lt;/span&gt;
    &lt;span class="n"&gt;vmProcess&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exitValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// Process actually exited — clean up&lt;/span&gt;
            &lt;span class="n"&gt;isRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
            &lt;span class="n"&gt;vmProcess&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
            &lt;span class="s"&gt;"stopped"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IllegalThreadStateException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"running"&lt;/span&gt;   &lt;span class="c1"&gt;// Process is alive&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"stopped"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;vmProcess&lt;/code&gt; is non-null, we check whether the process is actually alive (&lt;code&gt;exitValue()&lt;/code&gt; throws if still running). Only if the process has genuinely exited do we return &lt;code&gt;"stopped"&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Second Guard in Flutter
&lt;/h2&gt;

&lt;p&gt;We also added a guard in the Dart layer to prevent double-starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;startVm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'running'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;_status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'starting'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// guard&lt;/span&gt;
    &lt;span class="n"&gt;_isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'starting'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if something pushes a second &lt;code&gt;startVm()&lt;/code&gt; call, it no-ops if the VM is already in motion.&lt;/p&gt;




&lt;h2&gt;
  
  
  Confirmed Fixed
&lt;/h2&gt;

&lt;p&gt;Firebase Test Lab v33 logcat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VmManager: Starting VM...           ← first boot
VmManager: VM process launched
VmApiClient: Health check failed: Connection reset   ← still booting, normal
... (2 minutes of booting) ...
VmApiClient: Container started: alpine_1772702979346  ← SUCCESS ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No restart loop. Alpine booted fully. Container started from Docker Hub.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Boolean flags lie during state transitions.&lt;/strong&gt; Check the real source of truth — the OS process handle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log everything.&lt;/strong&gt; Logcat timestamps and thread IDs revealed the exact 3ms window where the race occurred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robo testing is brutal.&lt;/strong&gt; Firebase's Robo crawler taps every visible button, exposing race conditions that manual testing misses.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/ai2th/pockr-part-6-test-results-and-whats-next-8m5"&gt;Part 6 — Test Results and What's Next&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Pockr Series — Docker in Your Pocket
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pockr&lt;/strong&gt; = &lt;em&gt;Pocket&lt;/em&gt; + &lt;em&gt;Docker&lt;/em&gt;. A single Android APK that runs real Docker containers in your pocket — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-running-docker-on-a-non-rooted-android-phone-4g01"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Pockr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-1-the-idea-and-architecture-h80"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-2-executing-binaries-on-android-3b4k"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Executing Binaries — The SELinux Problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bundling 50 Native Libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-4-docker-without-kernel-modules-232m"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Docker Without Kernel Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22"&gt;Part 5&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Debugging the VM Restart Loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-6-test-results-and-whats-next-8m5"&gt;Part 6&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results and What's Next&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>debugging</category>
      <category>flutter</category>
    </item>
    <item>
      <title>Pockr | Part 4 — Docker Without Kernel Modules</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Thu, 05 Mar 2026 13:26:02 +0000</pubDate>
      <link>https://forem.com/ai2th/pockr-part-4-docker-without-kernel-modules-232m</link>
      <guid>https://forem.com/ai2th/pockr-part-4-docker-without-kernel-modules-232m</guid>
      <description>&lt;h1&gt;
  
  
  Docker Run Without iptables, bridge, or overlay2
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 4&lt;/strong&gt; of 6 — building Pockr, a single APK that runs Docker on non-rooted Android.&lt;br&gt;
&lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;← Part 3: Bundling 50 Native Libraries&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Docker Starts — Then Immediately Fails
&lt;/h2&gt;

&lt;p&gt;Once QEMU was running and Alpine booted, we launched Docker. It started, then failed silently. Containers wouldn't run. No useful error message.&lt;/p&gt;

&lt;p&gt;The root cause: Alpine 3.19's Docker daemon defaults assume a full Linux kernel. Our QEMU kernel (&lt;code&gt;6.6.14-0-virt&lt;/code&gt;) is stripped — it doesn't load &lt;code&gt;iptables&lt;/code&gt;, &lt;code&gt;bridge&lt;/code&gt;, &lt;code&gt;overlay2&lt;/code&gt;, or &lt;code&gt;ip_masq&lt;/code&gt; as kernel modules because there's no &lt;code&gt;/lib/modules&lt;/code&gt; in the disk image.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Missing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature Docker Expects&lt;/th&gt;
&lt;th&gt;Kernel Module&lt;/th&gt;
&lt;th&gt;Status in Our VM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Network filtering&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;iptables&lt;/code&gt; / &lt;code&gt;nf_tables&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌ Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container networking&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bridge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Efficient storage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overlay2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAT/masquerade&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip_masq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Docker's default config tries to set up all of these on startup. Every one fails with &lt;code&gt;operation not supported&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Minimal Docker Config
&lt;/h2&gt;

&lt;p&gt;We bake a custom &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; into the Alpine base image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iptables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bridge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8.8.8.8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8.8.4.4"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip-masq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userland-proxy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"storage-driver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vfs"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iptables: false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alpine's iptables uses nft backend — module unavailable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bridge: none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bridge kernel module unavailable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip-masq: false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No masquerading needed with host networking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;userland-proxy: false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Avoids port proxy process startup failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;storage-driver: vfs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;overlay2 needs kernel module; vfs always works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Container Networking: &lt;code&gt;--network host&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;With no bridge, containers run with &lt;code&gt;--network host&lt;/code&gt; — they share the VM's network stack directly.&lt;/p&gt;

&lt;p&gt;The VM's network is QEMU SLIRP (user-mode networking). SLIRP gives the guest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An eth0 at &lt;code&gt;10.0.2.15&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Default gateway at &lt;code&gt;10.0.2.2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;DNS proxy at &lt;code&gt;10.0.2.3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Port forwarding from Android host to guest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Containers inherit all of this. They can reach the internet, pull images from Docker Hub, and serve traffic on forwarded ports.&lt;/p&gt;




&lt;h2&gt;
  
  
  The DNS Trap
&lt;/h2&gt;

&lt;p&gt;QEMU's built-in DNS proxy at &lt;code&gt;10.0.2.3&lt;/code&gt; uses UDP. On real Android hardware (confirmed on Firebase Test Lab Pixel2.arm), UDP DNS is unreliable — Docker Hub pulls fail intermittently with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error response from daemon: Get "https://registry-1.docker.io/v2/":
dial tcp: lookup registry-1.docker.io: i/o timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix in &lt;code&gt;/etc/resolv.conf&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;nameserver&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;2&lt;/span&gt;.&lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;nameserver&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;.&lt;span class="m"&gt;8&lt;/span&gt;.&lt;span class="m"&gt;8&lt;/span&gt;.&lt;span class="m"&gt;8&lt;/span&gt;
&lt;span class="n"&gt;nameserver&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;.&lt;span class="m"&gt;8&lt;/span&gt;.&lt;span class="m"&gt;4&lt;/span&gt;.&lt;span class="m"&gt;4&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;:&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;:&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt;-&lt;span class="n"&gt;vc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;use-vc&lt;/code&gt; forces all DNS over &lt;strong&gt;TCP&lt;/strong&gt;. With multiple fallback nameservers, Docker Hub pulls work reliably — confirmed with nginx (~78 seconds on Pixel2.arm, Android 11).&lt;/p&gt;




&lt;h2&gt;
  
  
  Storage: vfs vs overlay2
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;vfs&lt;/code&gt; is not space-efficient (it copies layers instead of linking them), but it works without kernel modules. For our use case — a VM with an 8 GB overlay disk — it's perfectly acceptable.&lt;/p&gt;

&lt;p&gt;Pulled images are stored in the writable &lt;code&gt;user.qcow2&lt;/code&gt; overlay and survive VM restarts. Once &lt;code&gt;nginx&lt;/code&gt; is pulled, subsequent boots start it instantly with no re-download.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22"&gt;Part 5 — Debugging a VM Restart Loop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Pockr Series — Docker in Your Pocket
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pockr&lt;/strong&gt; = &lt;em&gt;Pocket&lt;/em&gt; + &lt;em&gt;Docker&lt;/em&gt;. A single Android APK that runs real Docker containers in your pocket — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-running-docker-on-a-non-rooted-android-phone-4g01"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Pockr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-1-the-idea-and-architecture-h80"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-2-executing-binaries-on-android-3b4k"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Executing Binaries — The SELinux Problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bundling 50 Native Libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-4-docker-without-kernel-modules-232m"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Docker Without Kernel Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22"&gt;Part 5&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Debugging the VM Restart Loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-6-test-results-and-whats-next-8m5"&gt;Part 6&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results and What's Next&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>linux</category>
      <category>android</category>
      <category>networking</category>
    </item>
    <item>
      <title>Pockr | Part 3 — Bundling 50 Native Libraries</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Thu, 05 Mar 2026 13:25:32 +0000</pubDate>
      <link>https://forem.com/ai2th/pockr-part-3-bundling-50-native-libraries-5chh</link>
      <guid>https://forem.com/ai2th/pockr-part-3-bundling-50-native-libraries-5chh</guid>
      <description>&lt;h1&gt;
  
  
  Libraries Without Breaking the Android Linker
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 3&lt;/strong&gt; of 6 — building Pockr, a single APK that runs Docker on non-rooted Android.&lt;br&gt;
&lt;a href="https://dev.to/ai2th/pockr-part-2-executing-binaries-on-android-3b4k"&gt;← Part 2: Executing Binaries on Android&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  QEMU Needs Friends
&lt;/h2&gt;

&lt;p&gt;QEMU from Termux doesn't link statically. It depends on a chain of ~50 shared libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;libqemu.so
  ├── libcurl.so → libssl.so, libcrypto.so, libssh.so, libgnutls.so ...
  ├── libglib-2.0.so → libpcre2-8.so, libffi.so, libandroid-support.so ...
  ├── libbz2.so
  └── libzstd.so
      ... (44 more)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of these are bundled in Termux's own prefix at &lt;code&gt;/data/data/com.termux/files/usr/lib/&lt;/code&gt; — which doesn't exist on devices without Termux installed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: RUNPATH Points to Nowhere
&lt;/h2&gt;

&lt;p&gt;Every Termux binary has a hardcoded &lt;code&gt;RUNPATH&lt;/code&gt; in its ELF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;readelf &lt;span class="nt"&gt;-d&lt;/span&gt; libqemu.so | &lt;span class="nb"&gt;grep &lt;/span&gt;RUNPATH
  &lt;span class="o"&gt;(&lt;/span&gt;RUNPATH&lt;span class="o"&gt;)&lt;/span&gt; Library runpath: &lt;span class="o"&gt;[&lt;/span&gt;/data/data/com.termux/files/usr/lib]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Android's dynamic linker tries this path first. It doesn't exist → symbols not found → crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Zero out the &lt;code&gt;RUNPATH&lt;/code&gt; in every &lt;code&gt;.so&lt;/code&gt; file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not patchelf?
&lt;/h2&gt;

&lt;p&gt;The obvious tool is &lt;code&gt;patchelf --remove-rpath&lt;/code&gt;. We tried it. It works on a PC, but the resulting &lt;code&gt;.so&lt;/code&gt; files crash Android's linker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;bionic&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;linker&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;linker_phdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpp&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;168&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Load&lt;/span&gt; &lt;span class="n"&gt;CHECK&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;did_read_&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;patchelf&lt;/code&gt; restructures ELF LOAD segments when it modifies the file — sometimes creating 5 to 50 segments per library. Android 11's linker has strict limits on LOAD segment count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; An in-place Python script that zeroes only the &lt;code&gt;d_val&lt;/code&gt; field of &lt;code&gt;DT_RUNPATH&lt;/code&gt;/&lt;code&gt;DT_RPATH&lt;/code&gt; entries, leaving the ELF structure completely intact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r+b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="c1"&gt;# Parse ELF header, find .dynamic section
&lt;/span&gt;    &lt;span class="c1"&gt;# For each DT_RPATH/DT_RUNPATH entry:
&lt;/span&gt;    &lt;span class="c1"&gt;#   zero out d_val only — leave d_tag intact
&lt;/span&gt;    &lt;span class="c1"&gt;# Write back
&lt;/span&gt;    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preserves every byte of the ELF except the path string offset — exactly what the linker needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Versioned Sonames
&lt;/h2&gt;

&lt;p&gt;Termux packages use versioned filenames (&lt;code&gt;libzstd.so.1.5.7&lt;/code&gt;) with sonames like &lt;code&gt;libzstd.so.1&lt;/code&gt;. Android's linker needs exact filename matches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Rename every library to its base soname and update the soname field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before&lt;/span&gt;
libzstd.so.1.5.7  &lt;span class="o"&gt;(&lt;/span&gt;soname: libzstd.so.1&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# After&lt;/span&gt;
libzstd.so  &lt;span class="o"&gt;(&lt;/span&gt;soname: libzstd.so&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Problem 3: Namespace Isolation
&lt;/h2&gt;

&lt;p&gt;Android 7+ enforces linker namespace isolation. A library loaded transitively (e.g. &lt;code&gt;libgnutls.so&lt;/code&gt; via &lt;code&gt;libcurl.so&lt;/code&gt;) is NOT visible to other libraries unless they have a direct &lt;code&gt;DT_NEEDED&lt;/code&gt; entry.&lt;/p&gt;

&lt;p&gt;This caused errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cannot locate symbol "gnutls_cipher_init"
referenced by libqemu_img.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though &lt;code&gt;libgnutls.so&lt;/code&gt; was present and loaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Add explicit &lt;code&gt;DT_NEEDED&lt;/code&gt; entries for the minimum set of libs that provide undefined symbols — again using the in-place approach, not patchelf.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Final Set: 50 Libraries
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Libraries&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TLS / Crypto&lt;/td&gt;
&lt;td&gt;libssl, libcrypto, libgnutls, libnettle, libhogweed, libtasn1, libgmp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH&lt;/td&gt;
&lt;td&gt;libssh, libssh2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/2-3&lt;/td&gt;
&lt;td&gt;libcurl, libnghttp2, libnghttp3, libngtcp2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression&lt;/td&gt;
&lt;td&gt;libzstd, libbz2, libz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLib&lt;/td&gt;
&lt;td&gt;libglib-2.0, libffi, libpcre2-8, libiconv&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android compat&lt;/td&gt;
&lt;td&gt;libandroid-support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unicode&lt;/td&gt;
&lt;td&gt;libidn2, libunistring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Events&lt;/td&gt;
&lt;td&gt;libevent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;27 more&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Key Rule
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never use &lt;code&gt;patchelf&lt;/code&gt; on these libraries.&lt;/strong&gt; The in-place Python script is the only safe modification method. &lt;code&gt;patchelf&lt;/code&gt; restructures ELF LOAD segments and breaks Android 11's linker.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/ai2th/pockr-part-4-docker-without-kernel-modules-232m"&gt;Part 4 — Making Docker Run Without Kernel Modules&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Pockr Series — Docker in Your Pocket
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pockr&lt;/strong&gt; = &lt;em&gt;Pocket&lt;/em&gt; + &lt;em&gt;Docker&lt;/em&gt;. A single Android APK that runs real Docker containers in your pocket — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-running-docker-on-a-non-rooted-android-phone-4g01"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Pockr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-1-the-idea-and-architecture-h80"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-2-executing-binaries-on-android-3b4k"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Executing Binaries — The SELinux Problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bundling 50 Native Libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-4-docker-without-kernel-modules-232m"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Docker Without Kernel Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22"&gt;Part 5&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Debugging the VM Restart Loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-6-test-results-and-whats-next-8m5"&gt;Part 6&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results and What's Next&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>linux</category>
      <category>cpp</category>
      <category>programming</category>
    </item>
    <item>
      <title>Pockr | Part 2 — Executing Binaries on Android</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Thu, 05 Mar 2026 13:24:45 +0000</pubDate>
      <link>https://forem.com/ai2th/pockr-part-2-executing-binaries-on-android-3b4k</link>
      <guid>https://forem.com/ai2th/pockr-part-2-executing-binaries-on-android-3b4k</guid>
      <description>&lt;h1&gt;
  
  
  Executing Binaries on Android — The SELinux Problem
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 2&lt;/strong&gt; of 6 — building Pockr, a single APK that runs Docker on non-rooted Android.&lt;br&gt;
&lt;a href="https://dev.to/ai2th/pockr-part-1-the-idea-and-architecture-h80"&gt;← Part 1: The Idea and Architecture&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The First Wall: Permission Denied
&lt;/h2&gt;

&lt;p&gt;The most obvious approach is to bundle the QEMU binary inside the APK and extract it to app storage on first launch, then execute it with &lt;code&gt;ProcessBuilder&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On Android 10+, this silently fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot run program ".../files/qemu/qemu-system-aarch64":
error=13, Permission denied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a file permission issue. &lt;code&gt;chmod +x&lt;/code&gt; won't fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why: SELinux W^X Policy
&lt;/h2&gt;

&lt;p&gt;Android 10 enforces a &lt;strong&gt;W^X (Write XOR Execute)&lt;/strong&gt; policy via SELinux. Any file in &lt;code&gt;getFilesDir()&lt;/code&gt; — the app's private data directory — is labelled &lt;code&gt;app_data_file&lt;/code&gt;. That label does not allow &lt;code&gt;execve()&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directory&lt;/th&gt;
&lt;th&gt;SELinux Label&lt;/th&gt;
&lt;th&gt;Executable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getFilesDir()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app_data_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getCacheDir()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app_data_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nativeLibraryDir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exec_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The native library directory is the exception — it's specifically labelled to allow execution. This is where Android puts &lt;code&gt;.so&lt;/code&gt; files from your APK's &lt;code&gt;jniLibs/&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Ship QEMU as a &lt;code&gt;.so&lt;/code&gt; File
&lt;/h2&gt;

&lt;p&gt;Android's PackageManager extracts files from &lt;code&gt;jniLibs/&amp;lt;abi&amp;gt;/&lt;/code&gt; to &lt;code&gt;nativeLibraryDir&lt;/code&gt; during installation. Those files &lt;strong&gt;must&lt;/strong&gt; end in &lt;code&gt;.so&lt;/code&gt;, but they don't have to be shared libraries.&lt;/p&gt;

&lt;p&gt;We renamed the QEMU binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qemu-system-aarch64  →  libqemu.so
qemu-img             →  libqemu_img.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put them in &lt;code&gt;android/app/src/main/jniLibs/arm64-v8a/&lt;/code&gt; and Android extracts them to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/data/app/~~.../com.example.dockerapp-.../lib/arm64/libqemu.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This path has &lt;code&gt;exec_type&lt;/code&gt;. &lt;code&gt;execve()&lt;/code&gt; works.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Second Problem: Wrong ELF Interpreter
&lt;/h2&gt;

&lt;p&gt;Our first attempt used a self-built QEMU binary compiled on Debian. Android installed it to &lt;code&gt;nativeLibraryDir&lt;/code&gt; but silently refused to extract it.&lt;/p&gt;

&lt;p&gt;The reason: Android's PackageManager only extracts &lt;code&gt;.so&lt;/code&gt; files with the Android linker as interpreter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian-built QEMU (rejected)&lt;/span&gt;
readelf &lt;span class="nt"&gt;-l&lt;/span&gt; qemu | &lt;span class="nb"&gt;grep &lt;/span&gt;interpreter
  &lt;span class="o"&gt;[&lt;/span&gt;Requesting program interpreter: /lib/ld-linux-aarch64.so.1]  ← glibc

&lt;span class="c"&gt;# Termux-built QEMU (accepted)&lt;/span&gt;
readelf &lt;span class="nt"&gt;-l&lt;/span&gt; qemu | &lt;span class="nb"&gt;grep &lt;/span&gt;interpreter
  &lt;span class="o"&gt;[&lt;/span&gt;Requesting program interpreter: /system/bin/linker64]  ← Bionic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Switch to pre-built QEMU from Termux packages. Termux builds everything against Android's Bionic libc and uses &lt;code&gt;/system/bin/linker64&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  One More Catch: &lt;code&gt;useLegacyPackaging&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;AGP 3.6+ compresses native libraries inside the APK by default (to reduce download size). Compressed &lt;code&gt;.so&lt;/code&gt; files are NOT extracted to disk — they're loaded directly from the APK zip.&lt;/p&gt;

&lt;p&gt;QEMU is not a shared library. We need it on disk to execute it. Fix in &lt;code&gt;build.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;packagingOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;jniLibs&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;useLegacyPackaging&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;// force extraction to disk&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Root Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Permission denied&lt;/code&gt; on exec&lt;/td&gt;
&lt;td&gt;SELinux W^X on &lt;code&gt;filesDir&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Ship as &lt;code&gt;.so&lt;/code&gt; in &lt;code&gt;jniLibs/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.so&lt;/code&gt; not extracted&lt;/td&gt;
&lt;td&gt;APK compression&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useLegacyPackaging true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong ELF interpreter&lt;/td&gt;
&lt;td&gt;glibc binary&lt;/td&gt;
&lt;td&gt;Use Termux pre-built QEMU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Part 3 — Bundling 50 Native Libraries&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Pockr Series — Docker in Your Pocket
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pockr&lt;/strong&gt; = &lt;em&gt;Pocket&lt;/em&gt; + &lt;em&gt;Docker&lt;/em&gt;. A single Android APK that runs real Docker containers in your pocket — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-running-docker-on-a-non-rooted-android-phone-4g01"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Pockr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-1-the-idea-and-architecture-h80"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-2-executing-binaries-on-android-3b4k"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Executing Binaries — The SELinux Problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bundling 50 Native Libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-4-docker-without-kernel-modules-232m"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Docker Without Kernel Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22"&gt;Part 5&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Debugging the VM Restart Loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-6-test-results-and-whats-next-8m5"&gt;Part 6&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results and What's Next&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>linux</category>
      <category>security</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Pockr | Part 1 — The Idea and Architecture</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Thu, 05 Mar 2026 13:23:41 +0000</pubDate>
      <link>https://forem.com/ai2th/pockr-part-1-the-idea-and-architecture-h80</link>
      <guid>https://forem.com/ai2th/pockr-part-1-the-idea-and-architecture-h80</guid>
      <description>&lt;h1&gt;
  
  
  The Idea and Architecture
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;This is &lt;strong&gt;Part 1&lt;/strong&gt; of a 6-part series on building Pockr — a single APK that runs real Docker containers on a non-rooted Android phone.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Android phones are powerful computers. A mid-range device today has 8 cores, 8 GB RAM, and runs a full Linux kernel underneath.&lt;/p&gt;

&lt;p&gt;Yet you cannot run a Docker container on one — not without root, not without Termux, not without jumping through hoops.&lt;/p&gt;

&lt;p&gt;We wanted to change that. &lt;strong&gt;One APK. Install it. Tap Start. Run containers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No root required. No Termux. No PC after install.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The stack is simpler than it sounds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flutter UI
  └── MethodChannel (Kotlin)
        └── QEMU (ARM64 binary embedded in APK)
              └── Alpine Linux VM
                    └── Docker daemon
                          └── Your containers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The phone runs a real &lt;strong&gt;Alpine Linux virtual machine&lt;/strong&gt; using QEMU. Inside that VM, Docker runs normally — same as on any Linux server. The app communicates with a FastAPI server running inside the VM over a forwarded port (&lt;code&gt;localhost:7080&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Pieces Fit Together
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Flutter (Dart)&lt;/td&gt;
&lt;td&gt;Dark-themed dashboard, container list, settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bridge&lt;/td&gt;
&lt;td&gt;MethodChannel&lt;/td&gt;
&lt;td&gt;Flutter ↔ Kotlin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM launcher&lt;/td&gt;
&lt;td&gt;Kotlin &lt;code&gt;ProcessBuilder&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Spawns QEMU as a child process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hypervisor&lt;/td&gt;
&lt;td&gt;QEMU &lt;code&gt;qemu-system-aarch64&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Runs Alpine in a headless VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guest OS&lt;/td&gt;
&lt;td&gt;Alpine Linux 3.19&lt;/td&gt;
&lt;td&gt;~100 MB QCOW2 disk image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container runtime&lt;/td&gt;
&lt;td&gt;Docker (inside Alpine)&lt;/td&gt;
&lt;td&gt;Full Docker daemon, no restrictions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;FastAPI (Python 3)&lt;/td&gt;
&lt;td&gt;Runs on &lt;code&gt;0.0.0.0:7080&lt;/code&gt; inside VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;UUID token&lt;/td&gt;
&lt;td&gt;Injected via QEMU kernel cmdline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Network Architecture
&lt;/h2&gt;

&lt;p&gt;QEMU's SLIRP networking provides user-mode TCP/IP without kernel modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android app
  └── HTTP to 127.0.0.1:7080
        └── QEMU hostfwd: hostfwd=tcp::7080-:7080
              └── Alpine guest eth0 (10.0.2.15):7080
                    └── FastAPI server
                          └── Docker socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API server &lt;strong&gt;must&lt;/strong&gt; bind to &lt;code&gt;0.0.0.0:7080&lt;/code&gt; — not &lt;code&gt;127.0.0.1&lt;/code&gt;. SLIRP delivers connections to the guest's eth0, not its loopback interface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The QCOW2 Disk Strategy
&lt;/h2&gt;

&lt;p&gt;Rather than shipping one large disk image, we use QCOW2 copy-on-write layering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;base.qcow2 (read-only, 102 MB compressed)
  └── user.qcow2 (writable overlay, 8 GB virtual, starts empty)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;base.qcow2&lt;/code&gt; — Alpine Linux with Docker pre-installed. Bundled in the APK.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user.qcow2&lt;/code&gt; — Created fresh on first install. Persists between VM restarts. Stores pulled Docker images.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means: pull &lt;code&gt;nginx&lt;/code&gt; once, and it's available on every subsequent boot — no re-download.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coming Up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 2:&lt;/strong&gt; Why you can't just &lt;code&gt;execve()&lt;/code&gt; a binary on Android — and how we worked around SELinux&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3:&lt;/strong&gt; Bundling 50 Termux shared libraries without breaking the ELF linker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4:&lt;/strong&gt; Making Docker start without iptables or bridge kernel modules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 5:&lt;/strong&gt; Debugging a VM restart loop caused by a race condition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 6:&lt;/strong&gt; Firebase Test Lab results and what comes next&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Pockr Series — Docker in Your Pocket
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pockr&lt;/strong&gt; = &lt;em&gt;Pocket&lt;/em&gt; + &lt;em&gt;Docker&lt;/em&gt;. A single Android APK that runs real Docker containers in your pocket — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-running-docker-on-a-non-rooted-android-phone-4g01"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Pockr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-1-the-idea-and-architecture-h80"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-2-executing-binaries-on-android-3b4k"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Executing Binaries — The SELinux Problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bundling 50 Native Libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-4-docker-without-kernel-modules-232m"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Docker Without Kernel Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22"&gt;Part 5&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Debugging the VM Restart Loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-6-test-results-and-whats-next-8m5"&gt;Part 6&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results and What's Next&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>docker</category>
      <category>qemu</category>
      <category>linux</category>
    </item>
    <item>
      <title>Pockr — Running Docker on a Non-Rooted Android Phone</title>
      <dc:creator>Ai2th</dc:creator>
      <pubDate>Thu, 05 Mar 2026 13:22:00 +0000</pubDate>
      <link>https://forem.com/ai2th/pockr-running-docker-on-a-non-rooted-android-phone-4g01</link>
      <guid>https://forem.com/ai2th/pockr-running-docker-on-a-non-rooted-android-phone-4g01</guid>
      <description>&lt;h2&gt;
  
  
  What is Pockr?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pockr&lt;/strong&gt; stands for &lt;strong&gt;Pocket + Docker&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Docker in your pocket.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pockr is a single Android APK that runs real Docker containers on any non-rooted Android phone. No Termux. No root access. No PC required after install.&lt;/p&gt;

&lt;p&gt;You install it, tap &lt;strong&gt;Start Engine&lt;/strong&gt;, and within minutes you have a live Docker daemon running inside a QEMU virtual machine — pulling images from Docker Hub, running containers, all from a device in your pocket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr/releases/download/v1.0.0/pockr-v1.apk" rel="noopener noreferrer"&gt;pockr-v1.apk&lt;/a&gt; — v1.0.0 · ARM64 · 163 MB&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Does This Exist?
&lt;/h2&gt;

&lt;p&gt;Android phones are powerful computers. A mid-range 2024 device has 8 cores, 8 GB RAM, and a full Linux kernel underneath. Yet the platform makes it nearly impossible to run Linux tools natively without root access.&lt;/p&gt;

&lt;p&gt;Pockr explores how far you can push Android without any special privileges — using only public APIs, standard APK packaging, and a creatively bundled QEMU binary.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Need to Know Before Reading
&lt;/h2&gt;

&lt;p&gt;This series assumes basic familiarity with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What containers and images are, basic &lt;code&gt;docker run&lt;/code&gt; usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Linux&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Filesystems, processes, shared libraries (&lt;code&gt;.so&lt;/code&gt; files)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Android&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;App lifecycle, APK structure, ADB basics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ELF binaries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Helpful for Parts 2–3 (linker, shared libraries)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kotlin / Flutter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Helpful for Parts 1 and 5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You do NOT need to know QEMU, Alpine Linux, or virtual machines — those are explained as we go.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack at a Glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flutter UI (Dart)
  └── MethodChannel
        └── Kotlin VmManager
              └── QEMU (ARM64 binary, ships as libqemu.so)
                    └── Alpine Linux 3.19 (QCOW2 disk image)
                          └── Docker daemon
                                └── Your containers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Flutter + Provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native bridge&lt;/td&gt;
&lt;td&gt;Android MethodChannel (Kotlin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hypervisor&lt;/td&gt;
&lt;td&gt;QEMU 10.x, aarch64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guest OS&lt;/td&gt;
&lt;td&gt;Alpine Linux 3.19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container runtime&lt;/td&gt;
&lt;td&gt;Docker (inside Alpine)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;FastAPI on port 7080&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  About the Name: Pockr
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pockr&lt;/strong&gt; = &lt;strong&gt;Pocket&lt;/strong&gt; + &lt;strong&gt;Docker&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The goal was always something you could hand to someone — &lt;em&gt;"here, install this APK, and you have Docker"&lt;/em&gt;. No server. No laptop. No setup beyond tapping a button.&lt;/p&gt;

&lt;p&gt;Docker in your pocket. The dropped vowel keeps it short. The double meaning — pocket-sized and Docker — felt right.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pockr Series — Docker in Your Pocket
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pockr&lt;/strong&gt; = &lt;em&gt;Pocket&lt;/em&gt; + &lt;em&gt;Docker&lt;/em&gt;. A single Android APK that runs real Docker containers in your pocket — no root, no Termux, no PC required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📖&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-running-docker-on-a-non-rooted-android-phone-4g01"&gt;Intro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;What is Pockr? Start here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-1-the-idea-and-architecture-h80"&gt;Part 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The Idea and Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-2-executing-binaries-on-android-3b4k"&gt;Part 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Executing Binaries — The SELinux Problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-3-bundling-50-native-libraries-5chh"&gt;Part 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bundling 50 Native Libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-4-docker-without-kernel-modules-232m"&gt;Part 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Docker Without Kernel Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-5-debugging-the-vm-restart-loop-g22"&gt;Part 5&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Debugging the VM Restart Loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ai2th/pockr-part-6-test-results-and-whats-next-8m5"&gt;Part 6&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Test Results and What's Next&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AI2TH/Pockr" rel="noopener noreferrer"&gt;github.com/AI2TH/Pockr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ai2th.github.io" rel="noopener noreferrer"&gt;ai2th.github.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Idea &amp;amp; Contributor: Kalvin Nathan&lt;/em&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="mailto:skalvinnathan@gmail.com"&gt;skalvinnathan@gmail.com&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/skalvinnathan/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>docker</category>
      <category>linux</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
