📑 Contents
Mac mini as a server Recommended stack Phase 1 — Foundation Phase 2 — Containers Phase 3 — Core Services Phase 4 — Networking Phase 5 — Monitoring Phase 6 — Storage & Backup Core services + FileBrowser Advanced services Media + Shoko + Bazarr Reverse proxy & TLS Remote access Security hardening ReferencesHomelab on Mac mini
A practical, phased guide to turning a Mac mini into a self-hosted server — from bare macOS to a full services stack with monitoring, remote access, and backups.
Why homelab?
A homelab is a personal server environment you control entirely. The Mac mini is an ideal choice: it's quiet, power-efficient (~20W idle), runs macOS natively, has excellent Apple Silicon performance, and doesn't require a loud rack or a separate NAS.
| Goal | What you get |
|---|---|
| Privacy | Your data never leaves your network — no cloud subscriptions, no data mining |
| Learning | Real hands-on experience with Docker, networking, Linux services, TLS, monitoring |
| Cost savings | Replace Dropbox, Bitwarden, GitHub, Notion, Netflix infra with self-hosted equivalents |
| Speed | Local services are faster than cloud for LAN clients (streaming, file sync, DNS) |
| Reliability | You control uptime, not a third-party SLA |
Mac mini as a server
Which Mac mini?
| Model | Verdict | Notes |
|---|---|---|
| Mac mini M4 (2024) | Best choice | 16 GB base RAM, excellent per-watt perf, 3 USB-A + 3 USB-C/TB. M4 Pro variant for heavy workloads. |
| Mac mini M2 / M2 Pro | Great | Still fast, good value used. M2 Pro has 10-core CPU + more RAM options. |
| Mac mini M1 (2020) | Good enough | 8 GB RAM is limiting. Fine for lightweight services; avoid if running 10+ containers. |
| Mac mini Intel (2018) | Avoid for new setup | Higher power draw, no Rosetta benefit, no future macOS support roadmap. |
RAM guidance
- 8 GB — basic homelab: DNS, VPN, dashboard, password manager, reverse proxy
- 16 GB — comfortable: adds monitoring stack, Nextcloud, Git server, media server
- 24–32 GB (M2/M4 Pro) — heavy: multiple databases, LLM inference, K3s cluster, full *arr stack
Storage
- Internal SSD for OS + Docker images + small databases
- External USB-C SSD or NAS for large media, backups, Time Machine
- WD My Passport / Samsung T7 (portable SSD) — cheap and fast for 2–4 TB
- Synology or QNAP NAS if you need RAID redundancy and > 8 TB
macOS-specific advantages
- No noisy fan — runs silently at idle, fine for a living room or office
- Wake on LAN / Wake on network access — built into System Settings → Energy
- Time Machine — excellent backup solution already built in
- Rosetta 2 — runs x86 Docker images on Apple Silicon transparently
- SMB sharing — built-in Samba server via System Settings → Sharing
Recommended Stack Overview
Phase 1 — Foundation (macOS Setup)
On your router, find the Mac mini's MAC address and assign it a reserved/static DHCP IP (e.g., 192.168.1.10). Every service will be reachable at this address consistently.
# From your laptop — test SSH works
ssh youruser@192.168.1.10
# Set a hostname so it's easy to identify
sudo scutil --set HostName macmini
sudo scutil --set LocalHostName macmini
sudo scutil --set ComputerName macmini
# Copy your SSH key (no password prompts)
ssh-copy-id youruser@192.168.1.10
Go to System Settings → Battery → Options and set:
- Prevent automatic sleeping when the display is off → ON
- Wake for network access → ON
- Start up automatically after a power failure → ON
# Also via CLI
sudo pmset -a sleep 0
sudo pmset -a disksleep 0
sudo pmset -a womp 1 # Wake on network access
sudo pmset -a autorestart 1
# Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Essential CLI tools
brew install git curl wget htop ncdu jq yq tmux
brew install --cask visual-studio-code # optional GUI
# Suggested layout — all your service configs live here
mkdir -p ~/homelab/{caddy,adguard,vaultwarden,gitea,nextcloud,monitoring,media,homepage}
mkdir -p ~/homelab/monitoring/{prometheus,grafana,loki}
mkdir -p ~/homelab/media/{jellyfin,sonarr,radarr,prowlarr,qbittorrent}
# Data directories — put on external drive if you have one
mkdir -p /Volumes/data/{nextcloud,jellyfin,gitea,vaultwarden}
Phase 2 — Container Runtime (Docker)
OrbStack (Recommended for Mac)
OrbStack is the fastest Docker runtime on macOS. It uses Apple's Virtualization framework, starts in ~2 seconds, has the lowest RAM/CPU overhead, and includes a native Mac menu bar app.
# Install OrbStack
brew install --cask orbstack
# Verify Docker works
docker run --rm hello-world
docker compose version
Docker Compose project layout
Structure each service as its own docker-compose.yml so you can start/stop them independently.
~/homelab/
├── caddy/
│ ├── docker-compose.yml
│ ├── Caddyfile
│ └── data/ # auto TLS certs, Caddy state
├── adguard/
│ ├── docker-compose.yml
│ └── data/
├── vaultwarden/
│ ├── docker-compose.yml
│ └── data/
└── monitoring/
├── docker-compose.yml
├── prometheus/
│ └── prometheus.yml
└── grafana/
└── provisioning/
Shared Docker network
Create a shared bridge network so containers in different Compose projects can reach each other (e.g., Caddy talking to Vaultwarden).
# Create once
docker network create homelab
# In each docker-compose.yml, add:
networks:
homelab:
external: true
services:
vaultwarden:
image: vaultwarden/server:latest
networks:
- homelab
# ... rest of config
Phase 3 — Core Services
AdGuard Home — DNS + Ad Blocking
Set this up first. Point your router's DNS to the Mac mini IP, and every device on your network gets ad blocking with zero client config. AdGuard Home is more polished than Pi-hole.
# ~/homelab/adguard/docker-compose.yml
services:
adguard:
image: adguard/adguardhome:latest
container_name: adguard
volumes:
- ./data/work:/opt/adguardhome/work
- ./data/conf:/opt/adguardhome/conf
ports:
- "53:53/tcp"
- "53:53/udp"
- "3000:3000" # setup wizard (first run only)
- "80:80" # admin UI after setup
networks:
- homelab
restart: unless-stopped
networks:
homelab:
external: true
sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist — or map AdGuard to a different port and point your router there.
Caddy — Reverse Proxy with Auto HTTPS
Caddy automatically obtains and renews TLS certificates. Point your local DNS entries (or /etc/hosts) to the Mac mini and get HTTPS for every service.
# ~/homelab/caddy/Caddyfile
{
email you@example.com
}
# Local-only services (uses self-signed or internal CA)
vault.home {
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
}
git.home {
reverse_proxy gitea:3000
}
grafana.home {
reverse_proxy grafana:3000
}
adguard.home {
reverse_proxy adguard:80
}
# ~/homelab/caddy/docker-compose.yml
services:
caddy:
image: caddy:2-alpine
container_name: caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./data:/data
- ./config:/config
networks:
- homelab
restart: unless-stopped
networks:
homelab:
external: true
vault.home → 192.168.1.10. Now every device on your LAN resolves https://vault.home to your Mac mini. Caddy handles TLS with its internal CA — import Caddy's root cert into your devices once.
Vaultwarden — Self-hosted Password Manager
Vaultwarden is a Bitwarden-compatible server written in Rust. Use the official Bitwarden browser extension and mobile app pointed at your self-hosted instance.
# ~/homelab/vaultwarden/docker-compose.yml
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
environment:
WEBSOCKET_ENABLED: "true"
SIGNUPS_ALLOWED: "false" # disable after creating your account
ADMIN_TOKEN: "your-long-random-token-here"
volumes:
- /Volumes/data/vaultwarden:/data
networks:
- homelab
restart: unless-stopped
Gitea / Forgejo — Self-hosted Git
Run your own Git server. Mirror your GitHub repos here, host private projects, or use it as a CI/CD backend with Gitea Actions.
# ~/homelab/gitea/docker-compose.yml
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__database__DB_TYPE: sqlite3
volumes:
- /Volumes/data/gitea:/data
ports:
- "2222:22" # SSH for git push
networks:
- homelab
restart: unless-stopped
Homepage — Dashboard
A clean, fast dashboard to see all your services at a glance. Supports service status checks, weather, bookmarks, and Docker integration.
# ~/homelab/homepage/docker-compose.yml
services:
homepage:
image: ghcr.io/gethomepage/homepage:latest
container_name: homepage
volumes:
- ./config:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro # for Docker widget
networks:
- homelab
restart: unless-stopped
Phase 4 — Networking & Remote Access
Tailscale creates a secure mesh VPN between your devices. Install it on the Mac mini and your phone/laptop — you can access every homelab service from anywhere as if you were on your LAN. Free for personal use (up to 100 devices).
# Install on Mac mini
brew install --cask tailscale
# Start + authenticate
tailscale up
# Enable subnet routing (expose your whole LAN to Tailscale clients)
tailscale up --advertise-routes=192.168.1.0/24 --accept-routes
# On your router — approve route in Tailscale admin console
# Then on your laptop:
tailscale up --accept-routes
If you want some services reachable from the internet (e.g., Vaultwarden for mobile app sync), Cloudflare Tunnel creates an outbound connection — no port forwarding, no public IP needed.
# Install cloudflared
brew install cloudflare/cloudflare/cloudflared
# Login and create a tunnel
cloudflared tunnel login
cloudflared tunnel create homelab
# config.yml — map public hostnames to local services
tunnel: <tunnel-id>
credentials-file: ~/.cloudflared/<tunnel-id>.json
ingress:
- hostname: vault.yourdomain.com
service: http://localhost:80 # Caddy proxy
- service: http_status:404
# Run as a launchd service (starts on boot)
sudo cloudflared service install
If you're not using Cloudflare Tunnel and prefer direct port forwarding, you need DDNS to handle your ISP's dynamic IP.
# Option A: Cloudflare DDNS (if your domain is on Cloudflare)
docker run -d \
-e CF_API_TOKEN=your_token \
-e DOMAINS=home.yourdomain.com \
--restart unless-stopped \
favonia/cloudflare-ddns:latest
# Option B: DuckDNS (free dynamic DNS service)
docker run -d \
-e SUBDOMAINS=yourlabel \
-e TOKEN=your-duckdns-token \
--restart unless-stopped \
lscr.io/linuxserver/duckdns:latest
Phase 5 — Monitoring Stack
The standard homelab monitoring stack: Prometheus collects metrics, Grafana visualizes them, Loki aggregates logs. node_exporter exposes host-level metrics from the Mac mini.
node_exporter runs inside a Linux container and sees the container's resources, not macOS host metrics. For real Mac mini CPU/memory/disk metrics, run node_exporter natively on macOS via Homebrew.
# Install node_exporter natively on macOS
brew install node_exporter
# Start as a background service (survives reboots)
brew services start node_exporter
# Verify metrics at:
curl http://localhost:9100/metrics | head -20
# ~/homelab/monitoring/prometheus/prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: node
static_configs:
- targets: ["host.docker.internal:9100"] # macOS host from container
- job_name: caddy
static_configs:
- targets: ["caddy:2019"]
- job_name: adguard
static_configs:
- targets: ["adguard:9617"] # adguard-exporter sidecar
# ~/homelab/monitoring/docker-compose.yml
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=30d'
networks:
- homelab
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
environment:
GF_SECURITY_ADMIN_PASSWORD: "changeme"
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
networks:
- homelab
restart: unless-stopped
loki:
image: grafana/loki:latest
container_name: loki
volumes:
- loki_data:/loki
networks:
- homelab
restart: unless-stopped
promtail:
image: grafana/promtail:latest
container_name: promtail
volumes:
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock
networks:
- homelab
restart: unless-stopped
volumes:
prometheus_data:
grafana_data:
loki_data:
networks:
homelab:
external: true
Recommended Grafana dashboards
- Node Exporter Full — Dashboard ID
1860— CPU, memory, disk, network for the Mac mini - Caddy dashboard — request rates, latency, error rates per service
- AdGuard Exporter — queries blocked, top blocked domains, DNS latency
- Docker containers — Dashboard ID
11600— per-container resource usage
Uptime Kuma — Status page
A simple, beautiful uptime monitor for all your services. Shows response time history and sends alerts (email, Telegram, Slack).
services:
uptime-kuma:
image: louislam/uptime-kuma:latest
container_name: uptime-kuma
volumes:
- uptime_data:/app/data
networks:
- homelab
restart: unless-stopped
Phase 6 — Storage & Backups
The 3-2-1 backup rule
- 3 copies of your data
- 2 different storage media/locations
- 1 copy offsite (cloud or remote)
Time Machine — macOS host backup
# Time Machine via CLI — backup to an external drive
sudo tmutil setdestination /Volumes/backup
# Exclude Docker data (large, rebuildable)
tmutil addexclusion ~/.orbstack
tmutil addexclusion ~/Library/Containers/com.docker.docker
Nextcloud — File sync & cloud replacement
Self-hosted Dropbox/Google Drive replacement. Sync files from all your devices to the Mac mini.
services:
nextcloud:
image: nextcloud:latest
container_name: nextcloud
environment:
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: changeme
NEXTCLOUD_TRUSTED_DOMAINS: "cloud.home"
volumes:
- /Volumes/data/nextcloud:/var/www/html
networks:
- homelab
restart: unless-stopped
Offsite backup with Backblaze B2
# Install rclone
brew install rclone
# Configure Backblaze B2
rclone config
# → New remote → b2 → enter your B2 key/secret
# Sync critical data to B2 (run via cron or launchd)
rclone sync /Volumes/data/vaultwarden b2:my-homelab-backup/vaultwarden
rclone sync /Volumes/data/gitea b2:my-homelab-backup/gitea
# Add to crontab — run at 3am daily
crontab -e
0 3 * * * /opt/homebrew/bin/rclone sync /Volumes/data b2:my-homelab-backup
Docker volume backup
# Backup a named Docker volume to a tarball
docker run --rm \
-v prometheus_data:/data \
-v /Volumes/backup/docker:/backup \
alpine tar czf /backup/prometheus_data.tar.gz /data
# Script to backup all volumes
for vol in prometheus_data grafana_data loki_data; do
docker run --rm \
-v ${vol}:/data \
-v /Volumes/backup/docker:/backup \
alpine tar czf /backup/${vol}-$(date +%Y%m%d).tar.gz /data
done
Core Services Catalog
FileBrowser — docker-compose
# ~/homelab/filebrowser/docker-compose.yml
services:
filebrowser:
image: filebrowser/filebrowser:latest
container_name: filebrowser
environment:
FB_BASEURL: "/files" # serve at /files path (optional)
volumes:
- /Volumes/data:/srv # root of what's browseable
- ./filebrowser.db:/database/filebrowser.db
- ./settings.json:/.filebrowser.json
networks:
- homelab
restart: unless-stopped
# Add to Caddyfile:
files.home {
reverse_proxy filebrowser:8080
}
Advanced Services
Media Stack
The *arr ecosystem automates TV/movie downloads and organizes your library. Jellyfin serves it to your devices. Add Shoko for anime-specific management, Bazarr for automatic subtitles, and RDTClient + FlareSolverr for premium debrid-based downloads.
Media servers
*arr automation stack
Download clients
RDTClient docker-compose
# ~/homelab/media/docker-compose.yml (excerpt)
services:
rdtclient:
image: rogerfar/rdtclient:latest
container_name: rdtclient
environment:
PUID: "1000"
PGID: "1000"
volumes:
- /Volumes/data/downloads:/data/downloads
- ./rdtclient:/data/db
networks:
- homelab
restart: unless-stopped
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr
environment:
LOG_LEVEL: info
networks:
- homelab
restart: unless-stopped
bazarr:
image: lscr.io/linuxserver/bazarr:latest
container_name: bazarr
environment:
PUID: "1000"
PGID: "1000"
TZ: Asia/Bangkok
volumes:
- ./bazarr/config:/config
- /Volumes/data:/data
networks:
- homelab
restart: unless-stopped
Shoko + Jellyfin setup
# ~/homelab/media/shoko/docker-compose.yml
services:
shoko:
image: ghcr.io/shokoanime/shokoserver:latest
container_name: shoko
environment:
PUID: "1000"
PGID: "1000"
TZ: Asia/Bangkok
volumes:
- ./shoko/config:/home/shoko/.shoko
- /Volumes/data/anime:/anime # your anime library
networks:
- homelab
restart: unless-stopped
# After setup:
# 1. Connect Shoko to your anime folder
# 2. Install Shoko Metadata plugin in Jellyfin
# 3. Point Jellyfin anime library to /anime
# 4. Jellyfin now uses Shoko for all anime metadata
*arr stack volume layout
# All *arr services share the same media directory
# This lets Radarr/Sonarr do instant hardlinks (no copy)
/Volumes/data/
├── downloads/ # qBittorrent / RDTClient downloads here
│ ├── complete/
│ └── incomplete/
├── movies/ # Radarr moves completed downloads here
├── tv/ # Sonarr moves completed downloads here
├── anime/ # Shoko manages this folder
└── music/
# All containers mount the SAME /data path — hardlinks work across arr apps
services:
radarr:
volumes:
- /Volumes/data:/data
rdtclient:
volumes:
- /Volumes/data/downloads:/data/downloads
jellyfin:
volumes:
- /Volumes/data:/data
Reverse Proxy & TLS Details
Local HTTPS with Caddy's internal CA
For LAN-only services, Caddy can act as its own CA and issue certificates. You import Caddy's root cert into your devices once, and all local services get trusted HTTPS.
# Tell Caddy to use internal TLS for .home domains
vault.home {
tls internal
reverse_proxy vaultwarden:80
}
# Get the root cert from Caddy's data directory
# ~/homelab/caddy/data/caddy/pki/authorities/local/root.crt
# Import it on your Mac
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain \
~/homelab/caddy/data/caddy/pki/authorities/local/root.crt
# On iOS: AirDrop the .crt file → Settings → General → VPN & Device Management → Install
Let's Encrypt for public domains
# Caddy auto-obtains Let's Encrypt certs for public domains
vault.yourdomain.com {
reverse_proxy vaultwarden:80
}
# That's it — Caddy handles cert issuance + renewal automatically
Alternative: Nginx Proxy Manager (GUI approach)
If you prefer clicking over editing config files, Nginx Proxy Manager (NPM) gives you a web UI to manage all proxy hosts, SSL certs, and access lists. This is what the dashboard in the example screenshot uses.
| Caddy | Nginx Proxy Manager | |
|---|---|---|
| Config method | Text file (Caddyfile) | Web UI (click-based) |
| Auto HTTPS | ✓ Zero config | ✓ Built-in Let's Encrypt UI |
| Learning curve | Low (simple syntax) | Very low (GUI) |
| Version control | ✓ Caddyfile in git | ✗ Config in SQLite DB |
| Internal CA | ✓ Built-in | ✗ Not supported |
| Best for | Code-first, git-backed | Beginners, quick setup |
# ~/homelab/nginx-proxy-manager/docker-compose.yml
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
ports:
- "80:80"
- "443:443"
- "81:81" # Admin UI
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- homelab
restart: unless-stopped
# Default login: admin@example.com / changeme
# Then: Hosts → Proxy Hosts → Add Proxy Host
# Domain: vault.home → Forward to: vaultwarden:80
# SSL tab: Request new cert → Let's Encrypt
Remote Access Options
| Method | Setup | Best for | Cost |
|---|---|---|---|
| Tailscale | 5 min | Personal use, all services via VPN | Free (personal) |
| Cloudflare Tunnel | 15 min | Public-facing services, no port forwarding | Free |
| WireGuard (self-managed) | 1–2 hr | Full control, no third-party | Free (VPS for endpoint ~$5/mo) |
| Port forwarding + DDNS | 30 min | Direct access, ISP allows it | Free (if ISP allows) |
| ZeroTier | 10 min | Alternative to Tailscale, self-hosted controller option | Free (25 devices) |
Security Hardening
- ☐Disable password SSH, use key auth only
Edit/etc/ssh/sshd_config:PasswordAuthentication no - ☐Change default SSH port
SetPort 2222in sshd_config — stops automated scans on port 22 - ☐Enable macOS Firewall
System Settings → Network → Firewall → ON. Block all incoming except what you explicitly allow. - ☐Disable SSH password auth on all containers too
Gitea, etc. — disable password login in their web UIs - ☐Set SIGNUPS_ALLOWED=false on Vaultwarden
After creating your account, immediately disable public registration - ☐Use strong, unique Admin API keys / tokens
Generate withopenssl rand -hex 32. Store in Vaultwarden itself. - ☐Never expose admin UIs publicly
Portainer, Grafana, AdGuard, Prometheus → LAN or Tailscale only - ☐Keep containers updated
Rundocker compose pull && docker compose up -dweekly. Use Watchtower for automation. - ☐Use non-root users in Docker containers
Adduser: "1000:1000"where possible. Avoidprivileged: true. - ☐Enable macOS FileVault
System Settings → Privacy & Security → FileVault. Encrypts the internal SSD. - ☐Review open ports regularly
Runsudo lsof -i -P | grep LISTENto see what's exposed
# Watchtower — auto-updates containers (optional)
services:
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
WATCHTOWER_CLEANUP: "true"
WATCHTOWER_SCHEDULE: "0 0 3 * * *" # 3am daily
WATCHTOWER_NOTIFICATIONS: "slack"
WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL: "https://hooks.slack.com/..."
restart: unless-stopped
References
Official docs and community resources. All links verified as of 2026-06-28. Updated with Shoko, Bazarr, RDTClient, FlareSolverr, FileBrowser, Nginx Proxy Manager.