I built my own AI lab on my desktop: six self-hosted services in rootless Podman, wired to a wide choice of models through OpenRouter, and controlled from desktop plugins I wrote myself. It worked well, and building it taught me a lot.
Over time, though, I was looking after the lab more than it was looking after my projects. So I right-sized it: one AI coding assistant, Claude Code, in a sandbox I designed so it can’t touch my machine. This post covers what I built, why I moved on, and how the new setup keeps an AI agent in its lane.
What I built: an AI lab in Podman
Every service ran as a rootless Podman container, defined as a Quadlet file and managed by systemd:
| Service | What it did |
|---|---|
| Open WebUI | Chat interface for the models |
| OpenHands | An AI agent that could write and run code |
| code-server | VS Code in the browser |
| NoteDiscovery | Notes |
| Homepage | A dashboard linking it all together |
| Caddy | Reverse proxy in front of the lot |
To control it, I wrote two plugins for Noctalia v5, my desktop shell, to bring the containers up and down from the bar. Noctalia v5 was still in beta, and the only plugins available were the demo ones, so I learned its plugin system from those and built my own. The plugins mattered: some of the services were heavy, and this PC has only 8 GB of memory, so I couldn’t leave everything running.

The models came through OpenRouter, which was the best part: one account, and a wide choice of models to pick from for each job.
Why it went
None of it was broken. Building it was the fun part; running it was the time consuming part. In the end I needed only a fraction of what all those tools offered, and I was maintaining the rest anyway.
- Six services to keep updated, each with its own release cycle, settings and quirks.
- Memory to juggle: with 8 GB, I was starting and stopping parts of the lab just to have room for everything else.
- Six web interfaces behind a proxy, which I also had to look after.
- Glue around it all: the Quadlet files, the proxy config, the plugins. All of it was mine to maintain.
- Most of my use was one thing: getting help with real projects. The agent, the editor and the chat were three tools doing parts of one job.
So I right-sized it. What I needed was one tool that covers the whole job (read a project, change it, build it, test it, publish it) without me running a platform for it.
What replaced it: one assistant, one sandbox
Now I work with Claude Code. I make the decisions (architecture, trade-offs, what’s safe to allow), and it does the hands-on work: editing files, running builds, testing pages in a headless browser and preparing commits. This site’s rebuild was done that way.
What I wasn’t willing to give up was control. An AI that can run commands is exactly the kind of thing that shouldn’t have the run of my machine. So I built a sandbox for it, and it never runs on my desktop directly. A small fish function, cbox, starts it in a container for one project at a time:
cbox ~/agent-projects/<project> # Claude, in that project
cbox ~/agent-projects/<project> c # continue the last conversation
cbox ~/agent-projects/<project> web # the project's dev server

Each one is a rootless Podman container from a small Fedora image:
--userns=keep-id --cap-drop=all --security-opt=no-new-privileges --init
--network=pasta:-4
-v <project>:/work/<project>:z
- Only the project folder is mounted. Claude sees one project, not my home folder.
- No capabilities, and no way to gain privileges.
- Network out, nothing in. It can install packages and reach the web, but no host ports are forwarded into the container.
- Its own settings volume holds Claude’s config and any keys I give it, mounted only into Claude’s containers.
Instead of six long-running services, there’s now one container image, and containers that only exist while I’m working.
The rules that keep it safe
I approached this as a threat model. Assume anything inside the sandbox could turn hostile: a malicious package, a confused agent, instructions hidden in a web page it reads. Then decide what it must never be able to reach. Every rule below follows from that:
Host changes go through an installer I review. Some project files are meant for my desktop: cbox itself, the image’s Containerfile, my desktop config. Claude edits them inside the project and checks them (bash -n, shellcheck, fish -n, and a test run against a fake podman). Then, as its rules require, it hands the change to me through the project’s install.sh, which:
- shows me a diff and asks y/N for each file
- backs up whatever it replaces
- rebuilds the image only if the Containerfile actually changed
- is safe to run again: unchanged files are skipped
![install.sh showing a diff for site-up.fish (one new comment line) and asking “Install site-up.fish? [y/N]”](/_astro/screenshot-from-2026-10-27-20-29-26.C3l76gR-_1ouHKe.webp)
Never live-link a project into the host’s config. Symlinks or stow from a folder the sandbox can write to would let anything in it change my real shell, unreviewed.
Never run git on the host inside a project. Git runs hooks and reads config from .git/, so whatever can write to the project could run code on the host the next time I pull.
One key per repository, and I approve every push. Claude pushes with a deploy key that can write to one repository only. An ask rule in Claude Code’s settings makes the tool itself prompt me before every git push:
{
"permissions": {
"ask": ["Bash(git push *)", "Bash(git -C * push *)"]
}
}

A shared memory instead of a notes app
The lab had a notes service. Now there’s a small knowledge base in Claude’s settings volume, loaded in every project. It holds how I like to work, my machine and desktop, how the sandbox works, and what each project is. When a project learns something lasting about my setup, it updates the right note and tells me in one line. Each fact is marked as something I said or something it observed, and secrets never go in.
The result: a new project starts already knowing my setup, which used to be the job of my notes and my own memory.
Retiring the lab
The old lab went out the way everything now changes on the host: one removal script, reviewed and run by me, which took down the lab’s services and plugins. None of its configuration was kept: nothing in the new setup needs it.
Before and after
| AI lab | Now | |
|---|---|---|
| Services | 6 behind a reverse proxy, started and stopped from 2 desktop plugins | none: containers run only while I work |
| Models | many, through OpenRouter | Claude |
| Web interfaces to run | 6 | 0 |
| Things to update | every service, separately | one container image |
| Changing my machine | by hand | a diff and a y/N |
| Publishing | by hand | Claude asks, I approve |
What I gave up
Model choice. With OpenRouter I could pick a different model for each job, and some of them were free, which was handy for lighter tasks. Now it’s Claude for everything. For my work that hasn’t been a loss, but that flexibility is the one thing the lab had that this setup doesn’t.
Was it worth it?
Yes. The lab taught me a lot about self-hosting, and I don’t regret building it. But for what I actually do and what interests me, it was overkill: one well-fenced assistant beats a platform I have to run. It handles whole jobs within the limits I set, and the two moments that matter, changing my machine and publishing something, always come back to me as a single y/N.
Skills used
Podman (rootless, Quadlets, systemd), Caddy reverse proxy, self-hosted AI tools (Open WebUI, OpenHands, code-server), the OpenRouter model API, Noctalia v5 plugin development, Claude Code, container hardening, threat modelling, least-privilege credentials (one-repository deploy keys), and reviewed, testable host installers (Bash, fish, ShellCheck).