Command & Control: A Studio Stack Run With AI Tooling

How a small studio wires a home machine, a tiny cloud edge, and self-hosted git into one system, and lets AI do a lot of the operating.

We're a small open-source software studio. We don't have an ops team, so we built a stack we can operate ourselves: a machine at the studio running local AI and automation, a small cloud node that gives it a public face, self-hosted git as the record of everything, and AI tooling that can reach all of it over SSH.

This post is the map, plus the lessons that cost us something to learn.


The Map

                ┌───────────────────────────────────────────────┐
                │  Self-hosted Gitea  (source of truth)         │
                │  flows · server configs · ADRs · runbooks     │
                └──────▲────────────────▲───────────────▲───────┘
                       │ git over SSH   │               │
 ┌─────────────────────┴───────┐        │      ┌────────┴──────────────────┐
 │  STUDIO: Mac mini           │        │      │  PRODUCTION WEB SERVER    │
 │  Ollama (local models)      │        │      │  Drupal sites · Gitea     │
 │  Open WebUI (chat)          │        │      │  ntfy (push alerts)       │
 │  Node-RED (orchestration)   │        │      │  CrowdSec agent + bouncer │
 │  LAN reverse proxy          │        │      └────────────▲──────────────┘
 └──────────┬──────────────────┘        │                   │ decisions
            │ reverse SSH tunnel        │                   │
            ▼ (outbound only)           │      ┌────────────┴──────────────┐
 ┌─────────────────────────────┐        │      │  EDGE NODE (tiny VPS)     │
 │  Public subdomain           │◄───────┴──────┤  reverse proxy + TLS      │
 │  (chat UI, behind a CDN)    │               │  shared CrowdSec engine   │
 └─────────────────────────────┘               └───────────────────────────┘

        AI tooling (Claude Code) reaches every box over SSH,
        reads the briefing + memory, writes the docs as it works.

Four layers: compute at the studio, a public edge, git as memory, and AI as the operator. Then there's a fifth, smaller one: alerts that reach a phone.


Layer 1: The Studio Machine

A Mac mini at the studio does the work that benefits from being local, cheap and private.

  • Ollama runs local models. They're good enough for summarizing, classifying and drafting, cost nothing per token, and the data never leaves the building.
  • Open WebUI gives those models a chat interface that anyone on the team can use.
  • Node-RED is the studio's orchestration layer (more below). It's installed natively, not in a container, so flows can reach USB serial and Bluetooth hardware: an OBD dongle, a camera gimbal, an SDR receiver.
  • A small reverse proxy gives every service a friendly LAN-only name, so nobody memorizes port numbers. The services themselves only listen on localhost; the proxy is the only thing on the network.

Everything starts from launchd, macOS's service manager, with restart-on-crash. That brings us to the first lesson.

Lesson: a reboot is the only real test. After an unplanned restart, our tunnel came back exactly as designed, but the container runtime behind the chat UI didn't, because it wasn't set to start at login. The tunnel was healthy and forwarding to nothing. Every service now has an explicit "who starts this at boot?" answer, and the machine's boot requirements (auto-login, disk encryption trade-offs, sleep settings) are written down, because changing any one of them quietly breaks unattended recovery.


Layer 2: The Edge, a Reverse Tunnel

The studio machine sits on a home-grade connection: no static IP, and no appetite for opening inbound ports. So it never accepts connections. It reaches out.

A long-lived SSH connection from the studio to a tiny cloud VPS carries reverse port forwards: the VPS listens on its own localhost, and anything it receives is carried back down the tunnel to a service at the studio. The VPS's reverse proxy terminates TLS and publishes that as a normal public subdomain, behind a CDN.

The tunnel is a plain ssh -N -R ... run by the service manager, with a few options that matter:

  • ExitOnForwardFailure=yes: if the remote port can't be bound, exit and let the service manager retry, instead of sitting there "connected" but useless.
  • ServerAliveInterval / ServerAliveCountMax: detect a dead connection in about 90 seconds rather than whenever TCP gets around to it.
  • BatchMode=yes and a dedicated key: it can never stop and wait for a prompt.

The same small VPS does a second job as the shared CrowdSec engine for our servers. One node makes the security decisions, and every server's firewall bouncer enforces them.

Lesson: services started by the service manager inherit things you don't expect. On macOS, processes launched from the user session inherit a pointer to the GUI's SSH agent. Over a headless SSH session, that agent can block forever waiting on a prompt nobody will see, so our git pushes from Node-RED simply hung. The fix was one line per host in the SSH config, IdentityAgent none, since those keys are dedicated and don't need the agent.


Layer 3: Gitea as the Source of Truth

Everything the system is lives in self-hosted Gitea:

  • Automation flows. Node-RED's Projects feature makes the flows a real git repo: deploy, then commit and push from the editor. Credentials inside flows are encrypted with a per-project secret that never enters the repo.
  • Server configuration. For software whose config lives in /etc (CrowdSec, for example), a snapshot script pulls it into a private repo, never copies credential files, and redacts anything shaped like a key. That gives us diffs, history, and a rebuild that isn't archaeology.
  • Decisions and incidents. Architecture Decision Records for why, and Technical Information Documents for what happened and how to recover. Every infrastructure change today ended up in one or the other.
  • The briefing. A plain-text briefing file plus notes that every AI session loads at the start (more on that below).

