macOS Homelab Architecture: Modern Infrastructure Design
Building a professional homelab on macOS requires careful architecture planning, automation tooling, and Infrastructure as Code (IaC) principles. This guide covers designing scalable, maintainable infrastructure using modern DevOps practices.
Why macOS for Homelab Infrastructure?
- Unified Development Environment: Same OS for development and deployment
- Security Framework: Built-in encryption, secure boot, and sandboxing
- Performance Efficiency: Optimized for Apple Silicon architecture
- Container Support: Native Docker and Kubernetes compatibility
Integration Benefits
- Xcode Toolchain: iOS/macOS app development and testing
- Creative Workflows: Media processing and content creation
- Unix Foundation: Full POSIX compliance with modern tooling
- Enterprise Features: Directory services, remote management
Infrastructure Architecture Overview
Core Components
┌─────────────────────────────────────────────────────────────┐
│ macOS Homelab Stack │
├─────────────────────────────────────────────────────────────┤
│ Applications │ Portfolio Site │ AI Services │ Development │
├─────────────────────────────────────────────────────────────┤
│ Orchestration│ Kubernetes (K3s) │ Docker Compose │
├─────────────────────────────────────────────────────────────┤
│ Virtualization│ OrbStack │ Multipass │
├─────────────────────────────────────────────────────────────┤
│ Networking │ WireGuard VPN │ Traefik │ DNS (AdGuard) │
├─────────────────────────────────────────────────────────────┤
│ Storage │ ZFS Pools │ TimeMachine │ S3 Compatible │
├─────────────────────────────────────────────────────────────┤
│ Base OS │ macOS Sonoma │
└─────────────────────────────────────────────────────────────┘
Service Topology
# Infrastructure Services Map
services:
core:
- dns: AdGuard Home (192.168.1.10)
- vpn: WireGuard Server (192.168.1.10)
- proxy: Traefik (192.168.1.10:80/443)
- monitoring: Prometheus/Grafana (192.168.1.10:3000)
development:
- git: Gitea (192.168.1.11)
- ci: Jenkins (192.168.1.11:8080)
- registry: Harbor (192.168.1.11:5000)
- docs: Outline (192.168.1.11:3000)
ai_services:
- llm: Ollama (192.168.1.12:11434)
- ui: Open WebUI (192.168.1.12:8080)
- vector: Qdrant (192.168.1.12:6333)
media:
- streaming: Plex (192.168.1.13:32400)
- downloads: qBittorrent (192.168.1.13:8080)
- automation: Sonarr/Radarr (192.168.1.13)
Infrastructure as Code Setup
# terraform/main.tf
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.4"
}
}
}
provider "docker" {
host = "unix:///var/run/docker.sock"
}
# Network configuration
resource "docker_network" "homelab" {
name = "homelab-network"
ipam_config {
subnet = "172.20.0.0/16"
gateway = "172.20.0.1"
}
}
# Core services
module "core_services" {
source = "./modules/core"
network_id = docker_network.homelab.id
domain = var.homelab_domain
}
module "development_services" {
source = "./modules/development"
network_id = docker_network.homelab.id
domain = var.homelab_domain
}
module "ai_services" {
source = "./modules/ai"
network_id = docker_network.homelab.id
domain = var.homelab_domain
}
Ansible Automation
# ansible/playbooks/homelab-setup.yml
---
- name: Configure macOS Homelab Infrastructure
hosts: localhost
connection: local
become: false
vars:
homelab_domain: "samcloud.local"
services_directory: "/opt/homelab"
tasks:
- name: Install Homebrew packages
homebrew:
name:
- docker
- kubernetes-cli
- terraform
- ansible
- wireguard-tools
- traefik
state: present
- name: Create services directory structure
file:
path: "{{ services_directory }}/{{ item }}"
state: directory
mode: '0755'
loop:
- core
- development
- ai
- media
- monitoring
- name: Deploy core infrastructure
include_tasks: tasks/deploy-core.yml
- name: Configure networking
include_tasks: tasks/configure-networking.yml
- name: Setup monitoring
include_tasks: tasks/deploy-monitoring.yml
Docker Compose Orchestration
# docker-compose.yml
version: '3.8'
networks:
homelab:
external: true
services:
traefik:
image: traefik:v3.0
container_name: traefik
networks:
- homelab
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./config/traefik:/etc/traefik:ro
- ./data/traefik:/data
command:
- --api.dashboard=true
- --providers.docker=true
- --providers.file.directory=/etc/traefik/dynamic
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.email=admin@samcloud.local
- --certificatesresolvers.letsencrypt.acme.storage=/data/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
labels:
- traefik.http.routers.dashboard.rule=Host(`traefik.samcloud.local`)
- traefik.http.routers.dashboard.service=api@internal
restart: unless-stopped
adguard:
image: adguard/adguardhome:latest
container_name: adguard
networks:
homelab:
ipv4_address: 172.20.0.10
ports:
- "53:53/tcp"
- "53:53/udp"
- "3000:3000/tcp"
volumes:
- ./data/adguard/work:/opt/adguardhome/work
- ./data/adguard/conf:/opt/adguardhome/conf
labels:
- traefik.http.routers.adguard.rule=Host(`dns.samcloud.local`)
- traefik.http.services.adguard.loadbalancer.server.port=3000
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
container_name: prometheus
networks:
- homelab
ports:
- "9090:9090"
volumes:
- ./config/prometheus:/etc/prometheus:ro
- ./data/prometheus:/prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --web.console.libraries=/usr/share/prometheus/console_libraries
- --web.console.templates=/usr/share/prometheus/consoles
- --web.enable-lifecycle
labels:
- traefik.http.routers.prometheus.rule=Host(`prometheus.samcloud.local`)
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
networks:
- homelab
ports:
- "3001:3000"
volumes:
- ./data/grafana:/var/lib/grafana
- ./config/grafana:/etc/grafana:ro
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
labels:
- traefik.http.routers.grafana.rule=Host(`monitoring.samcloud.local`)
restart: unless-stopped
Kubernetes Cluster Setup (K3s)
K3s Installation
# Install K3s on macOS
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable traefik --disable servicelb" sh -
# Configure kubectl
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
# Verify cluster
kubectl get nodes
kubectl get pods --all-namespaces
ArgoCD for GitOps
# k8s/argocd/install.yml
apiVersion: v1
kind: Namespace
metadata:
name: argocd
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: argocd
namespace: argocd
spec:
project: default
source:
repoURL: https://argoproj.github.io/argo-helm
chart: argo-cd
targetRevision: 5.46.8
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
Application Deployment
# k8s/apps/portfolio-site.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: portfolio-site
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: portfolio-site
template:
metadata:
labels:
app: portfolio-site
spec:
containers:
- name: portfolio
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: site-content
mountPath: /usr/share/nginx/html
volumes:
- name: site-content
configMap:
name: portfolio-content
---
apiVersion: v1
kind: Service
metadata:
name: portfolio-service
spec:
selector:
app: portfolio-site
ports:
- port: 80
targetPort: 80
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: portfolio-ingress
annotations:
kubernetes.io/ingress.class: traefik
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- portfolio.samcloud.local
secretName: portfolio-tls
rules:
- host: portfolio.samcloud.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: portfolio-service
port:
number: 80
Storage Architecture
ZFS Pool Configuration
# Install OpenZFS for macOS
brew install openzfs
# Create ZFS pool for homelab data
sudo zpool create -o ashift=12 \
-O compression=lz4 \
-O atime=off \
-O recordsize=1M \
homelab-pool \
/dev/disk2
# Create datasets
sudo zfs create homelab-pool/docker
sudo zfs create homelab-pool/k8s
sudo zfs create homelab-pool/backups
sudo zfs create homelab-pool/media
# Configure snapshots
sudo zfs set com.sun:auto-snapshot=true homelab-pool
sudo zfs set com.sun:auto-snapshot:hourly=true homelab-pool/docker
sudo zfs set com.sun:auto-snapshot:daily=true homelab-pool/backups
S3-Compatible Storage (MinIO)
# docker-compose.storage.yml
services:
minio:
image: minio/minio:latest
container_name: minio
networks:
- homelab
ports:
- "9000:9000"
- "9001:9001"
volumes:
- ./data/minio:/data
environment:
MINIO_ROOT_USER: admin
MINIO_ROOT_PASSWORD: strongpassword
MINIO_SERVER_URL: https://s3.samcloud.local
MINIO_BROWSER_REDIRECT_URL: https://s3-console.samcloud.local
command: server /data --console-address ":9001"
labels:
- traefik.http.routers.minio-api.rule=Host(`s3.samcloud.local`)
- traefik.http.routers.minio-api.service=minio-api
- traefik.http.services.minio-api.loadbalancer.server.port=9000
- traefik.http.routers.minio-console.rule=Host(`s3-console.samcloud.local`)
- traefik.http.routers.minio-console.service=minio-console
- traefik.http.services.minio-console.loadbalancer.server.port=9001
restart: unless-stopped
Monitoring and Observability
Prometheus Configuration
# config/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "rules/*.yml"
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node-exporter'
static_configs:
- targets: ['172.20.0.20:9100']
- job_name: 'docker'
static_configs:
- targets: ['172.20.0.1:9323']
- job_name: 'traefik'
static_configs:
- targets: ['traefik:8080']
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
Grafana Dashboards
{
"dashboard": {
"title": "Homelab Infrastructure Overview",
"panels": [
{
"title": "System Overview",
"type": "stat",
"targets": [
{
"expr": "up",
"legendFormat": "Services Up"
}
]
},
{
"title": "CPU Usage",
"type": "graph",
"targets": [
{
"expr": "100 - (avg(irate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
"legendFormat": "CPU Usage %"
}
]
},
{
"title": "Memory Usage",
"type": "graph",
"targets": [
{
"expr": "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100",
"legendFormat": "Memory Usage %"
}
]
}
]
}
}
Alerting Rules
# config/prometheus/rules/alerts.yml
groups:
- name: homelab.rules
rules:
- alert: ServiceDown
expr: up == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Service {{ $labels.instance }} is down"
description: "{{ $labels.instance }} has been down for more than 5 minutes"
- alert: HighCPUUsage
expr: 100 - (avg(irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 10m
labels:
severity: warning
annotations:
summary: "High CPU usage detected"
description: "CPU usage is above 80% for more than 10 minutes"
- alert: HighMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90
for: 5m
labels:
severity: critical
annotations:
summary: "High memory usage detected"
description: "Memory usage is above 90% for more than 5 minutes"
Automation and CI/CD
Jenkins Pipeline
// Jenkinsfile for homelab deployments
pipeline {
agent any
environment {
KUBECONFIG = credentials('kubeconfig')
DOCKER_REGISTRY = 'harbor.samcloud.local'
}
stages {
stage('Checkout') {
steps {
git branch: 'main', url: 'https://gitea.samcloud.local/infrastructure/homelab.git'
}
}
stage('Terraform Plan') {
steps {
dir('terraform') {
sh 'terraform init'
sh 'terraform plan -out=tfplan'
}
}
}
stage('Deploy Infrastructure') {
when {
branch 'main'
}
steps {
dir('terraform') {
sh 'terraform apply tfplan'
}
}
}
stage('Deploy Applications') {
steps {
dir('k8s') {
sh 'kubectl apply -f manifests/'
}
}
}
stage('Run Tests') {
steps {
sh './scripts/test-infrastructure.sh'
}
}
}
post {
always {
archiveArtifacts artifacts: 'terraform/tfplan', fingerprint: true
}
failure {
emailext (
subject: "Pipeline Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "The homelab infrastructure pipeline has failed. Check the console output for details.",
to: "admin@samcloud.local"
)
}
}
}
GitHub Actions Workflow
# .github/workflows/infrastructure.yml
name: Infrastructure Deployment
on:
push:
branches: [main]
paths: ['terraform/**', 'k8s/**', 'ansible/**']
pull_request:
branches: [main]
jobs:
terraform:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.5.0
- name: Terraform Init
working-directory: terraform
run: terraform init
- name: Terraform Plan
working-directory: terraform
run: terraform plan
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
working-directory: terraform
run: terraform apply -auto-approve
kubernetes:
runs-on: macos-latest
needs: terraform
steps:
- uses: actions/checkout@v3
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.28.0'
- name: Deploy to Kubernetes
run: |
kubectl apply -f k8s/manifests/
kubectl rollout status deployment/portfolio-site
Security Implementation
Network Segmentation
# Create network isolation with pfctl
cat > /etc/pf.anchors/homelab << EOF
# Homelab network segmentation
table <homelab_core> { 172.20.0.0/24 }
table <homelab_dev> { 172.20.1.0/24 }
table <homelab_ai> { 172.20.2.0/24 }
table <homelab_media> { 172.20.3.0/24 }
# Core services - restricted access
pass in on en0 from <homelab_core> to any
block in from <homelab_dev> to <homelab_core>
block in from <homelab_media> to <homelab_core>
# Development - limited access
pass in from <homelab_dev> to <homelab_dev>
pass out from <homelab_dev> to any port {80, 443, 22, 53}
# AI services - isolated
pass in from <homelab_ai> to <homelab_ai>
block in from <homelab_ai> to any
# Media - restricted outbound
pass in from <homelab_media> to <homelab_media>
pass out from <homelab_media> to any port {80, 443}
EOF
# Load rules
sudo pfctl -f /etc/pf.conf
SSL/TLS Management
# cert-manager for automatic SSL
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@samcloud.local
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: traefik
Backup and Disaster Recovery
Automated Backup Strategy
#!/bin/bash
# scripts/backup-homelab.sh
BACKUP_ROOT="/Volumes/Backup/homelab"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="$BACKUP_ROOT/$TIMESTAMP"
# Create backup directory
mkdir -p "$BACKUP_DIR"
# Backup Docker volumes
echo "Backing up Docker volumes..."
docker run --rm -v homelab_data:/data -v "$BACKUP_DIR":/backup alpine \
tar czf /backup/docker-volumes.tar.gz -C /data .
# Backup Kubernetes resources
echo "Backing up Kubernetes resources..."
kubectl get all --all-namespaces -o yaml > "$BACKUP_DIR/k8s-resources.yaml"
# Backup ZFS snapshots
echo "Creating ZFS snapshots..."
sudo zfs snapshot homelab-pool@backup-$TIMESTAMP
# Backup configurations
echo "Backing up configurations..."
cp -r /opt/homelab "$BACKUP_DIR/config"
# Backup to remote S3
echo "Uploading to remote backup..."
aws s3 sync "$BACKUP_DIR" s3://homelab-backups/$(hostname)/$TIMESTAMP/
# Cleanup old backups (keep 30 days)
find "$BACKUP_ROOT" -type d -mtime +30 -exec rm -rf {} \;
echo "Backup completed: $BACKUP_DIR"
Conclusion
A well-architected macOS homelab provides a powerful foundation for development, learning, and production workloads. By implementing Infrastructure as Code, container orchestration, and comprehensive monitoring, you create a scalable, maintainable environment that rivals enterprise infrastructure.
The combination of macOS stability, modern containerization, and automation tools enables rapid deployment, easy maintenance, and robust disaster recovery capabilities.
Next: Advanced Networking: VLAN Segmentation and Traffic Shaping