The Mac Mini Command Center
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-signalsinstead ofclaude-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.
| Topic | What | Voice |
|---|---|---|
hedge-signals | Trade alerts (BUY/SELL) | Terse floor trader: (⌐■_■) BUY NVDA 10sh @ $142.50 |
hedge-portfolio | Daily P&L, portfolio snapshots | Calm advisor: ( *^w^*) +$1,230 (+0.97%) |
hedge-pipelines | Pipeline failures | Tired data engineer: (;一_一) Yahoo stale, 8h since last pull |
docker-events | Container crashes/OOM | Drama queen: (ノಠ益ಠ)ノ彡┻━┻ trading-engine DIED exit 137 |
machines-health | Disk, memory, heartbeat miss | Concerned sysadmin: ( •᷄ὤ•᷅) disk 88%, Docker using 6.1GB |
commands-response | iPhone command results | Helpful assistant with context |
claude-notify-* | Claude Code done/needs attention | The 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:
| Schedule | Script | What |
|---|---|---|
| Every 15 min | resource-alerts.sh | Disk >80%, memory pressure, Docker size, Postgres size |
| Every 30 min | pipeline-alerts.sh | Pipeline failures, staleness >6h, error rates |
| Every 30 min | trade-alerts.sh | New trades → hedge-signals with ticker, side, price, strategy |
| 4:30 PM ET | daily-summary.sh | End-of-day P&L, regime, top movers, strategy performance |
| Every 5 min | heartbeat-sender.sh | Silent ping with load, disk, memory, container count |
| Every 6 min | heartbeat-checker.sh | (MacBook) Alert if Mac mini goes dark |
| Always-on | docker-alerts.sh | Container die/OOM/restart events |
| Always-on | command-listener.sh | iPhone 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:
| Layer | Device | Refresh | Purpose |
|---|---|---|---|
| Ambient | TRMNL e-ink | 1 hour | Glanceable dashboard, calm awareness |
| Active | iPhone/Watch via ntfy | Real-time | Alerts, trade signals, failures |
| Interactive | MacBook via SSH/Claude Code | On-demand | Development, 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
| Metric | Value |
|---|---|
| Duration | ~4 hours |
| Agents spawned | 11 (9 ntfy scripts + TRMNL audit + claude-notify research) |
| Postgres migrated | 26M rows, 6.1 GB → 475 MB compressed |
| Docker containers on Mac mini | 12 + ntfy |
| ntfy channels created | 7 |
| Cron jobs installed | 5 (Mac mini) + 1 (MacBook) |
| Launchd daemons | 2 (docker-alerts, command-listener) |
| Scripts deployed | 8 |
| Docs created | 3 (API audit, style guide, Shortcuts guide) |
| Skills synced to Mac mini | 12 |
| 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.