macOS Homelab Architecture: Infrastructure as Code

Jun 6, 2025

Design and implement a comprehensive macOS-based homelab infrastructure using Infrastructure as Code principles, containerization, and modern DevOps practices.

macOS Homelab Architecture: Infrastructure as Code

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?

Platform Advantages

  • 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 Configuration

# 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