Two safety nets keep it honest:

  • A nightly auto-commit on the studio machine commits any automation changes that were deployed but never committed, and pushes them. It never force-pushes, pulls or merges: if the remote has moved, it logs the failure and waits for a human.
  • SSH-only git. Our CDN challenges non-browser HTTPS clients, which breaks git over HTTPS in confusing ways (it shows up as 403s that look like auth failures). All git traffic goes over SSH straight to the origin.

Layer 4: Orchestration, Split by Blast Radius

We needed somewhere for automations, and soon a crew of AI agents, to live. We evaluated n8n first: it's polished, but its Sustainable Use License isn't open source, and it restricts exactly the thing we might want later, shipping automations to paying clients. So we split the job:

ECA on Drupal Node-RED on the Mac mini
License GPL Apache 2.0
For Business workflows and the AI agent crew Studio, hardware, media, local models
Runs on A server we'd trust with client work The studio machine
Superpower Workflows are config, so they ship as Drupal recipes Reaches serial, BLE, OBS, MQTT, shell

The rule that falls out of this: the studio machine is never on a client's critical path. It can lose power, internet or its login session, and only studio work waits. Anything a client depends on runs in the cloud. When cloud workflows use the studio (cheap local inference on Ollama, or triggering hardware), they're written to tolerate it being offline.


Layer 5: Eyes and Ears

The reboot lesson taught us that "it's probably fine" isn't monitoring. Our first real Node-RED flow is a health monitor:

  1. A timer fires every five minutes.
  2. A function fans out the checks: HTTP probes (local models, chat UI, the LAN proxy) and shell probes (is the tunnel process alive, does the container runtime answer, did last night's auto-commit log an error).
  3. Each result is normalized to { ok, detail }, and a join waits for all of them, with a timeout so a hung check counts as a failure.
  4. A summary function stores the result and only emits when something changes state: failed or recovered. No alert storms.

The output goes three places: a small status page on the LAN, a JSON endpoint that returns 503 when anything is down (so other monitors can chain off it), and a push notification to a phone through self-hosted ntfy. Anonymous access is disabled, and the publisher holds a token that can write to exactly one topic and nothing else.

Lesson: the last 5% of "self-hosted" is DNS and phones. Our phone alerts failed twice before working, and neither failure was the server. First, the home router cached the "this name doesn't exist" answer from before we created the record, for up to the zone's negative-caching TTL (30 minutes). Then the phone app quietly subscribed to the public ntfy server, because a "use another server" toggle was left off. Check where the client is actually connected before debugging the server.


Layer 6: AI as the Operator

This is the part that makes the rest feasible for a studio our size.

We run Claude Code from a laptop, and it reaches every machine above over SSH. It doesn't arrive cold to each session:

  • A briefing file says who we are, what's in flight, where each repo lives, and the conventions (tag formats, which remotes, what never to push publicly).
  • Persistent memory notes capture non-obvious facts as they're discovered ("a ban looks like an outage; check this first"), so the next session starts where this one left off.
  • It writes the documentation as it works. Every change today produced its own record: an ADR for the orchestration decision, a TID for the incident, a runbook page per machine, and a change log in the config repo.

What it actually did in one long session: diagnosed an outage, installed and wired up Node-RED, a LAN proxy and the nightly safety net, built the health flow through Node-RED's admin API, stood up ntfy with scoped tokens, traced a false-positive security ban to its root cause, wrote and tested a narrowly scoped whitelist, and found and fixed a double-counting bug in the security config along the way.

The guardrails matter as much as the capability:

  • Secrets never pass through the conversation. Passwords are generated on the server and written to mode-600 files. Tokens are piped machine to machine over SSH and land straight in encrypted credential stores. The human reads them with one command when needed.
  • Persistence and production need a human yes. The tooling's permission layer stopped it from installing a login-time service until we approved it, and it asked before touching the production web server, even when it had the access.
  • Evidence over assumptions. "It should work" isn't done. Done is a 200, a log line, a simulated replay through the security engine, and then a real one.
  • It corrects itself in writing. When an early guess turned out wrong (a clock that wasn't actually skewed, a log that was being read), the correction went into the document, not just the chat.

Local models fit into this too. Ollama on the studio machine is where cheap, private, high-volume inference will run for the agent crew, with the frontier model reserved for reasoning and operating.


What's Next

  • The agent crew on ECA: business workflows as Drupal config, built so they can become recipes.
  • Local inference as a crew resource: studio Ollama behind the tunnel, with cloud fallback when the studio is offline.
  • Hardware flows: the vehicle telemetry and camera rigs that Node-RED was chosen for in the first place.
  • Away-from-home access to the studio tools, behind a zero-trust gate rather than an open port.

The Short Version

  1. Outbound-only tunnels give a home machine a public face without opening a single port.
  2. Self-hosted git is the system's memory: flows, configs, decisions and incidents, all diffable.
  3. Split orchestration by blast radius: what can fail at the studio stays at the studio.
  4. Alert on change, not on state, and make the alert reach a phone.
  5. AI tooling is an operator, not an oracle. Give it a briefing, memory and SSH, make it document everything, and keep secrets and production decisions on the human side of the line.