The Mac Mini Command Center

infrastructureDockerTailscalentfyautomationClaude Code

The Mac Mini Command Center

I turned a Mac mini into a private infrastructure server in one Claude Code session. Self-hosted notifications to my Apple Watch. iPhone remote control via Shortcuts. E-ink dashboard. 26 million rows of market data. All private, all over Tailscale, all controlled from my couch.


The Setup

MacBook Air = mobile coding device. Mac mini (M2 Pro) = always-on command center. Connected over Tailscale (WireGuard-encrypted mesh VPN). SSH key auth so Claude Code on the MacBook can run commands on the Mac mini remotely.

MacBook Air (100.99.9.76)
  └── Claude Code + dev work

        ├── SSH ────────────────────► Mac mini (100.71.141.45)
        │                              ├── Docker: 12 containers
        │                              ├── Postgres: 26M rows market data
        │                              ├── ntfy: private notification server
        │                              ├── Pipelines: Finviz/Yahoo/Alpaca/GDELT
        │                              ├── Trading engine: daily paper trades
        │                              └── Command listener: iPhone remote control

        └── ntfy ◄──────────────────── notifications to iPhone/Watch

The entire migration — database dump, Docker rebuild, script deployment — happened over SSH from the MacBook. I never walked upstairs.


Self-Hosted ntfy

The first thing that changed everything: running my own ntfy notification server instead of the public ntfy.sh.

docker run -d --name ntfy --restart unless-stopped \
  -p 2586:80 binwiederhier/ntfy serve \
  --cache-file /var/cache/ntfy/cache.db

Why private matters:

  • No rate limits. Public ntfy.sh throttles you. Mine doesn’t.
  • Human-readable topic names. hedge-signals instead of claude-notify-1a047c47. No topic squatting.
  • Sensitive data in notifications. Portfolio values, trade signals, API costs — none of this should go through a public server.
  • Tailscale encrypts everything. HTTP over Tailscale is effectively HTTPS (WireGuard at the network layer). Zero internet exposure.

The ntfy iOS app connects to 100.71.141.45:2586 and receives push notifications like any other ntfy server. Works on Apple Watch too.


Seven Notification Channels

Each channel has its own personality. Not emojis — kaomoji.

TopicWhatVoice
hedge-signalsTrade alerts (BUY/SELL)Terse floor trader: (⌐■_■) BUY NVDA 10sh @ $142.50
hedge-portfolioDaily P&L, portfolio snapshotsCalm advisor: ( *^w^*) +$1,230 (+0.97%)
hedge-pipelinesPipeline failuresTired data engineer: (;一_一) Yahoo stale, 8h since last pull
docker-eventsContainer crashes/OOMDrama queen: (ノಠ益ಠ)ノ彡┻━┻ trading-engine DIED exit 137
machines-healthDisk, memory, heartbeat missConcerned sysadmin: ( •᷄ὤ•᷅) disk 88%, Docker using 6.1GB
commands-responseiPhone command resultsHelpful assistant with context
claude-notify-*Claude Code done/needs attentionThe original playful chaos (haiku, horoscopes, achievements)

The claude-notify channel got upgraded too — it now parses last_assistant_message from the hook payload. Instead of “done, come back” you get “done: Fixed iPad layout crash in reader view.” A background Haiku call (free via claude -p) sends a polished 10-word summary a few seconds later.


iPhone Remote Control

This is the part that made me lose it.

ntfy is just HTTP. Apple Shortcuts can make HTTP requests. The Mac mini has a command listener daemon subscribed to commands-mini via SSE. So:

iPhone Shortcut → POST "market-status" to ntfy

Mac mini listener picks it up

Queries Postgres (26M rows)

POSTs result back to ntfy "commands-response"

iPhone Shortcut reads the response

Shows on screen / Apple Watch

Available commands: market-status, portfolio, docker-status, disk-check, run-pipeline, regime. Each returns Apple Watch-readable output.

