Recently, I was sitting in a restaurant with my MacBook Air powered off inside my backpack. I opened Codex on my Android phone and asked it to make a change to an Expo app I was working on. I use Codex as a coding agent connected to a development workspace running on another machine.
The repository wasn't on the phone. It wasn't on the MacBook either. It lived on an old gaming PC running Ubuntu at home, along with the coding agent, the backend, the database, Metro, and the Android build tools.
When the change was ready, I switched from Codex to the app on the same phone and saw the result there.
I had originally built this setup because I wanted to use the hardware I already owned more efficiently. The restaurant was when I realized I had changed something more important.
If my laptop was off, where was “local development” actually happening? The tower was a modest personal PC I had used for gaming. It has 16 GB of RAM, a Ryzen 5 5600G, and roughly 1 TB of storage. It wasn't a workstation I bought for AI agents. It was hardware I already had at home.
My first attempt was to keep Windows and use WSL, Docker, and remote desktop from my 16 GB MacBook Air. Each part made sense on its own, but the combination felt awkward. Windows needed memory, WSL needed memory, Docker needed memory, and I was also paying the cost of rendering and streaming a remote desktop I didn't
really want.
I could make it work, but I couldn't work comfortably. The machine spent too much of its limited capacity supporting the environment around development instead of the services I wanted to run.
So I installed Ubuntu on the tower. The goal was simple: let that machine focus on repositories, Docker containers, databases, development servers, builds, and coding agents. My MacBook could keep doing what it was already good at: running the browser, the editor interface, and everything that required my attention.
I don’t have a clean before-and-after RAM benchmark from the Windows setup, so I won’t pretend this was a scientific comparison. However, I can say that the split felt significantly better in daily use.
The MacBook and the tower now have different jobs. The MacBook Air is my main client. I use VS Code, Codex, the browser, and the usual communication tools there. The Ubuntu tower owns the codebases and the processes around them: OpenSSH, Docker, PostgreSQL, backends, frontend servers, Metro, Android builds, and the agent tools that need access to all of it.
Tailscale connects the machines through a private network and gives them stable names. I use regular OpenSSH on top of that network. This is not Tailscale SSH, which is a separate feature.
With VS Code Remote SSH, I can open a repository on Ubuntu from the MacBook and use it almost as if the files were local. The integrated terminal runs on Ubuntu.
Extensions that need access to the code run there too. When I start a development server, port forwarding exposes it on the MacBook through localhost
.
That last detail matters more than it sounds. I didn't want to remember private addresses every time I launched a web app or changed its configuration depending on where I was. From the browser on my MacBook, a service running on the tower can still feel like a normal local server.
At this stage, I thought I had built a cleaner remote development environment. Useful, but not especially surprising.
The bigger difference appeared when I started letting coding agents handle longer tasks. The repository, development services, and agent tools were all on Ubuntu, when I closed the MacBook, that environment didn't go to sleep with it.
I could launch work, leave the laptop behind, and return later from another Codex client without rebuilding the environment on a temporary cloud VM or keeping my personal laptop awake.
When I inspected the tower for this article on July 20, it had been running continuously since July 3. That isn't a reliability benchmark, but it matches how I now treat the machine. It is infrastructure rather than a device I open and close throughout the day.
A more powerful MacBook would compile faster and run more services at once. It would still be a laptop. It sleeps, travels with me, runs my browser, joins video calls, and gets used for unrelated work. Moving the environment gave me a kind of persistence that a hardware upgrade alone wouldn't provide.
Not long after I started experimenting with this setup, I watched Theo Browne's Why I'm moving to Linux (for real). His setup is far more ambitious than mine, but one part felt familiar: moving coding-agent workloads away from the MacBook in front of you and onto Linux machines that can keep working remotely.
My version wasn't a fleet of servers. It was one reused gaming PC. But it was enough to change the lifecycle of my development work.
Web development already worked well from this environment. I could run a server on Ubuntu, forward the port, and open it from the MacBook. Android made the setup more interesting.
I installed ADB and the Android build toolchain on Ubuntu. The tower could now compile an Expo development build, install the APK, and open it on my physical Android phone. The backend, PostgreSQL, and Metro still ran on Ubuntu.
The first networking problem was getting Ubuntu to reach a phone that wasn't on the same physical network. Both devices were connected to Tailscale, so each had a private address reachable inside that network. Android's wireless debugging opened a dynamic ADB port on the phone, and Ubuntu could connect to that port through the phone's Tailscale address.
The second problem went in the opposite direction. The app expected its backend on localhost:3000
and Metro on localhost:8081
, but on Android, localhost
means the phone, not the Ubuntu tower.
ADB solved that boundary with two reverse mappings:
localhost:3000
on Android reached the backend on Ubuntu.localhost:8081
on Android reached Metro on Ubuntu.In plain language, adb reverse
made the phone's localhost point back to the development services on the tower. Tailscale let Ubuntu reach Android. Once ADB was connected, the reverse mappings let the Android app reach Ubuntu.
For normal JavaScript or TypeScript changes, I didn't need another native build. Metro served the updated bundle to the development build already installed on the phone. Changes to native dependencies, permissions, or configuration still required rebuilding and reinstalling the app that now with adb connected the agent can install on its own
I later recorded a separate demo with Synapse, the personal AI companion I built for my wife. This wasn't the restaurant session or the same feature. I wanted a recording that showed the mechanics from beginning to end.
From the Android phone, I paired wireless debugging with Ubuntu through the phone's Tailscale address. Codex established the ADB connection, restored the reverse mapping for Metro, and tried to open the Synapse development client. It then discovered that Synapse wasn't installed on the phone.
For this demo, the development build came from Expo services instead of being compiled locally on the tower. The build took about 25 minutes. Once it was ready, Codex downloaded the APK to Ubuntu, installed it through remote ADB, restored the Metro tunnel, and launched Synapse on the phone.
Then I used Codex on that same Android device to request a visible change:
the phone directed the coding agent and received the result.
The restaurant was the first time all of these pieces felt like one coherent workflow. The Ubuntu tower was at home. My MacBook was off in my backpack. The Android phone in my hand was connected to a hotspot from another phone, and both the development phone and Ubuntu were reachable through Tailscale.
That hotspot was necessary for a slightly annoying reason. Android wireless debugging is designed around Wi-Fi. Mobile data alone
wasn't enough to keep the wireless debugging mode available on my phone.
Tailscale solved the route between devices, but it couldn't make Android treat a cellular connection as Wi-Fi.
Connecting the development phone to another phone's hotspot satisfied Android's Wi-Fi requirement. Tailscale still provided the private path back to Ubuntu, so ADB on the tower could reach the wireless-debugging port even though one device was at home and the other was with me at the restaurant.
I opened Codex on Android and requested a change. The agent had the real repository, its dependencies, the backend, and the tools it needed on Ubuntu.
When the change was ready, I switched to the Expo app and verified it on the same phone.
This wasn't conventional coding on a phone. I wasn't editing TypeScript with a small keyboard or running Gradle on Android. The phone was doing two different jobs: it was the control surface for the coding agent and the physical device receiving the result.
My MacBook Air is still the best place for focused editing, browser work, and long development sessions. This setup didn't make it obsolete. It changed its role.
The codebase, services, tools, and agent environment now persist together on Ubuntu. The MacBook is one client for that environment. Android became another client for starting, steering, observing, and testing certain tasks.
This is why the setup now feels better to me than simply buying a more powerful laptop. A larger laptop could run more work locally, but the environment would still be attached to the device I carry around and close at the end of a session.
I notice the difference at my full-time job. There, I run the codebase and services locally and work with Claude Code on the same laptop. It is a familiar and capable setup, but now I miss being able to open my phone, request a full-stack change, and know that the complete environment will remain available even if the laptop is locked or turned off.
I thought I was giving an old gaming PC a second life. What changed was which machine my development work depended on.
Now, I’m contemplating what could be achieved with a more powerful development box or even a cluster of them.
Are your coding agents still tied to the laptop in front of you, or have you moved the environment somewhere persistent?
If you are building AI products and want to follow along, I write about the problems and trade-offs that appear when someone actually uses them. Follow me: