I moved all my development off my laptop. Now everything runs on an always-on Hetzner VPS, and my MacBook is just a thin client. This was the best decision I made this year. The setup is simple, and working like this is really nice. No issues, no lags. Everything is fast and responsive.
My Mac now runs almost nothing. Ghostty as the terminal, Tailscale for the network, Secretive for SSH keys, and a few other small tools. That is it. The rest lives in the cloud.
My first try: local VMs
Some time ago I built dvm.eshlox.net and used it for about two to three months. It ran my dev environments in local virtual machines on the MacBook. It worked very well. The only problem was resources. Running ten or more VMs at once ate all my RAM and killed the battery fast. The new setup fixes this completely. The same kind of isolated environments now run in a datacenter, so my laptop does almost nothing.
The server
The server is a Hetzner Cloud box, defined as code with OpenTofu. I can rebuild it from scratch any time. I default to a cpx42 with about 16 GB of RAM and size up when I need more. Going one size up is just a reboot.
The box is invisible to the public internet. The Hetzner firewall blocks everything except Tailscale. No public SSH, no open HTTP. All my traffic, SSH included, goes over the tailnet. The server has a public IP, but from the outside it looks like a black hole.
One thing to know: Docker writes its own firewall rules and can bypass the host firewall. So the Hetzner cloud firewall, which sits in front of the host, is the real security boundary.
Tailscale also runs in SSH mode, so there is no SSH key stored on the box at all. I connect over the tailnet, no key needed.
ssh dev@dev-box # over Tailscale, no key neededbox sh myproject # attach the project's tmux sessionSecurity updates install on their own, but the box never reboots by itself. An automatic reboot at night would kill my long agent runs and tmux sessions, so I reboot by hand when the box is idle.
One container per project
The tool I use all day is box, a single bash script of about 700 lines. It gives every project its own Docker container on the server. No VS Code, no devcontainer spec. Every step is a plain docker command I can read.
The model is simple. A base image holds all my tools and dotfiles. A project is a tiny Dockerfile that starts from that base, plus an optional compose file for services and an optional config for public URLs. Each project gets its own container, its own network, and two volumes: one for the code, one for the home directory.
The container is disposable. The volumes hold the state. I can rebuild a container on a fresh base any time and keep all my work. box update does exactly that. It throws away the container, rebuilds it on the latest base, and keeps both volumes. My runtime stays clean and current while my code, shell history, and credentials survive.
Closing the laptop is safe
tmux runs inside the container on Hetzner, not on my Mac. So nothing depends on my SSH connection staying alive. I can start a long agent run, close the MacBook lid, walk away, and reconnect hours later to the same session, still running.
This is also why agents are easy to run unattended. I start Claude Code or Codex on a task, go to sleep, and check the result in the morning.
It also means I am not tied to the MacBook. Anything with Tailscale and an SSH client can reach the box. From my iPhone, my iPad, or any other machine I can connect to the same session, check how a task is going, run a quick AI prompt, and keep working. So I can do something from almost everywhere.
The heavy work is not on my laptop
Now Hetzner takes care of the boring parts: internet connection, power, and hardware. I also get daily automatic backups of the whole box, so a point-in-time restore is always there.
This also helps a lot on slow internet. The big downloads do not happen on my laptop anymore. Every npm install, every Docker image pull, every build runs on the Hetzner server over its fast datacenter link. On my side only SSH needs to work, and SSH needs almost no bandwidth. So I can work from a train, a hotel, or a weak mobile connection and it still feels fast.
Databases and public URLs
When a project needs a database, it gets a compose file. These services run on the host Docker engine, not inside the dev container. So the untrusted code in my workspace never gets the host Docker socket. box up also checks the compose file and blocks dangerous things like privileged mode or socket mounts.
To share a dev server, like showing a client a live preview, each project can use a Cloudflare tunnel. One command serves the routes and gives me a public URL with free SSL.
I am on Linux now, and I like it more
The whole dev environment runs on Linux. This quietly fixed a long list of problems I had on the Mac. No more trouble with the Apple M chips, Rosetta, or x86 against ARM images. Tools just work. I like working on Linux much more than on macOS, and this setup gives me Linux for the real work while keeping the MacBook as a nice client. For me it is ideal.
Why I trust it with agents
I run AI agents in full-auto mode, so this part matters to me. The container is the security boundary, not the agent prompt. Inside the container the agent has full permissions, but the damage it can do stays inside that one project’s volumes.
A few layers hold this together. The host is invisible behind the Tailscale-only firewall. There are no SSH keys on the box. Projects are isolated from each other with separate containers, networks, and volumes, so a bad dependency in one project cannot reach another. Untrusted code never gets the Docker socket. My Mac stays untouched, because the agents live only on Hetzner. And my git key is hardware-backed and gated by Touch ID, so every push and every signed commit asks for my fingerprint on the Mac. An agent cannot use my identity in silence.
I am honest about the limits. Containers share the host kernel, and Touch ID stops silent abuse but not a prompt I approve at the wrong moment. For truly untrusted code I would use a separate disposable VM. But for my own projects with agents doing the work, this is the right balance.
The result
This is the most comfortable way I have found to work, especially with AI coding tools. One always-on server, one container per project, agents contained, everything reproducible from git. My laptop is quiet and cool, the battery lasts all day, and if the MacBook dies I just install Tailscale and an SSH client on any machine and I am back where I was.
It is my personal setup for now, about 700 lines of bash and a small OpenTofu module. I might clean it up and open source it one day. If I do, I will write a follow-up.