“Hey Siri, market check” → pipeline timestamps, row counts, portfolio value. From your wrist. Querying a Postgres database on a Mac mini upstairs. Over Tailscale.


The Database Migration

The ai-hedge-fund project had 26M+ rows of market data in a Docker Postgres on the MacBook. Moving it to the Mac mini:

# Dump on MacBook (6.1 GB → 475 MB compressed)
docker exec postgres pg_dump -Fc -Z6 > market_data_backup.dump

# Copy to Mac mini over SMB
cp market_data_backup.dump /Volumes/weixiangzhang/...

# Restore on Mac mini (2 minutes)
cat market_data_backup.dump | docker exec -i market-data-db pg_restore ...

The Mac mini now runs the full Docker platform: Postgres (26M rows), Grafana (11-panel dashboard), Streamlit (5-page analytics), nginx static dashboard, data pipeline scheduler (Finviz 4h, Yahoo 24h, Alpaca 4h, GDELT 6h), and a paper trading engine.

All accessible from the MacBook at http://100.71.141.45:<port>.


Cron Jobs: The Autonomous Layer

Eight scripts running on schedules:

ScheduleScriptWhat
Every 15 minresource-alerts.shDisk >80%, memory pressure, Docker size, Postgres size
Every 30 minpipeline-alerts.shPipeline failures, staleness >6h, error rates
Every 30 mintrade-alerts.shNew trades → hedge-signals with ticker, side, price, strategy
4:30 PM ETdaily-summary.shEnd-of-day P&L, regime, top movers, strategy performance
Every 5 minheartbeat-sender.shSilent ping with load, disk, memory, container count
Every 6 minheartbeat-checker.sh(MacBook) Alert if Mac mini goes dark
Always-ondocker-alerts.shContainer die/OOM/restart events
Always-oncommand-listener.shiPhone Shortcuts → command execution

If a pipeline fails at 3 AM, my Apple Watch taps my wrist. If the Mac mini’s disk hits 80%, I get a notification with specific numbers. If a container crashes, the Drama Queen channel tells me about it with maximum theatrical energy.


TRMNL: The Ambient Layer

The final piece: a TRMNL e-ink display on my desk. 800x480 pixels, updates hourly. Shows portfolio value, regime status, pipeline health — not as alerts, but as ambient information.

ntfy is for “something happened, react.” TRMNL is for “glance at the desk, everything’s fine.”

Different tools, different rhythms.


The Architecture Philosophy

Three layers, three rhythms:

LayerDeviceRefreshPurpose
AmbientTRMNL e-ink1 hourGlanceable dashboard, calm awareness
ActiveiPhone/Watch via ntfyReal-timeAlerts, trade signals, failures
InteractiveMacBook via SSH/Claude CodeOn-demandDevelopment, analysis, deep work

The Mac mini is the server. The MacBook is the client. The iPhone is the remote control. The TRMNL is the status board. Each device does what it’s best at.


Session Stats

MetricValue
Duration~4 hours
Agents spawned11 (9 ntfy scripts + TRMNL audit + claude-notify research)
Postgres migrated26M rows, 6.1 GB → 475 MB compressed
Docker containers on Mac mini12 + ntfy
ntfy channels created7
Cron jobs installed5 (Mac mini) + 1 (MacBook)
Launchd daemons2 (docker-alerts, command-listener)
Scripts deployed8
Docs created3 (API audit, style guide, Shortcuts guide)
Skills synced to Mac mini12
Cost$0 (Tailscale free, ntfy self-hosted, Docker local)

The Moment

The moment it clicked: I’m sitting on the couch gaming. My Apple Watch buzzes — (;一_一) Alpaca pipeline stale, 6h since last pull. I pick up my phone, open Shortcuts, tap “Refresh Data.” The Mac mini upstairs receives the command, runs the pipeline, and sends back (*^-^*) Finviz: 908 tickers pulled, Alpaca: 2,499 rows upserted. I go back to gaming.

Infrastructure should work for you while you’re doing something else. That’s the whole point.