{"id":2888,"date":"2026-06-14T04:08:17","date_gmt":"2026-06-14T04:08:17","guid":{"rendered":"https:\/\/tucumandevelopers.com\/index.php\/2026\/06\/14\/run-real-docker-on-android-no-root-no-tricks-just-qemu\/"},"modified":"2026-06-14T04:08:17","modified_gmt":"2026-06-14T04:08:17","slug":"run-real-docker-on-android-no-root-no-tricks-just-qemu","status":"publish","type":"post","link":"https:\/\/tucumandevelopers.com\/index.php\/2026\/06\/14\/run-real-docker-on-android-no-root-no-tricks-just-qemu\/","title":{"rendered":"Run Real Docker on Android \u2014 No Root, No Tricks, Just QEMU"},"content":{"rendered":"<div>\n<div>\n<p><strong>Phone battery tip:<\/strong> If your phone has a &#8220;Protect Battery&#8221; or &#8220;Maximum 85%&#8221; setting (Samsung does), turn it on. The phone will be plugged in 24\/7, and capping the charge doubles the battery&#8217;s lifespan.<\/p>\n<hr>\n<h2> <a name=\"the-architecture-mental-model\" href=\"#the-architecture-mental-model\"> <\/a> The architecture (mental model) <\/h2>\n<p>Here&#8217;s what we&#8217;re building: <\/p>\n<div>\n<pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 Your Android phone (not rooted) \u2502 \u2502 \u2502 \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u2502 \u2502 Termux (regular Android app) \u2502 \u2502 \u2502 \u2502 - QEMU binary (qemu-system-aarch64) \u2502 \u2502 \u2502 \u2502 - The .qcow2 disk image \u2502 \u2502 \u2502 \u2502 - Boot scripts \u2502 \u2502 \u2502 \u2502 - sshd (port 8022) \u2192 your computer SSHes \u2502 \u2502 \u2502 \u2502 in here for management \u2502 \u2502 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502 \u2502 \u2502 launches \u2502 \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u2502 \u2502 QEMU VM (looks like real ARM64 hardware) \u2502 \u2502 \u2502 \u2502 - Debian 12 (bookworm) \u2502 \u2502 \u2502 \u2502 - Real dockerd + Docker Compose v2 \u2502 \u2502 \u2502 \u2502 - sshd (port 22 \u2192 forwarded to 2222) \u2502 \u2502 \u2502 \u2502 - systemd works (unlike in proot) \u2502 \u2502 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2191 \u2502 SSH from your computer \u2502 ssh phone-vm \u2192 port 2222 \u2192 Debian VM \u2502 ssh phone-termux \u2192 port 8022 \u2192 Termux shell <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Two SSH targets. <strong><code>phone-vm<\/code><\/strong> drops you into Debian where Docker lives. <strong><code>phone-termux<\/code><\/strong> drops you into Termux itself (the Android-side layer), useful for troubleshooting the VM or restarting QEMU.<\/p>\n<p><strong>Why QEMU and not just proot-distro?<\/strong> Three reasons:<\/p>\n<ol>\n<li> <strong>proot-distro has no systemd.<\/strong> No systemd means no <code>systemctl enable docker<\/code>. You&#8217;d have to manually start the daemon on every login.<\/li>\n<li> <strong>proot-distro has no cgroups.<\/strong> No cgroups means no container resource control. Docker will refuse to start.<\/li>\n<li> <strong>proot-distro has no real namespaces.<\/strong> No namespaces means no isolation. Containers would see each other.<\/li>\n<\/ol>\n<p>QEMU gives us a real Linux kernel with all three. The cost is speed \u2014 every instruction is translated by QEMU&#8217;s TCG (Tiny Code Generator) software emulator. That&#8217;s why boot takes 20\u201330 minutes.<\/p>\n<hr>\n<h2> <a name=\"step-1-install-termux-the-right-way\" href=\"#step-1-install-termux-the-right-way\"> <\/a> Step 1: Install Termux (the right way) <\/h2>\n<p><strong>Do not install Termux from the Play Store.<\/strong> The Play Store version is frozen at v0.101 from 2020 and has a known security vulnerability. Install from <strong>F-Droid<\/strong> or the <strong>GitHub releases<\/strong> \u2014 but pick one and stick with it, because they&#8217;re signature-incompatible with each other.<\/p>\n<p>The cleanest option is F-Droid. Go to <a href=\"https:\/\/f-droid.org\/en\/packages\/com.termux\/\" target=\"_blank\" rel=\"noopener noreferrer\">f-droid.org\/en\/packages\/com.termux<\/a> on your phone and install the APK.<\/p>\n<p>While you&#8217;re at it, also install <strong>Termux:Boot<\/strong> from F-Droid: <a href=\"https:\/\/f-droid.org\/en\/packages\/com.termux.boot\/\" target=\"_blank\" rel=\"noopener noreferrer\">f-droid.org\/en\/packages\/com.termux.boot<\/a>. This is what makes QEMU auto-start when the phone reboots. We&#8217;ll configure it in Step 7.<\/p>\n<p><strong>After installing Termux:Boot, open it once.<\/strong> Just tap the icon, then close. This registers it with Android&#8217;s <code>BOOT_COMPLETED<\/code> broadcast. If you skip this, the auto-start script in Step 7 won&#8217;t fire.<\/p>\n<p>Now open Termux. You should see a <code>$<\/code> prompt. Run: <\/p>\n<div>\n<pre><code>pkg update <span>-y<\/span> <span>&amp;&amp;<\/span> pkg upgrade <span>-y<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>This updates the package lists and upgrades everything. First run takes 1\u20132 minutes.<\/p>\n<p>If it hangs on a mirror, run <code>termux-change-repo<\/code>, pick &#8220;Main repository&#8221;, and select a mirror close to you. Indonesian users: <code>linux.domainesia.com<\/code> and <code>mirror.nevacloud.com<\/code> are in the Asia group.<\/p>\n<hr>\n<h2> <a name=\"step-2-set-up-ssh-so-you-can-work-from-your-computer\" href=\"#step-2-set-up-ssh-so-you-can-work-from-your-computer\"> <\/a> Step 2: Set up SSH so you can work from your computer <\/h2>\n<p>Typing long commands on a phone keyboard is miserable. Let&#8217;s fix that by setting up SSH from your computer.<\/p>\n<h3> <a name=\"21-grant-storage-access-onetime\" href=\"#21-grant-storage-access-onetime\"> <\/a> 2.1 Grant storage access (one-time) <\/h3>\n<p>In Termux: <\/p>\n<div>\n<pre><code>termux-setup-storage <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>A dialog pops up on your phone asking for storage permission. Tap &#8220;Allow&#8221;. This creates <code>~\/storage\/<\/code> symlinks to shared storage \u2014 not strictly needed for QEMU, but useful later if you want to back up your disk image.<\/p>\n<h3> <a name=\"22-set-a-termux-password\" href=\"#22-set-a-termux-password\"> <\/a> 2.2 Set a Termux password <\/h3>\n<div>\n<pre><code>passwd <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Type a password twice. You&#8217;ll need this for SSH (though we&#8217;ll set up key-based auth in a moment).<\/p>\n<h3> <a name=\"23-add-your-computers-ssh-public-key\" href=\"#23-add-your-computers-ssh-public-key\"> <\/a> 2.3 Add your computer&#8217;s SSH public key <\/h3>\n<p>On your <strong>computer<\/strong>, get your public key: <\/p>\n<div>\n<pre><code><span># macOS \/ Linux \/ WSL<\/span> <span>cat<\/span> ~\/.ssh\/id_ed25519.pub <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>If you don&#8217;t have one, generate it: <\/p>\n<div>\n<pre><code>ssh-keygen <span>-t<\/span> ed25519 <span>-C<\/span> <span>\"your-email@example.com\"<\/span> <span># Press Enter through all the prompts<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Copy the output (a string starting with <code>ssh-ed25519 AAAA...<\/code>).<\/p>\n<p>Back in <strong>Termux<\/strong> on the phone: <\/p>\n<div>\n<pre><code><span>mkdir<\/span> <span>-p<\/span> ~\/.ssh <span>&amp;&amp;<\/span> <span>chmod <\/span>700 ~\/.ssh <span>echo<\/span> <span>'PASTE_YOUR_PUBLIC_KEY_HERE'<\/span> <span>&gt;&gt;<\/span> ~\/.ssh\/authorized_keys <span>chmod <\/span>600 ~\/.ssh\/authorized_keys <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Replace <code>PASTE_YOUR_PUBLIC_KEY_HERE<\/code> with the key you copied. Keep the single quotes around it.<\/p>\n<h3> <a name=\"24-start-the-ssh-server\" href=\"#24-start-the-ssh-server\"> <\/a> 2.4 Start the SSH server <\/h3>\n<div>\n<pre><code>sshd <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>No output means it started. Termux&#8217;s sshd listens on <strong>port 8022<\/strong> (not the usual 22 \u2014 Android reserves 22 for the system).<\/p>\n<h3> <a name=\"25-find-your-phones-ip\" href=\"#25-find-your-phones-ip\"> <\/a> 2.5 Find your phone&#8217;s IP <\/h3>\n<div>\n<pre><code>ip addr show wlan0 | <span>grep <\/span>inet <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>You&#8217;ll see something like <code>inet 192.168.0.9\/24<\/code>. Note the IP (without the <code>\/24<\/code>).<\/p>\n<h3> <a name=\"26-set-up-ssh-config-on-your-computer\" href=\"#26-set-up-ssh-config-on-your-computer\"> <\/a> 2.6 Set up SSH config on your computer <\/h3>\n<p>On your <strong>computer<\/strong>, edit <code>~\/.ssh\/config<\/code> and add: <\/p>\n<div>\n<pre><code><span>Host<\/span> phone-termux <span>HostName<\/span> <span>192<\/span>.168.0.9 <span>Port<\/span> <span>8022<\/span> <span>User<\/span> u0_a892 <span>IdentityFile<\/span> ~\/.ssh\/id_ed25519 <span>ControlMaster<\/span> <span>auto<\/span> <span>ControlPath<\/span> ~\/.ssh\/controlmasters\/%r@%h:%p <span>ControlPersist<\/span> <span>10<\/span>m <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Replace <code>192.168.0.9<\/code> with your phone&#8217;s IP. The <code>User<\/code> looks weird (<code>u0_a892<\/code>) \u2014 that&#8217;s Termux&#8217;s Android UID, which is also its Linux username. The number after <code>u0_a<\/code> varies per install; to find yours, run <code>whoami<\/code> in Termux.<\/p>\n<p>Create the controlmasters directory (needed for connection multiplexing, which makes repeated SSH commands nearly instant): <\/p>\n<div>\n<pre><code><span>mkdir<\/span> <span>-p<\/span> ~\/.ssh\/controlmasters <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Now test: <\/p>\n<div>\n<pre><code>ssh phone-termux <span>whoami<\/span> <span># \u2192 u0_a892<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>If that works, you&#8217;re set. From now on, you can run commands in Termux from the comfort of your computer&#8217;s keyboard.<\/p>\n<hr>\n<h2> <a name=\"step-3-download-debian-and-build-the-cloudinit-seed\" href=\"#step-3-download-debian-and-build-the-cloudinit-seed\"> <\/a> Step 3: Download Debian and build the cloud-init seed <\/h2>\n<p>Back in Termux (either on the phone directly, or via <code>ssh phone-termux<\/code> from your computer):<\/p>\n<h3> <a name=\"31-install-qemu-and-friends\" href=\"#31-install-qemu-and-friends\"> <\/a> 3.1 Install QEMU and friends <\/h3>\n<div>\n<pre><code>pkg <span>install<\/span> <span>-y<\/span> qemu-system-aarch64 openssh curl wget genisoimage <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>This installs:<\/p>\n<ul>\n<li> <strong><code>qemu-system-aarch64<\/code><\/strong> \u2014 the emulator itself (~400 MB, includes UEFI firmware)<\/li>\n<li> <strong><code>openssh<\/code><\/strong> \u2014 for the SSH server we already started<\/li>\n<li> <strong><code>curl<\/code> \/ <code>wget<\/code><\/strong> \u2014 for downloading the Debian image<\/li>\n<li> <strong><code>genisoimage<\/code><\/strong> \u2014 for building the cloud-init seed ISO<\/li>\n<\/ul>\n<h3> <a name=\"32-acquire-a-wake-lock-critical-do-not-skip\" href=\"#32-acquire-a-wake-lock-critical-do-not-skip\"> <\/a> 3.2 Acquire a wake lock (CRITICAL \u2014 do not skip) <\/h3>\n<div>\n<pre><code>termux-wake-lock <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>This tells Android &#8220;don&#8217;t kill this process when the screen is off.&#8221; Without it, Android&#8217;s Doze mode will murder QEMU 5\u201310 minutes after your screen goes dark, and your VM will die mid-boot. You only need to run this once per Termux session \u2014 but it&#8217;s easiest to put it in your boot script (Step 7) so it&#8217;s always active.<\/p>\n<h3> <a name=\"33-download-the-debian-cloud-image\" href=\"#33-download-the-debian-cloud-image\"> <\/a> 3.3 Download the Debian cloud image <\/h3>\n<div>\n<pre><code><span>mkdir<\/span> <span>-p<\/span> ~\/qemu-vm <span>&amp;&amp;<\/span> <span>cd<\/span> ~\/qemu-vm wget https:\/\/cloud.debian.org\/images\/cloud\/bookworm\/latest\/debian-12-genericcloud-arm64.qcow2 <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>This is a 500 MB qcow2 file \u2014 Debian&#8217;s official &#8220;generic cloud&#8221; image for ARM64. It&#8217;s built for exactly this use case (cloud VMs), so it has cloud-init pre-installed and no desktop environment.<\/p>\n<h3> <a name=\"34-resize-the-disk\" href=\"#34-resize-the-disk\"> <\/a> 3.4 Resize the disk <\/h3>\n<div>\n<pre><code>qemu-img resize debian-12-genericcloud-arm64.qcow2 16G <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>The downloaded image is ~2 GB (logical size). We resize to 16 GB so there&#8217;s room for Docker, containers, and pulled images. The qcow2 format only allocates disk space as it&#8217;s used, so this won&#8217;t actually consume 16 GB on your phone immediately.<\/p>\n<p>Rename it for clarity: <\/p>\n<div>\n<pre><code><span>mv <\/span>debian-12-genericcloud-arm64.qcow2 debian-12-arm64.qcow2 <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h3> <a name=\"35-create-the-cloudinit-seed\" href=\"#35-create-the-cloudinit-seed\"> <\/a> 3.5 Create the cloud-init seed <\/h3>\n<p>Cloud-init is how we configure the VM on first boot: set hostname, create users, add SSH keys. It reads from a &#8220;seed&#8221; \u2014 in our case, a small ISO file attached as a virtual CD-ROM.<\/p>\n<p>Create <code>user-data<\/code>: <\/p>\n<div>\n<pre><code><span>cat<\/span> <span>&gt;<\/span> user-data <span>&lt;&lt;<\/span><span>'<\/span><span>EOF<\/span><span>' #cloud-config hostname: docker-phone users: - name: sulthon sudo: ALL=(ALL) NOPASSWD:ALL shell: \/bin\/bash ssh_authorized_keys: - ssh-ed25519 AAAA...YOUR_KEY_HERE... your-email@example.com ssh_pwauth: false disable_root: false package_update: true packages: - qemu-guest-agent - ca-certificates - curl growpart: mode: auto devices: ['\/'] ignore_growroot: false <\/span><span>EOF <\/span><\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p><strong>Edit the SSH key<\/strong> to match your actual <code>~\/.ssh\/id_ed25519.pub<\/code> from Step 2.3. This is what lets you SSH into the Debian VM later. Change the username <code>sulthon<\/code> to whatever you like \u2014 just remember it for later steps.<\/p>\n<p>Create <code>meta-data<\/code>: <\/p>\n<div>\n<pre><code><span>cat<\/span> <span>&gt;<\/span> meta-data <span>&lt;&lt;<\/span><span>'<\/span><span>EOF<\/span><span>' instance-id: docker-phone-001 local-hostname: docker-phone <\/span><span>EOF <\/span><\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Build the seed ISO: <\/p>\n<div>\n<pre><code>genisoimage <span>-output<\/span> seed.iso <span>-volid<\/span> cidata <span>-joliet<\/span> <span>-rock<\/span> user-data meta-data <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>The <code>-volid cidata<\/code> is critical \u2014 cloud-init looks for a volume labeled exactly <code>cidata<\/code> (or <code>CIDATA<\/code>). If you get this wrong, cloud-init won&#8217;t run and you&#8217;ll have no users, no SSH keys, no nothing.<\/p>\n<hr>\n<h2> <a name=\"step-4-the-qemu-launcher-script\" href=\"#step-4-the-qemu-launcher-script\"> <\/a> Step 4: The QEMU launcher script <\/h2>\n<p>This is the script that boots Debian. There&#8217;s a lot going on, so let&#8217;s break it down carefully.<\/p>\n<p>Copy the UEFI firmware variables first (one-time): <\/p>\n<div>\n<pre><code><span>cp<\/span> <span>$PREFIX<\/span>\/share\/qemu\/edk2-aarch64-vars.fd ~\/qemu-vm\/edk2-vars.fd <span>truncate<\/span> <span>-s<\/span> 64M ~\/qemu-vm\/edk2-vars.fd <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>This creates a writable copy of the UEFI NVRAM (the <code>edk2-vars.fd<\/code> file). The <code>truncate<\/code> grows it to 64 MB for safety margin. <strong>Don&#8217;t skip this<\/strong> \u2014 without the vars file, UEFI state won&#8217;t persist across VM reboots, and your VM won&#8217;t boot a second time.<\/p>\n<p>Now create the launcher script. This is the heart of the whole setup: <\/p>\n<div>\n<pre><code><span>cat<\/span> <span>&gt;<\/span> ~\/boot-debian-mon.sh <span>&lt;&lt;<\/span><span>'<\/span><span>EOF<\/span><span>' #!\/data\/data\/com.termux\/files\/usr\/bin\/bash set +e VM_DIR=\"\/data\/data\/com.termux\/files\/home\/qemu-vm\" CODE=\"\/data\/data\/com.termux\/files\/usr\/share\/qemu\/edk2-aarch64-code.fd\" VARS=\"<\/span><span>$VM_DIR<\/span><span>\/edk2-vars.fd\" IMG=\"<\/span><span>$VM_DIR<\/span><span>\/debian-12-arm64.qcow2\" SEED=\"<\/span><span>$VM_DIR<\/span><span>\/seed.iso\" MONSOCK=\"<\/span><span>$VM_DIR<\/span><span>\/mon.sock\" SERIALSOCK=\"<\/span><span>$VM_DIR<\/span><span>\/serial.sock\" LOG=\"<\/span><span>$VM_DIR<\/span><span>\/debian-boot.log\" pkill -9 -f qemu-system-aarch64 2&gt;\/dev\/null sleep 2 rm -f <\/span><span>$MONSOCK<\/span><span> <\/span><span>$SERIALSOCK<\/span><span> setsid qemu-system-aarch64 <\/span><span>\\<\/span><span> -name docker-phone <\/span><span>\\<\/span><span> -machine virt,gic-version=3 <\/span><span>\\<\/span><span> -cpu max,aarch64=on,pmu=on <\/span><span>\\<\/span><span> -smp 6 <\/span><span>\\<\/span><span> -m 6144 <\/span><span>\\<\/span><span> -accel tcg,thread=multi,tb-size=512 <\/span><span>\\<\/span><span> -nodefaults <\/span><span>\\<\/span><span> -chardev socket,id=mon0,path=<\/span><span>$MONSOCK<\/span><span>,server=on,wait=off <\/span><span>\\<\/span><span> -mon chardev=mon0,mode=readline <\/span><span>\\<\/span><span> -chardev socket,id=ser0,path=<\/span><span>$SERIALSOCK<\/span><span>,server=on,wait=off,logfile=<\/span><span>$LOG<\/span><span> <\/span><span>\\<\/span><span> -serial chardev:ser0 <\/span><span>\\<\/span><span> -display none <\/span><span>\\<\/span><span> -drive if=pflash,format=raw,readonly=on,file=<\/span><span>$CODE<\/span><span> <\/span><span>\\<\/span><span> -drive if=pflash,format=raw,file=<\/span><span>$VARS<\/span><span> <\/span><span>\\<\/span><span> -drive file=<\/span><span>$IMG<\/span><span>,if=virtio,format=qcow2,cache=writeback <\/span><span>\\<\/span><span> -drive file=<\/span><span>$SEED<\/span><span>,if=virtio,format=raw,readonly=on <\/span><span>\\<\/span><span> -netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::8080-:80,hostfwd=tcp::9000-:9000 <\/span><span>\\<\/span><span> -device virtio-net-pci,netdev=net0 <\/span><span>\\<\/span><span> -device virtio-rng-pci <\/span><span>\\<\/span><span> -rtc base=utc <\/span><span>\\<\/span><span> &gt; <\/span><span>$LOG<\/span><span> 2&gt;&amp;1 &amp; disown sleep 3 echo \"QEMU PID: <\/span><span>$(<\/span>pgrep <span>-f<\/span> qemu-system-aarch64 | <span>head<\/span> <span>-1<\/span><span>)<\/span><span>\" echo \"Monitor socket: <\/span><span>$MONSOCK<\/span><span>\" echo \"Serial socket: <\/span><span>$SERIALSOCK<\/span><span>\" echo \"Log: <\/span><span>$LOG<\/span><span>\" <\/span><span>EOF <\/span><span>chmod<\/span> +x ~\/boot-debian-mon.sh <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h3> <a name=\"what-each-line-does\" href=\"#what-each-line-does\"> <\/a> What each line does <\/h3>\n<p><strong>The machine definition:<\/strong><\/p>\n<ul>\n<li> <code>-machine virt,gic-version=3<\/code> \u2014 QEMU&#8217;s &#8220;virt&#8221; machine, which is designed for VMs (not emulating any specific real phone). GIC version 3 is the modern ARM interrupt controller.<\/li>\n<li> <code>-cpu max,aarch64=on,pmu=on<\/code> \u2014 expose the maximum CPU feature set to the guest, including hardware crypto (ARMv8 crypto extensions). This makes TLS handshakes faster.<\/li>\n<li> <code>-smp 6<\/code> \u2014 give the VM 6 CPUs. Adjust based on your phone (8-core phones typically have a &#8220;big.LITTLE&#8221; layout; 6 leaves 2 for Android).<\/li>\n<li> <code>-m 6144<\/code> \u2014 give the VM 6 GB of RAM. Leave the rest for Android.<\/li>\n<\/ul>\n<p><strong>The emulator:<\/strong><\/p>\n<ul>\n<li> <code>-accel tcg,thread=multi,tb-size=512<\/code> \u2014 use TCG (software emulation) with multi-threading and a 512 MB translation block cache. This is the best TCG config for sustained throughput.<\/li>\n<\/ul>\n<p><strong>Storage:<\/strong><\/p>\n<ul>\n<li>The first <code>-drive if=pflash,readonly=on<\/code> is the UEFI firmware code (read-only).<\/li>\n<li>The second <code>-drive if=pflash<\/code> is the UEFI vars file (writable \u2014 this is why we made the copy earlier).<\/li>\n<li>The <code>-drive file=$IMG,if=virtio<\/code> is your Debian disk.<\/li>\n<li>The <code>-drive file=$SEED,if=virtio,readonly=on<\/code> is the cloud-init seed ISO.<\/li>\n<\/ul>\n<p><strong>Networking:<\/strong><\/p>\n<ul>\n<li> <code>-netdev user,id=net0,hostfwd=tcp::2222-:22,...<\/code> \u2014 user-mode networking with port forwarding. Anything connecting to port 2222 on the phone gets forwarded to port 22 inside the VM. We also forward 8080 and 9000 for web apps you might run later.<\/li>\n<\/ul>\n<p><strong>Process management (the tricky part):<\/strong><\/p>\n<ul>\n<li> <code>setsid<\/code> \u2014 detaches QEMU from Termux&#8217;s session leader. Without this, when SSH disconnects, Android kills the whole process tree.<\/li>\n<li> <code>disown<\/code> \u2014 removes QEMU from the shell&#8217;s job table.<\/li>\n<li>Together, these mean QEMU survives SSH disconnects. <strong>Do not use plain <code>nohup<\/code> \u2014 it&#8217;s not enough on Termux.<\/strong> <\/li>\n<\/ul>\n<hr>\n<h2> <a name=\"step-5-first-boot-patience-required\" href=\"#step-5-first-boot-patience-required\"> <\/a> Step 5: First boot (patience required) <\/h2>\n<p>Launch the VM: <\/p>\n<div>\n<pre><code>bash ~\/boot-debian-mon.sh <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>You should see: <\/p>\n<div>\n<pre><code><span>QEMU<\/span> <span>PID<\/span><span>:<\/span> <span>12345<\/span> <span>Monitor<\/span> <span>socket<\/span><span>:<\/span> <span>\/data\/data\/com.termux\/files\/home\/qemu-vm\/mon.sock<\/span> <span>Serial<\/span> <span>socket<\/span><span>:<\/span> <span>\/data\/data\/com.termux\/files\/home\/qemu-vm\/serial.sock<\/span> <span>Log<\/span><span>:<\/span> <span>\/data\/data\/com.termux\/files\/home\/qemu-vm\/debian-boot.log<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Now wait. <strong>20\u201330 minutes.<\/strong> No, that&#8217;s not a typo. TCG software emulation is brutally slow.<\/p>\n<p>You can watch the boot log from another SSH session: <\/p>\n<div>\n<pre><code><span># From your computer:<\/span> ssh phone-termux <span>tail<\/span> <span>-f<\/span> ~\/qemu-vm\/debian-boot.log <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>When you see something like: <\/p>\n<div>\n<pre><code><span>[ OK ] Started OpenSSH server <\/span><\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>\u2026and the log stops growing, the VM is ready. Verify from your computer: <\/p>\n<div>\n<pre><code>ssh <span>-p<\/span> 2222 sulthon@192.168.0.9 <span>hostname<\/span> <span># \u2192 docker-phone<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>(Reminder: change <code>sulthon<\/code> to whatever username you put in <code>user-data<\/code>.)<\/p>\n<p>If that works, add a second SSH config entry on your computer for the VM: <\/p>\n<div>\n<pre><code><span># append to ~\/.ssh\/config<\/span> <span>Host<\/span> phone-vm <span>HostName<\/span> <span>192<\/span>.168.0.9 <span>Port<\/span> <span>2222<\/span> <span>User<\/span> sulthon <span>IdentityFile<\/span> ~\/.ssh\/id_ed25519 <span>ControlMaster<\/span> <span>auto<\/span> <span>ControlPath<\/span> ~\/.ssh\/controlmasters\/%r@%h:%p <span>ControlPersist<\/span> <span>10<\/span>m <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Now <code>ssh phone-vm<\/code> Just Works.<\/p>\n<hr>\n<h2> <a name=\"step-6-install-docker-inside-the-vm\" href=\"#step-6-install-docker-inside-the-vm\"> <\/a> Step 6: Install Docker inside the VM <\/h2>\n<p>SSH into the VM and install everything: <\/p>\n<div>\n<pre><code>ssh phone-vm <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h3> <a name=\"61-install-docker\" href=\"#61-install-docker\"> <\/a> 6.1 Install Docker <\/h3>\n<div>\n<pre><code><span>sudo <\/span>apt-get update <span>sudo <\/span>apt-get <span>install<\/span> <span>-y<\/span> docker.io <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>The <code>docker.io<\/code> package is Debian&#8217;s official Docker package (version 20.10.24 as of this writing). It&#8217;s slightly older than Docker CE, but it&#8217;s in the main Debian repos and works perfectly. <strong>Expect this to take 25\u201335 minutes under TCG.<\/strong> Go get coffee.<\/p>\n<h3> <a name=\"62-install-docker-compose-v2\" href=\"#62-install-docker-compose-v2\"> <\/a> 6.2 Install Docker Compose v2 <\/h3>\n<div>\n<pre><code><span>sudo mkdir<\/span> <span>-p<\/span> \/usr\/libexec\/docker\/cli-plugins <span>sudo <\/span>curl <span>-fSL<\/span> <span>-o<\/span> \/usr\/libexec\/docker\/cli-plugins\/docker-compose <span>\\<\/span> https:\/\/github.com\/docker\/compose\/releases\/download\/v2.29.7\/docker-compose-linux-aarch64 <span>sudo chmod<\/span> +x \/usr\/libexec\/docker\/cli-plugins\/docker-compose <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>We download the binary directly because the <code>docker-compose-v2<\/code> Debian package isn&#8217;t in bookworm. Placing it in <code>cli-plugins\/<\/code> means <code>docker compose<\/code> (with a space, not a hyphen) works.<\/p>\n<h3> <a name=\"63-let-your-user-run-docker-without-sudo\" href=\"#63-let-your-user-run-docker-without-sudo\"> <\/a> 6.3 Let your user run Docker without sudo <\/h3>\n<div>\n<pre><code><span>sudo <\/span>usermod <span>-aG<\/span> docker <span>$USER<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>You&#8217;ll need to log out and back in for this to take effect: <\/p>\n<div>\n<pre><code><span>exit<\/span> <span># back to your computer<\/span> ssh phone-vm <span># back in<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h3> <a name=\"64-configure-docker-for-the-slow-vm\" href=\"#64-configure-docker-for-the-slow-vm\"> <\/a> 6.4 Configure Docker for the slow VM <\/h3>\n<p>This step is <strong>critical<\/strong>. Without it, <code>docker pull<\/code> will fail with <code>TLS handshake timeout<\/code> because TCG is too slow for Docker&#8217;s default timeouts. <\/p>\n<div>\n<pre><code><span>sudo tee<\/span> \/etc\/docker\/daemon.json <span>&gt;<\/span>\/dev\/null <span>&lt;&lt;<\/span><span>'<\/span><span>EOF<\/span><span>' { \"max-concurrent-downloads\": 1, \"max-download-attempts\": 5, \"dns\": [\"8.8.8.8\", \"1.1.1.1\"], \"ip6tables\": false, \"ipv6\": false } <\/span><span>EOF <\/span><span>sudo <\/span>systemctl restart docker <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>What each setting does:<\/p>\n<ul>\n<li> <code>max-concurrent-downloads: 1<\/code> \u2014 only download one layer at a time. Parallel downloads overwhelm TCG and time out.<\/li>\n<li> <code>max-download-attempts: 5<\/code> \u2014 retry failed layers.<\/li>\n<li> <code>dns: [8.8.8.8, 1.1.1.1]<\/code> \u2014 Docker&#8217;s embedded DNS sometimes can&#8217;t resolve registry hostnames under TCG; this forces public DNS.<\/li>\n<li> <code>ip6tables: false<\/code> and <code>ipv6: false<\/code> \u2014 disable IPv6. TCG&#8217;s IPv6 stack is even slower than IPv4, and many container images misbehave over IPv6 anyway.<\/li>\n<\/ul>\n<h3> <a name=\"65-add-zram-swap-recommended\" href=\"#65-add-zram-swap-recommended\"> <\/a> 6.5 Add ZRAM swap (recommended) <\/h3>\n<p>ZRAM is compressed swap in RAM. Under TCG, real disk I\/O is brutal, so having compressed memory swap gives you a safety net for memory spikes. <\/p>\n<div>\n<pre><code><span>sudo <\/span>apt-get <span>install<\/span> <span>-y<\/span> zram-tools <span>echo<\/span> <span>-e<\/span> <span>\"ALGO=zstd<\/span><span>\\n<\/span><span>PERCENT=75<\/span><span>\\n<\/span><span>PRIORITY=100\"<\/span> | <span>sudo tee<\/span> \/etc\/default\/zramswap <span>sudo <\/span>systemctl restart zramswap <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>This gives you ~4.3 GB of zstd-compressed swap (assuming 6 GB of VM RAM). zstd compresses roughly 3:1 on typical workload data, so it&#8217;s like having ~13 GB of effective memory.<\/p>\n<h3> <a name=\"66-verify\" href=\"#66-verify\"> <\/a> 6.6 Verify <\/h3>\n<div>\n<pre><code><span>sudo <\/span>docker run <span>--rm<\/span> hello-world <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Expect: <\/p>\n<div>\n<pre><code><span>Unable to find image 'hello-world:latest' locally ... pulling layers ... Hello from Docker! This message shows that your installation appears to be working correctly. <\/span><\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>The pull takes ~75 seconds (5 KB image, but TCG). The run takes ~15 seconds after that. <strong>If you see &#8220;Hello from Docker!&#8221;, you&#8217;re done with the hard part.<\/strong><\/p>\n<hr>\n<h2> <a name=\"step-7-make-it-survive-phone-reboots\" href=\"#step-7-make-it-survive-phone-reboots\"> <\/a> Step 7: Make it survive phone reboots <\/h2>\n<p>If the phone reboots (battery died, OS update, accidental power button press), you want QEMU to come back automatically. Termux:Boot handles this.<\/p>\n<h3> <a name=\"71-create-the-termuxboot-scripts\" href=\"#71-create-the-termuxboot-scripts\"> <\/a> 7.1 Create the Termux:Boot scripts <\/h3>\n<p>Back in Termux (via <code>ssh phone-termux<\/code>): <\/p>\n<div>\n<pre><code><span>mkdir<\/span> <span>-p<\/span> ~\/.termux\/boot <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Create the first script \u2014 auto-start QEMU: <\/p>\n<div>\n<pre><code><span>cat<\/span> <span>&gt;<\/span> ~\/.termux\/boot\/01-start-vm.sh <span>&lt;&lt;<\/span><span>'<\/span><span>EOF<\/span><span>' #!\/data\/data\/com.termux\/files\/usr\/bin\/bash sleep 15 termux-wake-lock 2&gt;\/dev\/null pkill -9 -f qemu-system-aarch64 2&gt;\/dev\/null sleep 2 bash ~\/boot-debian-mon.sh <\/span><span>EOF <\/span><span>chmod<\/span> +x ~\/.termux\/boot\/01-start-vm.sh <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>The <code>sleep 15<\/code> gives Android time to finish booting before we start hammering the CPU. The <code>pkill<\/code> is a safety net in case QEMU somehow came back half-alive.<\/p>\n<p>Create the second script \u2014 auto-start Termux sshd: <\/p>\n<div>\n<pre><code><span>cat<\/span> <span>&gt;<\/span> ~\/.termux\/boot\/02-start-sshd.sh <span>&lt;&lt;<\/span><span>'<\/span><span>EOF<\/span><span>' #!\/data\/data\/com.termux\/files\/usr\/bin\/bash sleep 5 sshd <\/span><span>EOF <\/span><span>chmod<\/span> +x ~\/.termux\/boot\/02-start-sshd.sh <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h3> <a name=\"72-verify-the-scripts-are-registered\" href=\"#72-verify-the-scripts-are-registered\"> <\/a> 7.2 Verify the scripts are registered <\/h3>\n<p>If you&#8217;ve opened Termux:Boot once (Step 1), Termux automatically picks up scripts in <code>~\/.termux\/boot\/<\/code> and runs them at boot. To confirm: <\/p>\n<div>\n<pre><code><span>ls<\/span> <span>-la<\/span> ~\/.termux\/boot\/ <span># Should show:<\/span> <span># -rwx------ 1 u0_a892 u0_a892 ... 01-start-vm.sh<\/span> <span># -rwx------ 1 u0_a892 u0_a892 ... 02-start-sshd.sh<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h3> <a name=\"73-test-it-optional-but-recommended\" href=\"#73-test-it-optional-but-recommended\"> <\/a> 7.3 Test it (optional but recommended) <\/h3>\n<p>Reboot your phone the normal Android way. After it restarts:<\/p>\n<ol>\n<li>Wait ~5 minutes for Android to fully boot.<\/li>\n<li>Wait another ~20 minutes for QEMU to cold-boot Debian.<\/li>\n<li>From your computer: <code>ssh phone-vm hostname<\/code> should return <code>docker-phone<\/code>.<\/li>\n<\/ol>\n<p>If that works, your phone is a self-healing Docker host. You can unplug it, plug it back in, reboot it, whatever \u2014 it&#8217;ll come back.<\/p>\n<hr>\n<h2> <a name=\"step-8-use-it-from-your-computer-like-its-local\" href=\"#step-8-use-it-from-your-computer-like-its-local\"> <\/a> Step 8: Use it from your computer like it&#8217;s local <\/h2>\n<p>Typing <code>ssh phone-vm docker run ...<\/code> for everything gets old. Let&#8217;s make the phone feel like a local Docker host.<\/p>\n<h3> <a name=\"81-create-a-docker-context\" href=\"#81-create-a-docker-context\"> <\/a> 8.1 Create a Docker context <\/h3>\n<p>On your <strong>computer<\/strong>: <\/p>\n<div>\n<pre><code>docker context create phone <span>--docker<\/span> <span>\"host=ssh:\/\/sulthon@192.168.0.9:2222\"<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>(Change <code>sulthon<\/code> to your VM username.)<\/p>\n<p>Docker&#8217;s SSH integration bypasses your <code>~\/.ssh\/config<\/code> aliases, so you need to add the phone to <code>~\/.ssh\/known_hosts<\/code> explicitly: <\/p>\n<div>\n<pre><code>ssh-keyscan <span>-p<\/span> 2222 <span>-t<\/span> ed25519 192.168.0.9 <span>&gt;&gt;<\/span> ~\/.ssh\/known_hosts <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Verify: <\/p>\n<div>\n<pre><code>docker <span>--context<\/span> phone ps docker <span>--context<\/span> phone images docker <span>--context<\/span> phone run <span>--rm<\/span> hello-world <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h3> <a name=\"82-use-docker-compose-against-the-phone\" href=\"#82-use-docker-compose-against-the-phone\"> <\/a> 8.2 Use Docker Compose against the phone <\/h3>\n<p>Anywhere you have a <code>docker-compose.yml<\/code>: <\/p>\n<div>\n<pre><code>docker <span>--context<\/span> phone compose up <span>-d<\/span> docker <span>--context<\/span> phone compose logs <span>-f<\/span> docker <span>--context<\/span> phone compose down <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>This is the magic. <strong>The phone is now indistinguishable from a remote Docker host.<\/strong><\/p>\n<h3> <a name=\"83-optional-make-it-the-default\" href=\"#83-optional-make-it-the-default\"> <\/a> 8.3 (Optional) Make it the default <\/h3>\n<div>\n<pre><code>docker context use phone <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Now <code>docker ps<\/code> (without <code>--context<\/code>) hits the phone. Be careful with this \u2014 it&#8217;s surprising when you forget and accidentally push an image to the phone over Wi-Fi instead of your local Docker. I prefer to leave it as <code>--context phone<\/code> explicitly.<\/p>\n<hr>\n<h2> <a name=\"troubleshooting-the-5-things-most-likely-to-break\" href=\"#troubleshooting-the-5-things-most-likely-to-break\"> <\/a> Troubleshooting: the 5 things most likely to break <\/h2>\n<h3> <a name=\"1-qemu-died-after-my-ssh-disconnected\" href=\"#1-qemu-died-after-my-ssh-disconnected\"> <\/a> 1. &#8220;QEMU died after my SSH disconnected&#8221; <\/h3>\n<p>You forgot <code>setsid<\/code> and <code>disown<\/code> in the launcher script, or you launched it manually with <code>nohup<\/code> (which is not enough). Use the script from Step 4 as-is. Never launch QEMU directly with <code>qemu-system-aarch64 ...<\/code> \u2014 always go through the script.<\/p>\n<h3> <a name=\"2-docker-pull-fails-with-tls-handshake-timeout\" href=\"#2-docker-pull-fails-with-tls-handshake-timeout\"> <\/a> 2. &#8220;Docker pull fails with TLS handshake timeout&#8221; <\/h3>\n<p>You skipped the <code>\/etc\/docker\/daemon.json<\/code> config in Step 6.4, or the config is malformed. Verify: <\/p>\n<div>\n<pre><code>ssh phone-vm <span>cat<\/span> \/etc\/docker\/daemon.json ssh phone-vm <span>sudo <\/span>systemctl restart docker <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Then retry. If it still fails, check that <code>max-concurrent-downloads<\/code> is set to <code>1<\/code> and not to a higher number.<\/p>\n<h3> <a name=\"3-termux-sshd-isnt-running-after-reboot\" href=\"#3-termux-sshd-isnt-running-after-reboot\"> <\/a> 3. &#8220;Termux sshd isn&#8217;t running after reboot&#8221; <\/h3>\n<p>Either you forgot to open Termux:Boot once (Step 1), or the <code>~\/.termux\/boot\/02-start-sshd.sh<\/code> script isn&#8217;t executable. Fix: <\/p>\n<div>\n<pre><code>ssh phone-termux <span>chmod<\/span> +x ~\/.termux\/boot\/02-start-sshd.sh <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>And open the Termux:Boot app icon on your phone, just to be safe.<\/p>\n<h3> <a name=\"4-the-vm-boots-to-a-uefi-shell-instead-of-debian\" href=\"#4-the-vm-boots-to-a-uefi-shell-instead-of-debian\"> <\/a> 4. &#8220;The VM boots to a UEFI shell instead of Debian&#8221; <\/h3>\n<p>This is a known issue with Debian cloud images on some QEMU versions. The fix is to write a <code>startup.nsh<\/code> script to the EFI System Partition. This is fiddly \u2014 you need to boot Alpine from ISO with your qcow2 as a data disk, mount the ESP, and write the file. I&#8217;ll cover this in detail in a follow-up post. For now, if this happens, leave a comment on this post and I&#8217;ll walk you through it.<\/p>\n<h3> <a name=\"5-everything-worked-yesterday-but-today-its-broken\" href=\"#5-everything-worked-yesterday-but-today-its-broken\"> <\/a> 5. &#8220;Everything worked yesterday, but today it&#8217;s broken&#8221; <\/h3>\n<p>You hit Android&#8217;s phantom process killer. Modern Android (12+) kills background processes that use too much CPU. The fixes:<\/p>\n<ol>\n<li> <strong>Keep the phone plugged in.<\/strong> Battery-saver mode is brutal.<\/li>\n<li> <strong>Keep <code>termux-wake-lock<\/code> active.<\/strong> Run <code>ssh phone-termux termux-wake-lock<\/code> to verify.<\/li>\n<li> <strong>Disable Samsung Game Tuning \/ GOS<\/strong> if you&#8217;re on a Samsung device. It throttles sustained CPU workloads. Settings \u2192 Gaming Services \u2192 Game Booster \u2192 Maximum Performance (or similar).<\/li>\n<li> <strong>Check Settings \u2192 Battery \u2192 Termux \u2192 Unrestricted.<\/strong> This should be automatic via Termux:Boot, but if it&#8217;s not, set it manually.<\/li>\n<\/ol>\n<hr>\n<h2> <a name=\"what-to-do-next\" href=\"#what-to-do-next\"> <\/a> What to do next <\/h2>\n<p>You now have a working Docker host that costs $0\/month and fits in your pocket. Some ideas:<\/p>\n<ul>\n<li> <strong>Run a personal <a href=\"https:\/\/traefik.io\/\" target=\"_blank\" rel=\"noopener noreferrer\">Traefik<\/a> + whoami demo<\/strong> to verify compose works end-to-end. Port 8080 is already forwarded.<\/li>\n<li> <strong>Self-host <a href=\"https:\/\/www.postgresql.org\/\" target=\"_blank\" rel=\"noopener noreferrer\">Postgres<\/a><\/strong> for local dev. Use a volume so data survives container restarts.<\/li>\n<li> <strong>Run <a href=\"https:\/\/containrrr.dev\/watchtower\/\" target=\"_blank\" rel=\"noopener noreferrer\">Watchtower<\/a><\/strong> to keep your containers auto-updated.<\/li>\n<li> <strong>Add <a href=\"https:\/\/tailscale.com\/\" target=\"_blank\" rel=\"noopener noreferrer\">Tailscale<\/a><\/strong> to the VM so you can reach your Docker host from anywhere \u2014 not just your Wi-Fi.<\/li>\n<li> <strong>Build images on the phone.<\/strong> <code>docker build<\/code> works fine (slowly) under TCG. Useful for iterating without hitting your laptop&#8217;s battery.<\/li>\n<\/ul>\n<p>The phone-as-server dream is real. It just takes QEMU to get there. Enjoy your free Docker host.<\/p>\n<hr>\n<p><em>If you find bugs in this tutorial, please leave a comment \u2014 I&#8217;ll keep it updated. Happy hacking.<\/em><\/p>\n<\/p><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p>Fuente: <a href=\"https:\/\/dev.to\/sulthonzh\/run-real-docker-on-android-no-root-no-tricks-just-qemu-15jn\">Art\u00edculo original<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Phone battery tip: If your phone has a &#8220;Protect Battery&#8221; or &#8220;Maximum 85%&#8221; setting (Samsung does), turn it on. The phone will be plugged in 24\/7, and capping the charge doubles the battery&#8217;s lifespan. The architecture (mental model) Here&#8217;s what we&#8217;re building: \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 Your Android phone (not rooted) \u2502 \u2502 \u2502 \u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":2648,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[41],"tags":[],"class_list":["post-2888","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devto"],"jetpack_publicize_connections":[],"_links":{"self":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/2888","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/comments?post=2888"}],"version-history":[{"count":0,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/2888\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media\/2648"}],"wp:attachment":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media?parent=2888"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/categories?post=2888"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/tags?post=2888"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}