DEV Community

Cover image for I built a headless Mac mini AI server and by week two I was debugging sleep, plist files, and Redis
Lars Winstand
Lars Winstand

Posted on Originally published at standardcompute.com

I built a headless Mac mini AI server and by week two I was debugging sleep, plist files, and Redis

The moment I knew my cute little local AI box had turned into actual infrastructure was when a folder stopped moving files at 3:14 a.m.

Nothing crashed.

Disk was fine.

Ollama was still up.

The machine had just quietly stopped being useful.

That was my headless Mac mini setup: a Mac mini on a shelf, no monitor, Ollama serving local models on http://localhost:11434/v1, a few Python helpers, and n8n running background automations.

For six days, it felt elegant.

Then week two started.

The first real failure wasn't inference

I expected model serving to be the hard part.

Maybe Llama would be too slow.
Maybe Qwen would eat RAM.
Maybe Ollama would fall over under concurrent requests.

Nope.

The first reliability problem was sleep.

A headless Mac mini loves to look alive while doing nothing useful.

Your file watcher still exists.
Your local API still responds sometimes.
Your helper process is technically running.

But if the machine slept, or the session changed in a way your setup didn't handle, your "automation server" became desktop theater.

That was the first lesson:

If you don't explicitly manage power behavior, you're not running a server. You're running a desktop that occasionally pretends to be one.

The two commands that fixed the obvious nonsense

If you're trying to keep a headless Mac mini alive for background jobs, you meet caffeinate and pmset fast.

caffeinate -i python3 watch.py
Enter fullscreen mode Exit fullscreen mode

That prevents idle sleep while the command runs.

For broader power settings:

sudo pmset -a sleep 0
pmset -g assertions
Enter fullscreen mode Exit fullscreen mode

That does two useful things:

  • disables system sleep across power profiles
  • shows which processes are currently asserting power management

A practical pattern is:

caffeinate -i ollama serve
Enter fullscreen mode Exit fullscreen mode

or wrapping a long-running worker:

caffeinate -i /usr/local/bin/python3 /Users/you/ai-helper/watch.py
Enter fullscreen mode Exit fullscreen mode

One footgun worth calling out: caffeinate -u defaults to a 5 second timeout if you don't pass -t.

That is exactly the kind of detail that makes a test look fine and an overnight job fail.

Why launchd matters more than another shell script

A lot of people approach this with Linux habits.

Start a process in the background.
Add &.
Maybe use nohup.
Call it a service.

On macOS, that gets messy fast.

If the Mac mini is going to be headless and useful, launchd is the real primitive.

And Apple is pretty clear about one thing: jobs launched by launchd should not daemonize themselves with the old fork-and-exit pattern.

That means a lot of generic "run this as a service" tutorials are subtly wrong for macOS.

Once I stopped fighting that, things got cleaner.

A minimal launchd watcher that actually behaves like a service

For file-triggered helpers, a plist beats a Terminal tab every time.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.example.aihelper</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/python3</string>
    <string>/Users/you/ai-helper/watch.py</string>
  </array>
  <key>KeepAlive</key><true/>
  <key>WatchPaths</key>
  <array>
    <string>/Users/you/Inbox</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>StandardOutPath</key><string>/tmp/aihelper.out.log</string>
  <key>StandardErrorPath</key><string>/tmp/aihelper.err.log</string>
</dict>
</plist>
Enter fullscreen mode Exit fullscreen mode

The three keys doing most of the work are:

  • KeepAlive: restart the helper if it dies
  • WatchPaths: react to directory changes
  • RunAtLoad: start immediately when loaded

Load it with:

launchctl load ~/Library/LaunchAgents/com.example.aihelper.plist
Enter fullscreen mode Exit fullscreen mode

Or on newer macOS versions, bootstrap it explicitly:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.aihelper.plist
launchctl kickstart -k gui/$(id -u)/com.example.aihelper
Enter fullscreen mode Exit fullscreen mode

Check status:

launchctl print gui/$(id -u)/com.example.aihelper
Enter fullscreen mode Exit fullscreen mode

Tail logs:

tail -f /tmp/aihelper.out.log /tmp/aihelper.err.log
Enter fullscreen mode Exit fullscreen mode

This is the point where your Mac mini starts acting like a background worker instead of laptop cosplay.

The downside is also immediate.

You stop debugging only your Python code.
Now you're debugging:

  • plist syntax
  • launchctl
  • process environment differences
  • restart behavior
  • macOS logging

That is a very different hobby.

Ollama is easy right up until it becomes infrastructure

I still think Ollama is the easiest on-ramp for local AI.

Point your app at:

http://localhost:11434/v1
Enter fullscreen mode Exit fullscreen mode

and use the OpenAI-compatible API.

That means existing SDK code changes very little.

Example with Python:

from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

resp = client.chat.completions.create(
    model="llama3.1",
    messages=[
        {"role": "user", "content": "Summarize this file in 3 bullets."}
    ]
)

print(resp.choices[0].message.content)
Enter fullscreen mode Exit fullscreen mode

For experiments, this rules.

You can swap models, test a summarizer, wire up an agent, and get the "wait, this actually works" feeling in under an hour.

Then you realize Ollama is not a vibe.

It's a server.

And a server needs:

  • startup behavior
  • logs
  • restart behavior
  • predictable uptime
  • some answer to "what happens after a crash?"

