How I Self-Host 17 Services on a Mac Mini
The complete guide to running a full homelab on Apple Silicon — from Docker stacks to reverse proxy, with compose files you can copy.
How I Self-Host 17 Services on a Mac Mini
I run my entire digital life on a Mac mini M4. Media server, Git, backups, monitoring, a custom freight dashboard, even an IRC bot — all running in Docker, all reachable from anywhere.
Here’s the full stack, how it’s wired together, and the exact compose files you can copy to get started.
The Hardware
| Device | Role |
|---|---|
| Mac mini M4 | Primary compute, Docker host |
| UGREEN NAS | Storage, compose files, media library |
| Raspberry Pi 5 | AdGuard Home, WireGuard (planned) |
| Raspberry Pi 4 | Media center (planned) |
| Raspberry Pi 3B | Home Assistant (planned) |
The Mac mini has 16GB RAM and a 512GB SSD. That’s it. No rack, no server motherboard, no noise.
The Software Stack
Everything runs in Docker via OrbStack on macOS. I group services into logical stacks:
Core Infrastructure
| Service | Purpose | Port |
|---|---|---|
| Portainer | Container management UI | 9000 |
| Nginx Proxy Manager | Reverse proxy + SSL | 80/443/81 |
| Watchtower | Auto-update containers | — |
| Duplicati | Backups to NAS + B2 | 8200 |
| Gitea | Self-hosted Git | 3000 |
Media Acquisition (“arr” Stack)
| Service | Purpose | Port |
|---|---|---|
| Prowlarr | Indexer aggregator | 9696 |
| Radarr | Movie management | 7878 |
| Sonarr | TV management | 8989 |
| Lidarr | Music management | 8686 |
| qBittorrent | Torrent client | 8080 |
| Jellyseerr | Request UI | 5055 |
| slskd | Soulseek client | 5030 |
| Mylar3 | Comics management | 8090 |
| MeTube | YouTube downloads | 8081 |
Media Server
| Service | Purpose | Port |
|---|---|---|
| Jellyfin | Media server | 8096 |
| Portfolio | Static site (nginx) | 3001 |
Monitoring
| Service | Purpose | Port |
|---|---|---|
| Beszel | System monitoring | 8089 |
| Dozzle | Docker log viewer | 8888 |
| Homepage | Service dashboard | 3005 |
| Dashboard | Custom status page | 3004 |
Tools
| Service | Purpose | Port |
|---|---|---|
| AnythingLLM | Local LLM chat | 3002 |
| Planka | Kanban board | 3333 |
| Daily Brief | News aggregator | 3003 |
Total: 17 services across 6 compose stacks.
Storage Architecture
The key insight: compose files live on the NAS, container data lives on the Mac mini’s SSD.
/Volumes/homelab/compose/ # NAS — all docker-compose.yml files
~/homelab-data/ # Mini SSD — container config volumes
/Volumes/homelab/media/ # NAS — Jellyfin library + downloads
This gives me:
- Portability: If the mini dies, all configs are on the NAS
- Speed: Container metadata and databases on local SSD
- Capacity: Media library grows on the NAS without filling the mini
Networking
Tailscale (Admin Access)
Install Tailscale on any device, log in, and you have direct access to all services:
100.102.8.89:9000 # Portainer
100.102.8.89:7878 # Radarr
100.102.8.89:8989 # Sonarr
100.102.8.89:8096 # Jellyfin
Cloudflare Tunnel (Public Access)
Seven services are public-facing via Cloudflare Tunnel — no port forwarding, no DDNS:
| URL | Service |
|---|---|
https://iamfaulty.com | Portfolio |
https://jellyfin.iamfaulty.com | Jellyfin |
https://request.iamfaulty.com | Jellyseerr |
https://gitea.iamfaulty.com | Gitea |
https://home.iamfaulty.com | Homepage |
https://dailybrief.iamfaulty.com | Daily Brief |
https://irc.iamfaulty.com | IRC |
The Compose Files
Core Stack
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: unless-stopped
ports: ["9000:9000"]
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ~/homelab-data/portainer:/data
npm:
image: jc21/nginx-proxy-manager:latest
container_name: npm
restart: unless-stopped
ports: ["80:80", "443:443", "81:81"]
volumes:
- ~/homelab-data/npm:/data
- ~/homelab-data/npm/letsencrypt:/etc/letsencrypt
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_POLL_INTERVAL=3600
duplicati:
image: lscr.io/linuxserver/duplicati:latest
container_name: duplicati
restart: unless-stopped
ports: ["8200:8200"]
volumes:
- ~/homelab-data/duplicati:/config
- /Volumes/homelab:/source:ro
- /Volumes/faultycloud:/backup
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: unless-stopped
ports: ["3000:3000", "222:22"]
volumes:
- ~/homelab-data/gitea:/data
Jellyfin
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
restart: unless-stopped
ports: ["8096:8096"]
volumes:
- ~/homelab-data/jellyfin/config:/config
- ~/homelab-data/jellyfin/cache:/cache
- /Volumes/homelab/media:/media:ro
See the private wiki for complete compose files for all 17 services.
What It Costs
| Item | Cost |
|---|---|
| Mac mini M4 (16GB) | ~$800 (one-time) |
| UGREEN NAS (4-bay) | ~$300 (one-time) |
| Cloudflare Tunnel | Free |
| Tailscale | Free (personal) |
| Backblaze B2 backups | ~$2/month |
| Domain (iamfaulty.com) | $12/year |
Total monthly: ~$3 for a full self-hosted platform.
Lessons Learned
- Put compose files on the NAS. When I rebuild the mini, I just re-mount the NAS and
docker compose up. - Use Nginx Proxy Manager. SSL certificates, reverse proxy rules, and access lists — all in a web UI.
- Tailscale for admin, Cloudflare for public. Two paths, zero port forwarding.
- Watchtower with
CLEANUP=true. Auto-updates without filling disk with old images. - SMB → NFS for arr hardlinks. Still migrating this. NFS enables instant imports without double-copies.
What’s Next
- Gluetun VPN kill switch for qBittorrent
- Pi 5 with AdGuard + WireGuard
- Duplicati → Backblaze B2 backup verification
- Home Assistant on Pi 3B
- The 24-phase teardown roadmap (it’s… extensive)
Want the full starter pack? Get the Docker Compose files →
Questions? Find me at iamfaulty.com or on IRC.