Jay Stewart
Infrastructure

My development environment was a server

One always-on machine, provisioned like production, that was simultaneously a warm CI runner and the place I worked — why, what it cost, and the reversal thirteen days later.


Contents
  1. 01The laptop was the bottleneck
  2. 02One box, two jobs
  3. 03Keeping CI from starving the work
  4. 04Closed to the internet
  5. 05Sessions that outlive the connection
  6. 06What it cost

Update, 10 August 2026. Thirteen days after this was published, I destroyed the box and reversed the model: the laptop — now a 16 GB M4 Air — is the workstation again. The CI half had already stopped paying for itself; every workflow had moved back to GitHub-hosted runners, and the main system’s merge gate now runs on the dev machine at push time. What the reversal bought back is the two costs at the bottom of this piece that a server could never fix: the hard dependency on connectivity — with no fixed base, a dead link was a lost day — and the hardware that emulators and Xcode need. The first cost did not disappear; the single point of failure moved into my bag, and it is managed by pushing everything daily rather than by backups. The piece stays as written, because the reasoning was sound on the day it was published, and the costs it named honestly are the ones that won.

Not a container and not a hosted workspace. One always-on machine, provisioned with the same infrastructure-as-code discipline as production, that is simultaneously a warm CI runner and the place I do my work.

The laptop was the bottleneck

I run several coding-agent sessions at once. On a machine with 8 GB of memory that is not a workflow, it is a queue. Builds and agents compete, and the laptop spends its time swapping.

The obvious fix is a bigger laptop. The better fix was noticing that almost none of that work needs to happen where I am sitting. So the compute moved to a cloud box — 8 vCPU, 16 GB — and the laptop became what it had quietly already been: a terminal and a browser.

One box, two jobs

The same machine is also the CI runner, and that is the part that actually pays for it.

Ephemeral runner pools are correct for most projects and wrong for this one. A pool that scales to zero takes 30 to 60 minutes to come back, and a queued job that waits an hour is indistinguishable from broken CI — you stop trusting it, then you stop waiting for it, then you start merging without it. A warm runner that is always up because it is also my desk removes that failure mode entirely.

Keeping CI from starving the work

Two jobs on one box means they compete. The interesting part is that CPU and memory need opposite treatment.

CPU is compressible. A build that gets less of it runs slower and still finishes, so CPU is allocated by weight — the interactive session outranks CI by roughly ten to one, and when nothing interactive is happening CI gets the whole machine.

Memory is not compressible. A process denied memory does not run slowly, it dies. So rather than capping CI, the configuration reserves memory for the interactive side and sets CI a soft limit that reclaims under pressure instead of killing.

There is deliberately no hard memory cap on the CI slice. A hard cap does not slow a build down — it gets the process killed, and a build that dies at 90% because a limit was five per cent too tight is worse than a build that swaps for thirty seconds.

Closed to the internet

The box has zero inbound firewall rules. Access is over a mesh VPN, which needs no inbound rule at all because it dials out.

An explicit zero-rule firewall is attached rather than simply leaving the firewall off, because on this provider an unattached firewall means no filtering whatsoever. “No rules” and “no firewall” read the same in a dashboard and are opposites in effect.

Sessions that outlive the connection

Three layers, because there are three different failures:

  • A multiplexer so a session survives the SSH connection dropping.
  • A roaming shell so the connection itself survives a network change.
  • Session persistence so the layout survives a reboot.

The payoff is that the client stops mattering. Nothing is “my machine” any more, which is the point.

What it cost

It is a single point of failure. One box, one region. Backups are configured in code and — at time of writing — have not been applied. A backup that has never been restored is a hypothesis, and I am not going to describe it as anything else.

It is a hard dependency on connectivity. SSH does not exist without a link. The mitigations are real but partial, and on a bad connection the whole setup is worse than a laptop.

No nested virtualisation. This provider’s cloud instances expose no KVM, so Android emulators cannot run on the box. That is not a tuning problem, it needs different hardware, and pretending otherwise wasted an afternoon.

The upside is that the machine in my bag stops being load-bearing — which, for someone who moves around, turned out to matter more than any laptop specification.