If your AI helper is doing useful work while you're asleep, a Terminal tab is not a deployment strategy.

The real pain starts when one automation becomes five

This is where local AI setups get oversold.

A single user with a few lightweight jobs? Great.

A Mac mini with launchd, Ollama, and a couple scripts can be genuinely nice:

  • private
  • fast on LAN
  • fixed hardware cost
  • no cloud bill anxiety

But add:

  • webhooks
  • file triggers
  • scheduled jobs
  • overlapping LLM calls
  • retries
  • multiple users

and your tiny local setup starts acting like a small production system.

That is where orchestration becomes the problem, not inference.

n8n gets real about concurrency faster than most people do

n8n is a good example because the docs are pretty honest.

In regular mode, production executions can pile up unless you cap them.

The practical env var is:

export N8N_CONCURRENCY_PRODUCTION_LIMIT=20
Enter fullscreen mode Exit fullscreen mode

In Docker:

environment:
  - N8N_CONCURRENCY_PRODUCTION_LIMIT=20
Enter fullscreen mode Exit fullscreen mode

That sounds like a tuning detail.

It isn't.

It's the point where you admit overlapping work can absolutely make one box unresponsive.

And once you need more than one busy process path, the architecture changes.

Queue mode is where your "local box" becomes a system

n8n queue mode exists for a reason.

It separates trigger handling from execution workers and uses Redis in the middle.

That's the right move when jobs overlap heavily.

It's also the point where your Mac mini is no longer just a local automation box.

Now you have:

  • an n8n main instance
  • one or more workers
  • Redis
  • shared encryption key management
  • a database setup that should not be SQLite

That is a real operational jump.

Here's the cleanest summary I can give:

Option What week two feels like
launchd Native macOS fit for headless jobs, solid for watchers and helpers, but debugging moves into plist files and system behavior
n8n regular mode with concurrency cap Fine for a single instance and moderate load, but easy to outgrow once executions overlap
n8n queue mode Much better scaling story, but now you're operating Redis, workers, and actual workflow infrastructure

That is the local-first tax.

Not API compatibility.
Not whether Ollama can answer a prompt.

Orchestration is where the maintenance cost shows up.

Remote admin is where the cute Mac mini story gets thin

The internet makes headless Mac mini setups sound adorable.

Tiny box.
Silent.
Efficient.
Put it on a shelf and call it your AI server.

Sure.

But the minute you need reliable remote administration, you start touching way more of macOS than expected.

SSH, power settings, permissions, launch agents vs launch daemons, login state, Full Disk Access edge cases.

This is the part YouTube tutorials usually skip because it's less fun than benchmark screenshots.

None of it is impossible.

It's just not the clean little "local is simpler" story people like to tell.

So was the Mac mini a bad idea?

No.

It was a good idea for exactly the amount of complexity I had on day one.

That's the part I wish more people said out loud.

Local hosting is not bad.

A Mac mini can absolutely win on:

  • privacy
  • LAN latency
  • fixed hardware cost
  • fast iteration for one person

Ollama plus launchd plus a couple helpers is a sane setup.

The mistake is assuming a useful helper stays small.

Useful helpers attract:

  • more jobs
  • more triggers
  • retries
  • logs
  • webhooks
  • other users
  • uptime expectations

And that's when local orchestration stops being fun.

My rule now

If I'm:

  • testing prompts
  • running a private summarizer
  • building one-user automations
  • experimenting with local models

I still like the Mac mini.

If I'm depending on:

  • overlapping background executions
  • reliable webhook handling
  • worker behavior
  • 24/7 automations
  • something I don't want to babysit at midnight

I stop pretending I'm "just running it locally."

At that point I'm operating infrastructure, and I make decisions like it.

The part most local AI posts skip

The breaking point wasn't model performance.

It wasn't even Apple being weird.

It was realizing that local-first breaks down around orchestration long before it breaks down around inference.

That's why I think local model serving and production automation should be treated as separate decisions.

You might absolutely want Ollama locally for privacy or speed.

But if your agents, n8n workflows, or background automations are running constantly, the thing that hurts is usually not token generation.

It's concurrency, retries, uptime, and cost predictability once usage stops being "a few tests" and turns into always-on work.

That's also why flat-rate API infrastructure is more interesting than it used to be.

If you're building agents or automations that hit LLMs all day, per-token pricing changes how you design everything. You start rationing calls, skipping useful steps, or watching cost dashboards instead of shipping.

Standard Compute is interesting because it flips that tradeoff: OpenAI-compatible API, flat monthly pricing, and routing across models like GPT-5.4, Claude Opus 4.6, and Grok 4.20 without the usual per-token anxiety.

That's a very different fit from a shelf Mac mini, but it's solving the exact problem week two exposed for me: once the workflow matters, predictability matters more than the demo.

Practical advice if you're building this right now

If you're setting up a headless Mac mini for AI workflows, here's the short version:

  1. Use Ollama for local experiments and private one-user tools.
  2. Use caffeinate and inspect pmset before blaming your app.
  3. Move long-running jobs into launchd early.
  4. Add logs from day one.
  5. Cap concurrency in n8n before the box teaches you why.
  6. Be honest about when "local" has become infrastructure.
  7. If your workflows run constantly, evaluate whether predictable API compute is actually simpler than self-hosting orchestration.

That's the whole lesson.

Week one was AI.

Week two was operations.

Top comments (0)