Setting up a remote environment for agentic coding on a VPS A developer moved AI coding sessions from a laptop to a VPS using Tailscale for private networking, a browser-based editor GUI, and persistent session management, eliminating the need to keep the laptop awake and enabling seamless device handoff. The setup allows closing the laptop without interrupting work, surviving dropped connections, and accessing the same session from any device on the tailnet. For 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 . 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 https://apps.apple.com/us/app/amphetamine/id937984704 , 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. If you’ve used Claude Code’s Remote Control https://code.claude.com/docs/en/remote-control 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. 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. What you get what-you-get 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. 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. 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”. 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. 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. 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. Tailscale: the network that makes it private tailscale-the-network-that-makes-it-private 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. Tailscale https://tailscale.com solves this cleanly. It’s a mesh VPN built on WireGuard https://www.wireguard.com/ , 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. 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: bash $ sudo ufw status verbose Status: active Default: deny incoming , allow outgoing , disabled routed To Action From -- ------ ---- 9999/tcp ALLOW IN Anywhere SSH break-glass, public 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 and nothing more. If you take one idea from this post, take this one: put the box on a tailnet and keep it off the public internet. The rest is detail. The server: a standard Ubuntu 26.04 LTS VPS the-server-a-standard-ubuntu-2604-lts-vps The server itself is deliberately unremarkable. It’s a DigitalOcean droplet running Ubuntu 26.04 LTS, 2 vCPUs, about 8 GB of RAM, 28 GB of disk. Any provider works. Hetzner, Vultr, whoever. You want enough RAM to run a few agents plus a database or two at once, so I’d aim for 8 GB rather than 4, and add a bit of swap. On top of the base OS it’s just a normal dev machine: git, tmux, the language runtimes I use PHP, Node, a database , and Docker for anything that’s easier in a container. I also turn off automatic updates on this box, which feels wrong until you think about it. Unattended-upgrades can decide to reboot at 3am, and a reboot kills every running tmux session and every in-flight agent. That’s the exact thing this setup exists to prevent. So I patch by hand, when I choose to, and let the sessions live: bash $ sudo apt-get purge unattended-upgrades $ sudo systemctl mask apt-daily.timer apt-daily-upgrade.timer Long-running work goes in tmux , one session per project. Detach with Ctrl-b d , reattach from any device with tmux attach . That’s the fallback layer that survives everything, including the fancier GUI on top of it dying. If the browser editor ever breaks, ssh in and tmux attach and I’ve lost nothing. The coding GUI: T3, but bring your own the-coding-gui-t3-but-bring-your-own This layer is the least settled of the three, and that’s fine. I want a proper editor I can drive from a browser, including my phone’s browser, so I’m not squinting at tmux on a 6-inch screen when I just want to review a diff and approve a change. The server runs the editor as a service; I open a URL and there’s my project, my files, my git state, my agents. Right now I use T3 Code https://github.com/pingdotgg/t3code for this. It’s a web GUI that runs on the server not on my Mac , speaks to Claude Code and Codex, does git worktrees, shows diffs, and opens pull requests. It has first-class Tailscale support, so t3 serve --tailscale-serve publishes it on tailnet-only HTTPS and I open that URL from anything. It’s early software, the project says so itself, and I’ve hit rough edges. But it does the job today. This piece is swappable, and I’d expect to swap it. The whole space of “browser-based agent coding UIs” is moving fast, and a year from now the good option might be something else entirely. That’s fine. Nothing else in this setup depends on the specific GUI. The network is Tailscale, the box is a plain Linux server, the work lives in git and tmux . If I rip out T3 tomorrow and drop in a different editor, the foundation doesn’t move. Pick whatever’s good now , and don’t marry it. So don’t take T3 as the recommendation here, take the shape as the recommendation. A few others solve the same problem already: Emdash https://emdash.ai/ and Orca https://github.com/stablyai/orca are open-source apps that run agents in parallel git worktrees, with remote-over-SSH execution so the runtime can sit on another box. CloudCLI https://github.com/siteboon/claudecodeui and Paseo https://paseo.sh/ go the web route like T3: a self-hosted server you reach from a browser or your phone. I haven’t run all of them in anger, so this isn’t a leaderboard. The point is that the layer is a commodity now, and swapping it doesn’t touch anything underneath. The rest of the post is the durable part. No code editor no-code-editor I haven’t opened a real code editor in months. No VS Code, no PhpStorm, no vim beyond a quick :wq on a config file. And I don’t miss it. That’s the biggest change in how I work, and I didn’t plan it. I assumed the browser GUI was me swapping my editor for a worse one I could reach from my phone. It turns out I don’t use it as an editor at all. I open T3 to read a diff, approve a change, start the next task, and watch an agent work. I almost never type code into a file by hand. The loop now is: describe what I want, let the agent write it, read the diff, run the tests, say what’s wrong, go again. The editor used to be where I did that middle step. It’s mostly gone, and what’s left is the two ends, deciding what to build and judging whether it’s right. Those are the parts I want to be doing anyway. So what I tuned the setup for isn’t editing, it’s that loop: how fast I get from an idea to a diff I can judge, from anywhere, without a machine I have to keep awake. A snappy editor does nothing for that. A server that runs the suite in a couple of seconds and shows me the result on my phone does. I’m not claiming hand-coding is dead. I still read every line before it merges, and I still drop into the code when an agent is flailing. But the editor as the place I spend my day is over. The custom sauce the-custom-sauce Everything above is the skeleton. These are the tweaks that make it feel like mine rather than a generic tutorial, and they’re the bits I’d miss if they were gone. The agents update themselves every morning the-agents-update-themselves-every-morning Claude Code, Codex, and T3 all ship new versions constantly. I don’t want to npm update three tools by hand every morning, and I don’t want stale versions either. A systemd timer updates all three at 05:00 my time, before I’m awake, so whatever I open is already current. It runs on UTC as servers should , but I pin the schedule to my actual timezone, so daylight saving never shifts it: Timer Box runs UTC; the explicit timezone keeps this at 05:00 local year-round. OnCalendar= - - 05:00:00 Europe/Brussels Persistent=true If that looks like it clashes with turning off automatic OS updates earlier, the difference is what can reboot the box. An OS upgrade can, and it drags every session down with it. These don’t touch the OS: Claude and Codex just swap a binary the next run picks up, and T3 only restarts itself. That restart still drops a live T3 connection, which is exactly why it runs at 05:00 and not while I’m working. T3 gets the careful treatment because it’s alpha software backing the UI I work in: after each update the timer health-checks it, and if the new version doesn’t come back up, it rolls back to the previous one automatically. I tested both paths on the way in, a real upgrade and a forced failure, because an auto-updater you haven’t watched roll back is just a hope. Onboarding a workspace in one command onboarding-a-workspace-in-one-command Adding a project shouldn’t be a checklist I half-remember. newproject clones the repo, provisions its databases, writes its .env , and registers it in T3, which can’t add a project from its own UI on a remote box. The whole script is about a dozen lines: bash /usr/bin/env bash set -euo pipefail URL="${1:?usage: newproject