Setting up a remote environment for agentic coding on a VPS · ma.ttias.beSetting up a remote environment for agentic coding on a VPS
Mattias GeniarFor the last few months my laptop has been the bottleneck in my own workflow. Not the CPU, not the RAM. The fact that it’s a laptop.<br>I run long AI coding sessions now. Claude Code churns away on a feature for twenty minutes, I go make coffee, and if I close the lid the session dies with it. So for a while I did the obvious dumb thing: I kept the Mac awake with Amphetamine<br>, lid open, plugged in, sitting on a desk I then couldn’t walk away from. The machine doing the work had to be the machine in front of me, powered on, present. That’s backwards.
So I moved the whole thing off my laptop. There’s now a small VPS that runs the agents, the code, the databases, and the editor GUI. I reach it from my MacBook at my desk, from the couch, or from my phone on the train. Close the laptop, the work keeps running on the server. Open my phone twenty minutes later and I’m looking at the exact same session, right where it was.<br>If you’ve used Claude Code’s Remote Control<br>to pick a session up from the phone app, it’s a bit like that, except it’s on by default, running all the time, and it’s my own box with none of the limits. Every project I have, not just the one I remembered to start a remote session for.<br>This post walks through how it’s built and why each piece is there. It’s also written so you can hand the whole thing to an LLM and have it build the same setup for you. More on that at the end, but keep it in mind as you read: everything here is a real command I ran on a real box, not a sketch.<br>What you get#<br>The point isn’t “a server in the cloud”. I’ve had those for twenty years. The point is that the machine doing the work is no longer tied to the machine I’m sitting at.<br>I can close my laptop. The agent, the editor, the database, all of it lives on the server. My laptop is a window, not the engine. Shut the lid, get on a train, nothing stops.<br>I hand off between devices instantly. Start a task on the MacBook, check on it from my phone in the kitchen, back to the desk. Same session, same state, no syncing, no “let me push this real quick so I can pull it elsewhere”.<br>It survives a dropped connection. My train goes through a tunnel, SSH drops, the work on the server doesn’t care. I reconnect and reattach.<br>No more caffeine hacks. Amphetamine, Caffeine, caffeinate, the little while true; do; done tricks. All gone. The laptop is allowed to sleep because it isn’t doing anything important.<br>It all rests on three pieces: a network layer (Tailscale), a box (a VPS), and a coding GUI you reach from a browser (I use T3, but that part is swappable). Then a bunch of small tweaks on top that make it pleasant instead of fiddly.<br>Tailscale: the network that makes it private#<br>The first problem with “a server I reach from anywhere” is the “from anywhere” part. You do not want an editor with a shell on it exposed to the open internet. That’s how you end up in someone’s botnet.<br>Tailscale<br>solves this cleanly. It’s a mesh VPN built on WireGuard<br>, but you never touch a WireGuard config. You install a client on each of your devices, log in, and they all end up on one private network (a tailnet) where they can talk to each other directly, encrypted, no matter which network they’re physically on. My laptop, my phone, and the VPS all see each other as if they were on the same LAN, whether I’m at home, on cellular, or on hotel wifi.<br>There are proper native clients for everything: macOS, Windows, Linux, iOS, and Android. On the phone it’s an app-store install and a login, that’s the whole setup. Once a device is on your tailnet it can reach the VPS by name, and nothing outside the tailnet can.
That last part is what makes the whole thing safe. The VPS exposes exactly one port to the public internet: SSH, and even that I keep only as a break-glass door in case Tailscale itself is ever down. Everything else, the editor, the preview servers, the databases, is bound to localhost and published only onto the tailnet. The firewall denies inbound by default and allows the tailnet interface:<br>$ sudo ufw status verbose<br>Status: active<br>Default: deny (incoming), allow (outgoing), disabled (routed)
To Action From<br>9999/tcp ALLOW IN Anywhere # SSH (break-glass, public)<br>Anywhere on tailscale0 ALLOW IN Anywhere # Tailnet (all services)
Tailscale also does the SSH auth for me (tailscale up --ssh), so device-to-device SSH inside the tailnet needs no key juggling, and it can hand out real, valid HTTPS certificates for your machines. That’s how the editor gets a proper https:// URL with no cert warnings, reachable from Safari on my phone, without ever being public. tailscale serve puts a localhost port onto the tailnet over HTTPS...