DEV Community

Ai2th
Ai2th

Posted on

Pockr | Part 1 — The Idea and Architecture

The Idea and Architecture

This is Part 1 of a 6-part series on building Pockr — a single APK that runs real Docker containers on a non-rooted Android phone.


The Problem

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

Yet you cannot run a Docker container on one — not without root, not without Termux, not without jumping through hoops.

We wanted to change that. One APK. Install it. Tap Start. Run containers.

No root required. No Termux. No PC after install.


The Architecture

The stack is simpler than it sounds:

Flutter UI
  └── MethodChannel (Kotlin)
        └── QEMU (ARM64 binary embedded in APK)
              └── Alpine Linux VM
                    └── Docker daemon
                          └── Your containers
Enter fullscreen mode Exit fullscreen mode

The phone runs a real Alpine Linux virtual machine 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 (localhost:7080).


How the Pieces Fit Together

Layer Technology Notes
UI Flutter (Dart) Dark-themed dashboard, container list, settings
Bridge MethodChannel Flutter ↔ Kotlin
VM launcher Kotlin ProcessBuilder Spawns QEMU as a child process
Hypervisor QEMU qemu-system-aarch64 Runs Alpine in a headless VM
Guest OS Alpine Linux 3.19 ~100 MB QCOW2 disk image
Container runtime Docker (inside Alpine) Full Docker daemon, no restrictions
API FastAPI (Python 3) Runs on 0.0.0.0:7080 inside VM
Auth UUID token Injected via QEMU kernel cmdline

Network Architecture

QEMU's SLIRP networking provides user-mode TCP/IP without kernel modules:

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
Enter fullscreen mode Exit fullscreen mode

The API server must bind to 0.0.0.0:7080 — not 127.0.0.1. SLIRP delivers connections to the guest's eth0, not its loopback interface.


The QCOW2 Disk Strategy

Rather than shipping one large disk image, we use QCOW2 copy-on-write layering:

base.qcow2 (read-only, 102 MB compressed)
  └── user.qcow2 (writable overlay, 8 GB virtual, starts empty)
Enter fullscreen mode Exit fullscreen mode
  • base.qcow2 — Alpine Linux with Docker pre-installed. Bundled in the APK.
  • user.qcow2 — Created fresh on first install. Persists between VM restarts. Stores pulled Docker images.

This means: pull nginx once, and it's available on every subsequent boot — no re-download.


Coming Up

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

GitHub: github.com/AI2TH/Pockr



Pockr Series — Docker in Your Pocket

Pockr = Pocket + Docker. A single Android APK that runs real Docker containers in your pocket — no root, no Termux, no PC required.

# Post Topic
📖 Intro What is Pockr? Start here
1 Part 1 The Idea and Architecture
2 Part 2 Executing Binaries — The SELinux Problem
3 Part 3 Bundling 50 Native Libraries
4 Part 4 Docker Without Kernel Modules
5 Part 5 Debugging the VM Restart Loop
6 Part 6 Test Results and What's Next

GitHub: github.com/AI2TH/Pockr

Architect & Developer: Kalvin Nathan
skalvinnathan@gmail.com · LinkedIn

Top comments (0)