April 25, 2026
Remote Development from an iPad
Using an iPad as the client for a personal Google Cloud development setup: Tailscale for daily access, separate operator and workspace machines, self-hosted Coder, app previews, and a tested fallback path.
iPad
joins tailnet
Tailscale private network
Operator VM
Cloud admin, runbooks, fallback access.
Coder VM
Workspaces, app previews, browser editor.
$ ssh coder-machine
$ tmux new -A -s codex
$ codex --yolo
I had an iPad with a Magic Keyboard and a pile of personal software work that needed more than browser tabs.
Buying another laptop would have been the simple answer… but the prices of hardware right now makes my eyes water. I wanted to see how far I could get with the iPad as the client and a Linux machine somewhere else: long-lived sessions, cloud credentials, local previews, and a way back in if I broke access.
Hosted Coder, GitHub Codespaces, and Google Cloud Workstations would all have been reasonable. I had Google Cloud credits, so the cloud VM route was easy to justify while I worked out what I actually needed.
I kept notes as I went because I wanted the setup to be movable later. That could mean another cloud provider. It could also mean a home lab or desktop machine behind the same basic pattern. I did not have access to that hardware during this experiment, so Google Cloud was the path in front of me.
I built it while using it. Codex helped me build and debug most of the setup from inside the same environment: the site, virtual machine configuration, Coder deployment, and domain routing notes.
What This Became
Over the last couple of weeks this turned into a small personal cloud workstation.
I built and now use:
- an Astro personal site and notes system
- Google Cloud with separate operator and workspace roles
- an operator VM for
gcloud, Terraform, runbooks, config, fallback commands, and Codex - a Coder VM for self-hosted Coder, workspace terminals,
code-server, app previews, and project environments - private daily access over Tailscale instead of public SSH
- routes for Coder and workspace app previews
- a small Shortcut to start and stop the cloud machines
- a fallback path through Cloud Shell, , and
- real project environments for this site, finance dashboards, a property-search workflow in my finance app, and cloud runbooks
The iPad is the client. The development environment lives on machines I can start, reach, use, and stop from the same device.
A Normal Session
A normal session usually starts from the iPad:
- Run the Shortcut to start the Coder VM, or both VMs if I need cloud administration.
- Connect over Tailscale.
- Open the Coder workspace terminal, or SSH in and reattach .
- Run Codex, shell tools, tests, and local app servers inside the workspace.
- Preview browser apps through private Coder app routes.
- Open
code-serveronly when the browser-hosted editor is more convenient than the terminal. - Stop the VM from iOS when the session is done.
The keyboard version is usually closer to this:
ssh <coder-vm-alias>
cd ~/workspaces/<repo>
tmux new -As codex
# inside tmux
codex
pnpm dev --host 0.0.0.0
One night the iPad ran out of battery in the middle of a task. I picked up my iPhone, opened the Coder URL, reconnected to the same terminal session, and finished the task from there. That is the practical reason I care about Coder terminals and tmux.
What I Have Used It For
So far that includes:
- this Astro site, including the remote development note
- self-hosted Coder, DNS, and app routing work
- finance dashboards and a property-search workflow inside the same personal app
- cloud runbooks and fallback notes
For the property workflow, the finance app exposes a small Model Context Protocol surface. Codex connects to it from the remote workspace, takes a task like “find properties that fit my criteria against my financial constraints”, and compiles options in the shape the app already consumes. I can inspect the result in the app. For a personal app, I do not need to build a Bedrock, Vercel Gateway, or direct model-provider integration just to get this loop working. I can use the coding agent already running in the VM.
The flow is roughly:
finance app
-> read-only MCP tools and calculators
-> Codex property-finder workspace
-> app review surface
The property workspace is staged so the agent does not rely on one large prompt:
workspaces/property-finder/
00-profile/
01-budget/
02-collection/
03-enrichment/
04-ranking/
05-review/
Each stage writes artifacts forward. That makes the work inspectable from the terminal and from the app.
The Architecture
The live shape is two Google Cloud VMs behind Tailscale:
- an operator VM for cloud administration, Terraform,
gcloud, runbooks, SSH config, and Codex - a Coder VM for self-hosted Coder, workspace terminals, browser editing, and project environments
Both are modest machines: e2-standard-2, 2 vCPU, 8 GB memory. The operator VM has a 50 GB balanced persistent disk. The Coder VM has a 40 GB boot disk and a 30 GB state disk.
Tailscale carries the normal network path. I use regular SSH over the tailnet, so the VMs can stay closed to public SSH for daily work.
The Google Cloud pieces are Compute Engine, IAP, OS Login, Secret Manager, Cloud Storage for Terraform state, and Terraform-managed configuration.
Architecture
The route from iPad to Linux.
The iPad is only the client. Google Cloud runs the cloud VMs, Tailscale carries the daily routes, and provider access stays available as a fallback.
-
Client
iPad / iPhone
Browser, terminal, keyboard, Tailscale client.
-
Network
Tailscale
Normal SSH and browser routes over the tailnet.
-
Compute
Cloud VMs
Operator VM plus Coder VM. Cloud administration stays separate from project work.
-
Primary interface
Shell / Codex / optional code-server
Terminal first. tmux keeps sessions alive. code-server only when a browser editor helps.
-
Fallback
Cloud Shell + IAP + OS Login
A provider path remains available when Tailscale or SSH changes.
Components
What each part does.
The split is simple: client, private network, operator machine, workspace machine, fallback path, and lifecycle control.
Client
iPad or iPhone, Safari, terminal app, keyboard, Tailscale client.
Network
Tailscale tailnet, normal SSH, app routes over the tailnet, no public SSH path.
Operator VM
gcloud command line, Terraform, Codex command line, SSH config, runbooks.
Coder VM
Self-hosted Coder, workspace terminal, optional browser editor, private localhost routing.
Fallback
Cloud Shell, IAP tunnel, OS Login.
Lifecycle
iOS Shortcut, authenticated VM helper, start or stop the operator VM, the Coder VM, or both.
Paths
How I actually get in.
Daily routes use Tailscale. The fallback path exists so access changes do not strand the environment.
Start / stop
iPhone or iPad -> Shortcut -> VM helper -> start or stop operator VM, Coder VM, or both Terminal work
iPad -> Tailscale -> Coder VM -> workspace terminal -> shell / Codex Browser editing
iPad -> Tailscale -> Coder VM -> Coder -> code-server Local app route
iPad browser -> Tailscale -> <port>-<project>.<private-domain>.dev -> workspace app Operations
iPad -> Tailscale -> operator VM -> gcloud / Terraform / Codex Fallback
Cloud Shell -> IAP + OS Login -> operator or Coder VM Why Two VMs
I started with the operator VM. It has the cloud command line, Terraform state awareness, notes, fallback commands, and long-running shells. It is the machine for managing the setup.
Coder came later, after the SSH baseline worked. I wanted to know whether a self-hosted workspace layer was worth running from an iPad every day.
One VM with SSH and Git may be enough. tmux keeps long-running shells alive across disconnects and device switches. For Vim users, or for people mostly using coding agents with small manual edits, that may be the whole setup.
The two-VM version keeps the roles clean. Admin context stays away from project work, and the Coder VM can be resized, rebuilt, or replaced without touching the operator machine.
What Coder Is Doing Here
Coder is the workspace layer here, not the coding agent. Codex is the agent I run in terminal sessions. Coder is the self-hosted web control plane that starts workspaces, gives me browser terminals, and exposes local app previews through private routes.
I kept self-hosted Coder because it solved annoying parts of this setup: workspace startup, browser terminals, private app URLs, and code-server when I want a full editor in the browser. Most days I stay in tmux and shell tools.
Local services use private app routes. The pattern is something like:
coder.<private-domain>.dev
<port>-<project>.<private-domain>.dev
The DNS names are public, but they resolve to tailnet addresses. From outside the tailnet, the names may resolve, but the routes do not work.
dig +short coder.<private-domain>.dev
# 100.x.y.z
# fd7a:...
dig +short <port>--<workspace>--<user>.<private-domain>.dev
# 100.x.y.z
# fd7a:...
DNS handles names. Tailscale handles access.
Access And Fallback
For terminal work, I connect over Tailscale with SSH:
iPad
-> Tailscale
-> SSH to operator VM or Coder VM
For browser work:
iPad Safari
-> Tailscale
-> Coder
-> code-server or workspace app, when I need it
For fallback:
Cloud Shell
-> IAP
-> OS Login
-> VM
Access changes can lock me out, so the fallback path gets tested before I change SSH, firewall rules, identity settings, Tailscale config, or VM startup behavior.
That path is Google Cloud’s documented IAP SSH route, with OS Login handling VM user access.
The command shape is:
gcloud compute ssh <operator-vm> \
--project=<gcp-project-id> \
--zone=<gcp-zone> \
--tunnel-through-iap
I keep the working path open until the replacement path has been proven.
Hardening Order
I did not harden everything at once. I made the daily path work first, then tightened the parts that could realistically cause problems for this setup.
- Build the daily path first: iPad, Tailscale, SSH, tmux, and a working VM.
- Keep the current session open before changing access.
- Verify Cloud Shell, IAP, and OS Login from a second path.
- Move VM configuration into Terraform without destroying the working machine.
- Keep public SSH closed and route normal access through the tailnet.
- Put Terraform state in a remote backend with versioning.
- Add self-hosted Coder only after the SSH baseline worked.
- Treat the iOS Shortcut helper as a sensitive lifecycle control, not a toy endpoint.
People should harden this kind of setup against the risks they actually have. For me, the obvious ones were public access, losing SSH access, leaking config, and leaving machines running when I was done.
Implementation Notes
These are shapes, not live configuration.
The main VM posture is simple: no public SSH path for daily use.
resource "google_compute_instance" "workspace" {
name = var.workspace_vm_name
machine_type = var.workspace_machine_type
zone = var.zone
tags = ["tailnet-only", "iap-ssh"]
boot_disk {
initialize_params {
image = var.boot_image
size = 100
type = "pd-balanced"
}
}
network_interface {
subnetwork = var.subnetwork
# No access_config block here.
# That means no external IPv4 address.
}
metadata = {
enable-oslogin = "TRUE"
block-project-ssh-keys = "TRUE"
}
}
The VM can still reach out through managed egress for package installs and image pulls. Admin access does not need a public SSH endpoint.
IAP SSH has its own firewall shape:
resource "google_compute_firewall" "allow_iap_ssh" {
name = "allow-iap-ssh"
network = var.network
direction = "INGRESS"
source_ranges = ["35.235.240.0/20"]
target_tags = ["iap-ssh"]
allow {
protocol = "tcp"
ports = ["22"]
}
}
That is not public SSH. It allows SSH from Google’s IAP tunnel range to tagged VMs, and Google Cloud Identity and Access Management still decides who can use the tunnel. The public version can show roles, not identities: roles/iap.tunnelResourceAccessor, roles/compute.osLogin, or roles/compute.osAdminLogin depending on whether normal login or admin login is needed.
Terraform state has a similar rule: show the shape, keep the live file out of Git.
terraform {
backend "gcs" {}
}
# backend.hcl.example
bucket = "<terraform-state-bucket>"
prefix = "remote-workbench/<environment>"
terraform init -backend-config=backend.hcl
terraform plan -input=false -no-color
The real backend.hcl, Terraform variable files, plans, keys, and state files stay out of Git. The example file exists so future me knows the shape without publishing the live bucket.
Coder app routes are the other useful piece from the iPad:
resource "coder_app" "web" {
agent_id = coder_agent.main.id
slug = "web"
display_name = "Web"
url = "http://localhost:4321"
share = "authenticated"
}
The template can change. The pattern is the part I care about: the app runs on localhost inside the workspace, and Coder gives the iPad a browser route to it.
I do not need every repo to have a devcontainer. For small projects, a prepared workspace image and pnpm install are enough. If a repo needs stricter tooling, I would add a repo-level devcontainer. The Coder workspace gives me the machine; a devcontainer is only worth adding when the repo itself needs repeatable tooling.
Starting and Stopping It
I could start and stop the machines from the Google Cloud Console app. The Shortcut was part of the exercise: make the whole setup usable from the same iPad, without opening a cloud console every time.
It gives me three choices:
- start or stop the operator VM
- start or stop the Coder VM
- start or stop both
Behind that is an authenticated helper that performs the VM action. The live address, auth details, and project names stay private.
The helper only accepts fixed targets and fixed actions:
target: operator | coder | both
action: start | stop
Its service account can get, start, and stop the allowed VMs. It cannot edit metadata, change firewall rules, mint keys, or create new machines.
compute.instances.get
compute.instances.start
compute.instances.stop
Uptime is the main cost driver, so start and stop needs to be easy.
Cost
I also checked the rough cost after using it for a couple of weeks.
The measured window was April 6 to April 26, 2026 for the operator VM, and April 11 to April 26, 2026 for the Coder VM.
Measured use:
- operator VM running time:
159.29hours - Coder VM running time:
101.70hours - total VM-hours:
260.98hours - at least one VM running:
197.01hours - both VMs running:
63.97hours
Using list prices for the region and resources I used, the Google Cloud estimate came out around $35 to $37 before credits, taxes, currency conversion, or billing adjustments. Across that measured window, that is roughly $1.70 to $1.80 per day.
Rough comparison for the same period:
- current Google Cloud setup:
$35-$37 - one practical 2-core GitHub Codespace:
$37-$39before personal included usage - direct two-machine GitHub Codespaces equivalent:
$50-$53before personal included usage
That includes VM compute, persistent disks, and small networking costs. The tiny storage buckets, Cloud Run helper, and secrets were not material at this scale.
With personal included usage, Codespaces can be cheaper for this kind of period. Without that included usage, the one-Codespace comparison was roughly similar, and the direct two-machine comparison was higher.
Price is only one part of it. Codespaces gives a managed dev container, browser editor, port forwarding, storage, and idle behavior. Hosted Coder or managed workstations would also remove a lot of infrastructure ownership.
If I were paying cash from day one, I would compare the managed options again.
What Is Optional
Almost every part can be removed.
I would start here:
iPad
-> Tailscale
-> one VM
-> SSH + Git
-> tmux for long-running terminal sessions
Add code-server if you want VS Code in the browser. Add Coder if you want workspace lifecycle and app routing. Add a second VM if you want a cleaner separation between operations and project work. Add the Shortcut if starting and stopping cloud resources from iOS is useful.
The parts worth keeping: no public SSH on the VMs; prove Cloud Shell/IAP/OS Login before touching access; give each machine a clear job.
Links
- Coder quickstart, Coder install, and workspace port forwarding
- GitHub Codespaces billing
- Google Cloud Workstations
- Tailscale docs and SSH over Tailscale
- tmux getting started
- Codex CLI docs
- code-server docs
- Google Cloud IAP SSH and OS Login
- Astro content collections
- Model Context Protocol architecture