1. Warum einen privaten KI-Server aufbauen?
Das Senden sensibler Daten an KI-APIs von Drittanbietern wie OpenAI, Anthropic oder Google birgt Risiken, die viele Organisationen nicht akzeptieren können. Ein privater KI-Server behält jedes Byte an Daten unter Ihrer Kontrolle -- auf Hardware, die Sie besitzen oder exklusiv mieten -- ohne jegliche externen API-Aufrufe.
Datensouveränität
Ihre Prompts, Dokumente und Modellausgaben verlassen niemals Ihre Infrastruktur. Keine Protokollierung durch Dritte, kein Training mit Ihren Daten, kein Risiko von Datenlecks über API-Endpunkte, die Sie nicht kontrollieren.
Regulatorische Compliance
Erfüllen Sie die Compliance-Anforderungen von GDPR, HIPAA, SOC 2 und branchenspezifischen Vorgaben, indem Sie die KI-Verarbeitung innerhalb Ihrer Datengrenze halten. Keine Bedenken hinsichtlich grenzüberschreitender Datenübertragung.
Kostenvorhersehbarkeit
Feste monatliche Kosten unabhängig von der Nutzung. Keine Überraschungen bei der Abrechnung pro Token, keine plötzlichen Änderungen der Ratenbegrenzung, keine Preiserhöhungen durch API-Anbieter. Betreiben Sie unbegrenzte Inferenz rund um die Uhr zum Pauschaltarif.
Keine Ratenbegrenzungen
Verarbeiten Sie so viele Anfragen, wie Ihre Hardware bewältigen kann. Keine Obergrenzen für Tokens pro Minute, keine Warteschlangen für Anfragen auf Anbieterseite, keine verminderte Leistung zu Stoßzeiten.
Das Risiko von Cloud-APIs: Wenn Sie Daten an einen Cloud-KI-Anbieter senden, verlieren Sie die Kontrolle. Selbst mit vertraglichen Garantien durchqueren Ihre Daten Netzwerke, die Sie nicht verwalten, liegen auf Servern, die Sie nicht kontrollieren, und unterliegen der Sicherheitslage des Anbieters. Für regulierte Branchen -- Gesundheitswesen, Finanzen, Recht, Verteidigung -- ist dies oft ein Ausschlusskriterium.
Warum der Mac Mini M4? Die Unified-Memory-Architektur von Apple Silicon ermöglicht es Ihnen, Modelle mit 7B bis 70B Parametern auf einem einzigen Gerät auszuführen, das weniger als 15 W verbraucht. Die Speicherbandbreite des M4 (bis zu 120 GB/s) versorgt die GPU effizient mit Modellgewichten und liefert 30-40 Tokens/Sekunde für 7B-Modelle -- schnell genug für Echtzeit-Chat. Ab 85 $/Monat für dedizierte Hardware ist es die kosteneffizienteste Methode, einen privaten KI-Server aufzubauen.
2. Architekturübersicht
Der Stack des privaten KI-Servers besteht aus fünf Schichten, die alle lokal auf Ihrem Mac Mini M4 laufen. Es werden keine externen Dienste benötigt.
Systemarchitektur
# Port mapping summary for the full stack:
#
# Port 443 - nginx (HTTPS, public-facing)
# Port 80 - nginx (HTTP, redirects to HTTPS)
# Port 11434 - Ollama (LLM inference, internal only)
# Port 3000 - Open WebUI (chat interface, proxied via nginx)
# Port 8000 - RAG API (FastAPI, proxied via nginx)
# Port 8200 - ChromaDB (vector database, internal only)
# Port 51820 - WireGuard VPN (optional, for remote access)
3. Schritt 1: Server-Einrichtung
Beginnen Sie mit der Bereitstellung Ihres Mac Mini M4 und der Konfiguration des sicheren Fernzugriffs. Wenn Sie My Remote Mac nutzen, ist der SSH-Zugang standardmäßig enthalten.
SSH-Konfiguration
# Connect to your Mac Mini M4 via SSH
ssh admin@your-mac-mini.myremotemac.com
# Generate an SSH key pair (if you don't have one)
ssh-keygen -t ed25519 -C "ai-server-admin" -f ~/.ssh/id_ai_server
# Copy your public key to the server
ssh-copy-id -i ~/.ssh/id_ai_server.pub admin@your-mac-mini.myremotemac.com
# Configure SSH client for easier access
cat <<EOF >> ~/.ssh/config
Host ai-server
HostName your-mac-mini.myremotemac.com
User admin
IdentityFile ~/.ssh/id_ai_server
ForwardAgent no
ServerAliveInterval 60
ServerAliveCountMax 3
EOF
# Now connect with just:
ssh ai-server
System-Updates und Vorbereitung
# Update macOS to latest version
sudo softwareupdate --install --all --agree-to-license
# Install Homebrew (package manager)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install essential tools
brew install wget curl jq htop python@3.12 git
# Create a dedicated directory for AI services
mkdir -p ~/ai-server/{models,data,logs,config}
mkdir -p ~/ai-server/rag/{documents,vectorstore}
# Set up Python virtual environment for AI tools
python3.12 -m venv ~/ai-server/venv
source ~/ai-server/venv/bin/activate
pip install --upgrade pip setuptools wheel
Einen dedizierten Service-Benutzer erstellen (optional)
# Create a dedicated user for AI services (principle of least privilege)
sudo dscl . -create /Users/aiservice
sudo dscl . -create /Users/aiservice UserShell /bin/zsh
sudo dscl . -create /Users/aiservice RealName "AI Service Account"
sudo dscl . -create /Users/aiservice UniqueID 550
sudo dscl . -create /Users/aiservice PrimaryGroupID 20
sudo dscl . -create /Users/aiservice NFSHomeDirectory /Users/aiservice
sudo mkdir -p /Users/aiservice
sudo chown aiservice:staff /Users/aiservice
# Grant access to the AI server directory
sudo chown -R aiservice:staff ~/ai-server
4. Schritt 2: Ollama und Modelle installieren
Ollama ist das Rückgrat Ihres privaten KI-Servers. Es verwaltet Modell-Downloads, Quantisierung und stellt eine OpenAI-kompatible API bereit -- alles läuft lokal ohne externe Aufrufe.
Ollama installieren
# Download and install Ollama
curl -fsSL https://ollama.com/install.sh | sh
# Verify installation
ollama --version
# ollama version 0.5.4
# Start the Ollama server (runs on localhost:11434 by default)
ollama serve &
Modelle für verschiedene Anwendungsfälle herunterladen
# General-purpose assistant (recommended starting point)
ollama pull llama3:8b # 4.7 GB - fits 16GB RAM
# Instruction-following and reasoning
ollama pull mistral:7b # 4.1 GB - excellent for RAG
# Code generation and analysis
ollama pull codellama:7b # 3.8 GB - code-specific model
ollama pull codellama:13b # 7.4 GB - better code quality (needs 24GB)
# Embedding model for RAG pipeline
ollama pull nomic-embed-text # 274 MB - text embeddings
# Verify all models are downloaded
ollama list
# NAME SIZE MODIFIED
# llama3:8b 4.7 GB 2 minutes ago
# mistral:7b 4.1 GB 5 minutes ago
# codellama:7b 3.8 GB 8 minutes ago
# nomic-embed-text 274 MB 10 minutes ago
# Test a model interactively
ollama run llama3:8b "What are the benefits of self-hosted AI?"
Ollama als dauerhaften Dienst konfigurieren
# Create a launchd plist for Ollama to start on boot
cat <<'EOF' > ~/Library/LaunchAgents/com.ollama.server.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ollama.server</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/ollama</string>
<string>serve</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>OLLAMA_HOST</key>
<string>127.0.0.1:11434</string>
<key>OLLAMA_KEEP_ALIVE</key>
<string>-1</string>
<key>OLLAMA_NUM_PARALLEL</key>
<string>4</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/admin/ai-server/logs/ollama.log</string>
<key>StandardErrorPath</key>
<string>/Users/admin/ai-server/logs/ollama-error.log</string>
</dict>
</plist>
EOF
# Load the service
launchctl load ~/Library/LaunchAgents/com.ollama.server.plist
# Verify Ollama is running
curl -s http://localhost:11434/api/tags | jq '.models[].name'
# "llama3:8b"
# "mistral:7b"
# "codellama:7b"
# "nomic-embed-text"
Wichtig: Beachten Sie, dass OLLAMA_HOST auf 127.0.0.1:11434 (nur localhost) gesetzt ist. Dadurch wird sichergestellt, dass Ollama nicht direkt aus dem Netzwerk erreichbar ist. Jeglicher externer Zugriff erfolgt über den in Schritt 5 konfigurierten nginx Reverse-Proxy.
5. Schritt 3: Eine RAG-Pipeline aufbauen
Retrieval-Augmented Generation (RAG) ermöglicht es Ihrer KI, Fragen auf Basis Ihrer eigenen Dokumente zu beantworten -- Unternehmenswikis, juristische Verträge, technische Dokumentation -- ohne diese Daten an einen Cloud-Anbieter zu senden. Hier bauen wir eine vollständige RAG-Pipeline mit ChromaDB und LangChain auf.
Abhängigkeiten installieren
# Activate the virtual environment
source ~/ai-server/venv/bin/activate
# Install RAG pipeline dependencies
pip install \
langchain==0.1.20 \
langchain-community==0.0.38 \
langchain-chroma==0.1.0 \
chromadb==0.4.24 \
sentence-transformers==2.7.0 \
pypdf==4.2.0 \
docx2txt==0.8 \
fastapi==0.111.0 \
uvicorn==0.29.0 \
python-multipart==0.0.9 \
pydantic==2.7.1
Pipeline zur Dokumenteneinbindung
# ~/ai-server/rag/ingest.py
"""
Document ingestion pipeline for the private RAG system.
Loads PDFs, DOCX, and text files, splits them into chunks,
generates embeddings via Ollama, and stores them in ChromaDB.
"""
import os
import sys
from pathlib import Path
from langchain_community.document_loaders import (
PyPDFLoader,
Docx2txtLoader,
TextLoader,
DirectoryLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OllamaEmbeddings
from langchain_chroma import Chroma
# Configuration
DOCUMENTS_DIR = os.path.expanduser("~/ai-server/rag/documents")
VECTORSTORE_DIR = os.path.expanduser("~/ai-server/rag/vectorstore")
OLLAMA_BASE_URL = "http://localhost:11434"
EMBEDDING_MODEL = "nomic-embed-text"
CHUNK_SIZE = 1000
CHUNK_OVERLAP = 200
def load_documents(directory: str):
"""Load all supported document types from a directory."""
documents = []
path = Path(directory)
# Load PDFs
for pdf_file in path.glob("**/*.pdf"):
loader = PyPDFLoader(str(pdf_file))
documents.extend(loader.load())
print(f" Loaded: {pdf_file.name} ({len(loader.load())} pages)")
# Load DOCX files
for docx_file in path.glob("**/*.docx"):
loader = Docx2txtLoader(str(docx_file))
documents.extend(loader.load())
print(f" Loaded: {docx_file.name}")
# Load text files
for txt_file in path.glob("**/*.txt"):
loader = TextLoader(str(txt_file))
documents.extend(loader.load())
print(f" Loaded: {txt_file.name}")
# Load markdown files
for md_file in path.glob("**/*.md"):
loader = TextLoader(str(md_file))
documents.extend(loader.load())
print(f" Loaded: {md_file.name}")
return documents
def create_vectorstore(documents):
"""Split documents into chunks and store embeddings in ChromaDB."""
# Split documents into chunks
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = text_splitter.split_documents(documents)
print(f"\nSplit {len(documents)} documents into {len(chunks)} chunks")
# Create embeddings using Ollama (runs locally!)
embeddings = OllamaEmbeddings(
model=EMBEDDING_MODEL,
base_url=OLLAMA_BASE_URL,
)
# Store in ChromaDB
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=VECTORSTORE_DIR,
collection_name="private_docs",
)
print(f"Stored {len(chunks)} chunks in ChromaDB at {VECTORSTORE_DIR}")
return vectorstore
if __name__ == "__main__":
print("=== Private RAG Document Ingestion ===\n")
print(f"Loading documents from: {DOCUMENTS_DIR}")
docs = load_documents(DOCUMENTS_DIR)
print(f"\nTotal documents loaded: {len(docs)}")
if not docs:
print("No documents found. Add files to ~/ai-server/rag/documents/")
sys.exit(1)
print("\nCreating vector store...")
create_vectorstore(docs)
print("\nIngestion complete!")
RAG-Abfrage-API
# ~/ai-server/rag/api.py
"""
RAG Query API - FastAPI service for document Q&A.
Retrieves relevant chunks from ChromaDB and generates
answers using Ollama. Everything runs locally.
"""
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional, List
import os
import shutil
from langchain_community.embeddings import OllamaEmbeddings
from langchain_chroma import Chroma
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
# Configuration
VECTORSTORE_DIR = os.path.expanduser("~/ai-server/rag/vectorstore")
DOCUMENTS_DIR = os.path.expanduser("~/ai-server/rag/documents")
OLLAMA_BASE_URL = "http://localhost:11434"
EMBEDDING_MODEL = "nomic-embed-text"
LLM_MODEL = "mistral:7b"
app = FastAPI(
title="Private RAG API",
description="Self-hosted document Q&A with zero cloud dependencies",
version="1.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize components
embeddings = OllamaEmbeddings(
model=EMBEDDING_MODEL,
base_url=OLLAMA_BASE_URL,
)
vectorstore = Chroma(
persist_directory=VECTORSTORE_DIR,
embedding_function=embeddings,
collection_name="private_docs",
)
llm = Ollama(
model=LLM_MODEL,
base_url=OLLAMA_BASE_URL,
temperature=0.3,
num_ctx=4096,
)
# Custom prompt template
PROMPT_TEMPLATE = """Use the following context to answer the question.
If you cannot find the answer in the context, say "I don't have enough
information in the provided documents to answer this question."
Context:
{context}
Question: {question}
Answer:"""
prompt = PromptTemplate(
template=PROMPT_TEMPLATE,
input_variables=["context", "question"],
)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4},
),
chain_type_kwargs={"prompt": prompt},
return_source_documents=True,
)
class QueryRequest(BaseModel):
question: str
model: Optional[str] = "mistral:7b"
num_results: Optional[int] = 4
class QueryResponse(BaseModel):
answer: str
sources: List[dict]
model: str
@app.post("/query", response_model=QueryResponse)
async def query_documents(request: QueryRequest):
"""Query your private documents using RAG."""
try:
result = qa_chain.invoke({"query": request.question})
sources = [
{
"content": doc.page_content[:200] + "...",
"source": doc.metadata.get("source", "unknown"),
"page": doc.metadata.get("page", None),
}
for doc in result.get("source_documents", [])
]
return QueryResponse(
answer=result["result"],
sources=sources,
model=request.model,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/upload")
async def upload_document(file: UploadFile = File(...)):
"""Upload a document for ingestion into the RAG pipeline."""
allowed_types = [".pdf", ".docx", ".txt", ".md"]
ext = os.path.splitext(file.filename)[1].lower()
if ext not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"Unsupported file type. Allowed: {allowed_types}",
)
file_path = os.path.join(DOCUMENTS_DIR, file.filename)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
return {"message": f"Uploaded {file.filename}", "path": file_path}
@app.get("/health")
async def health_check():
"""Health check endpoint."""
return {"status": "healthy", "model": LLM_MODEL, "vectorstore": "chromadb"}
# Run: uvicorn api:app --host 127.0.0.1 --port 8000 --workers 2
Die RAG-Pipeline ausführen
# Step 1: Add your documents
cp /path/to/your/documents/*.pdf ~/ai-server/rag/documents/
cp /path/to/your/documents/*.docx ~/ai-server/rag/documents/
# Step 2: Run ingestion
cd ~/ai-server/rag
python ingest.py
# === Private RAG Document Ingestion ===
# Loading documents from: /Users/admin/ai-server/rag/documents
# Loaded: company-handbook.pdf (45 pages)
# Loaded: api-documentation.md
# Loaded: compliance-policy.docx
# Total documents loaded: 47
# Split 47 documents into 312 chunks
# Stored 312 chunks in ChromaDB
# Step 3: Start the RAG API server
uvicorn api:app --host 127.0.0.1 --port 8000 --workers 2 &
# Step 4: Test with a query
curl -s http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"question": "What is our company vacation policy?"}' | jq .
# {
# "answer": "According to the company handbook, employees receive...",
# "sources": [...],
# "model": "mistral:7b"
# }
6. Schritt 4: Open WebUI bereitstellen
Open WebUI bietet eine ausgefeilte, ChatGPT-ähnliche Oberfläche für die Interaktion mit Ihren lokalen Modellen. Es verbindet sich direkt mit Ollama und unterstützt Konversationen, Bildgenerierung und Dokument-Uploads -- alles läuft privat auf Ihrem Mac Mini.
Docker installieren
# Install Docker Desktop for Mac (Apple Silicon native)
brew install --cask docker
# Start Docker Desktop
open -a Docker
# Verify Docker is running
docker --version
# Docker version 26.1.0, build 9714adc
docker compose version
# Docker Compose version v2.27.0
Docker Compose für Open WebUI
# ~/ai-server/docker-compose.yml
version: '3.8'
services:
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
restart: unless-stopped
ports:
- "127.0.0.1:3000:8080"
environment:
# Connect to Ollama running on the host
- OLLAMA_BASE_URL=http://host.docker.internal:11434
# Disable telemetry and external connections
- ENABLE_SIGNUP=false
- ENABLE_COMMUNITY_SHARING=false
- WEBUI_AUTH=true
- WEBUI_SECRET_KEY=your-strong-secret-key-change-this
# Data privacy settings
- ENABLE_OPENAI_API=false
- ENABLE_OLLAMA_API=true
- SAFE_MODE=true
volumes:
- open-webui-data:/app/backend/data
extra_hosts:
- "host.docker.internal:host-gateway"
# Optional: ChromaDB as a persistent service
chromadb:
image: chromadb/chroma:latest
container_name: chromadb
restart: unless-stopped
ports:
- "127.0.0.1:8200:8000"
volumes:
- chromadb-data:/chroma/chroma
environment:
- IS_PERSISTENT=TRUE
- ANONYMIZED_TELEMETRY=FALSE
volumes:
open-webui-data:
chromadb-data:
Starten und konfigurieren
# Start the services
cd ~/ai-server
docker compose up -d
# Check that containers are running
docker compose ps
# NAME STATUS PORTS
# open-webui Up 2 minutes 127.0.0.1:3000->8080/tcp
# chromadb Up 2 minutes 127.0.0.1:8200->8000/tcp
# View logs
docker compose logs -f open-webui
# Open WebUI is now accessible at http://localhost:3000
# On first visit, create an admin account (this account is local only)
# Verify Ollama connectivity from Open WebUI
curl -s http://localhost:3000/api/config | jq '.ollama'
# To update Open WebUI later:
docker compose pull && docker compose up -d
Hinweis zum Datenschutz: Open WebUI ist mit ENABLE_OPENAI_API=false und ENABLE_COMMUNITY_SHARING=false konfiguriert, um sicherzustellen, dass keine Daten an externe Dienste gesendet werden. Die Einstellung ENABLE_SIGNUP=false verhindert, dass nicht autorisierte Benutzer Konten erstellen. Alle Konversationen werden lokal im Docker-Volume gespeichert.
7. Schritt 5: API-Gateway mit nginx
nginx dient als einziger Einstiegspunkt für alle Dienste. Es übernimmt die SSL/TLS-Terminierung, Ratenbegrenzung, Authentifizierung und leitet den Datenverkehr an Ollama, Open WebUI und die RAG-API weiter.
nginx installieren und konfigurieren
# Install nginx
brew install nginx
# Generate a self-signed SSL certificate (or use Let's Encrypt)
mkdir -p ~/ai-server/config/ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ~/ai-server/config/ssl/server.key \
-out ~/ai-server/config/ssl/server.crt \
-subj "/CN=ai-server.local/O=Private AI/C=US"
nginx-Konfiguration
# /opt/homebrew/etc/nginx/nginx.conf
worker_processes auto;
error_log /Users/admin/ai-server/logs/nginx-error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
# Logging
access_log /Users/admin/ai-server/logs/nginx-access.log;
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=chat:10m rate=60r/m;
limit_req_zone $binary_remote_addr zone=upload:10m rate=5r/m;
# Connection limits
limit_conn_zone $binary_remote_addr zone=addr:10m;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'" always;
# Redirect HTTP to HTTPS
server {
listen 80;
server_name ai-server.local;
return 301 https://$host$request_uri;
}
# Main HTTPS server
server {
listen 443 ssl;
server_name ai-server.local;
ssl_certificate /Users/admin/ai-server/config/ssl/server.crt;
ssl_certificate_key /Users/admin/ai-server/config/ssl/server.key;
# Client body size limit (for document uploads)
client_max_body_size 50M;
# Open WebUI (chat interface)
location / {
limit_req zone=chat burst=20 nodelay;
limit_conn addr 10;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support for streaming
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
}
# Ollama API (for programmatic access)
location /ollama/ {
limit_req zone=api burst=10 nodelay;
limit_conn addr 5;
# Basic auth for API access
auth_basic "Private AI API";
auth_basic_user_file /Users/admin/ai-server/config/.htpasswd;
rewrite ^/ollama/(.*) /$1 break;
proxy_pass http://127.0.0.1:11434;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
}
# RAG API
location /rag/ {
limit_req zone=api burst=10 nodelay;
limit_conn addr 5;
auth_basic "Private AI API";
auth_basic_user_file /Users/admin/ai-server/config/.htpasswd;
rewrite ^/rag/(.*) /$1 break;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 120s;
}
# Document upload endpoint
location /rag/upload {
limit_req zone=upload burst=3 nodelay;
auth_basic "Private AI API";
auth_basic_user_file /Users/admin/ai-server/config/.htpasswd;
rewrite ^/rag/(.*) /$1 break;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Health check (no auth required)
location /health {
proxy_pass http://127.0.0.1:8000/health;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
}
}
Authentifizierung einrichten und starten
# Install htpasswd utility
brew install httpd
# Create API credentials
htpasswd -c ~/ai-server/config/.htpasswd api-user
# Enter a strong password when prompted
# Test nginx configuration
nginx -t
# nginx: configuration file /opt/homebrew/etc/nginx/nginx.conf test is successful
# Start nginx
brew services start nginx
# Test HTTPS access
curl -k https://localhost/health
# {"status": "healthy", "model": "mistral:7b", "vectorstore": "chromadb"}
# Test API access with authentication
curl -k -u api-user:your-password \
https://localhost/ollama/api/tags | jq '.models[].name'
# Test RAG query through nginx
curl -k -u api-user:your-password \
https://localhost/rag/query \
-H "Content-Type: application/json" \
-d '{"question": "What is our data retention policy?"}'
8. Sicherheitshärtung
Ein privater KI-Server ist nur so sicher wie sein schwächster Einstiegspunkt. Dieser Abschnitt behandelt die Firewall-Konfiguration, die SSH-Härtung, den VPN-Zugang und die Intrusion Detection, um eine mehrstufige Sicherheitslage (Defense-in-Depth) zu schaffen.
macOS-Firewall mit pf
# ~/ai-server/config/pf.rules
#
# Packet Filter rules for private AI server
# Only allow SSH, HTTPS, and WireGuard VPN from outside
# Define macros
ext_if = "en0"
vpn_if = "utun1"
# Default: block everything
block all
# Allow loopback traffic
pass quick on lo0 all
# Allow established connections
pass in quick on $ext_if proto tcp from any to any flags A/A
# Allow SSH (port 22) - restrict to known IPs if possible
pass in on $ext_if proto tcp from any to any port 22
# Allow HTTPS (port 443) through nginx
pass in on $ext_if proto tcp from any to any port 443
# Allow HTTP (port 80) for redirect to HTTPS
pass in on $ext_if proto tcp from any to any port 80
# Allow WireGuard VPN (port 51820)
pass in on $ext_if proto udp from any to any port 51820
# Allow all traffic on VPN interface
pass on $vpn_if all
# Allow all outbound traffic
pass out on $ext_if all
# Block everything else inbound (implicit from "block all")
# Internal services (11434, 3000, 8000, 8200) are NOT exposed
# --- Load these rules ---
# sudo pfctl -f ~/ai-server/config/pf.rules
# sudo pfctl -e # Enable pf
# sudo pfctl -sr # Show active rules
Ausschließliche SSH-Schlüssel-Authentifizierung
# Harden SSH configuration
# Edit /etc/ssh/sshd_config (requires sudo)
# Disable password authentication (key-only)
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM no
# Disable root login
PermitRootLogin no
# Only allow specific users
AllowUsers admin
# Use strong key exchange algorithms
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
# Reduce login grace time and max attempts
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 5
# Disable unused features
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
# Restart SSH
# sudo launchctl stop com.openssh.sshd
# sudo launchctl start com.openssh.sshd
WireGuard VPN für sicheren Fernzugriff
# Install WireGuard
brew install wireguard-tools
# Generate server keys
wg genkey | tee ~/ai-server/config/wg-server-private.key | \
wg pubkey > ~/ai-server/config/wg-server-public.key
# Generate client keys
wg genkey | tee ~/ai-server/config/wg-client-private.key | \
wg pubkey > ~/ai-server/config/wg-client-public.key
# Server configuration
cat <<EOF > ~/ai-server/config/wg0.conf
[Interface]
PrivateKey = $(cat ~/ai-server/config/wg-server-private.key)
Address = 10.66.66.1/24
ListenPort = 51820
PostUp = echo "WireGuard started"
PostDown = echo "WireGuard stopped"
[Peer]
# Client 1 - Your workstation
PublicKey = $(cat ~/ai-server/config/wg-client-public.key)
AllowedIPs = 10.66.66.2/32
PersistentKeepalive = 25
EOF
# Client configuration (copy to your workstation)
cat <<EOF > ~/ai-server/config/wg-client.conf
[Interface]
PrivateKey = $(cat ~/ai-server/config/wg-client-private.key)
Address = 10.66.66.2/24
DNS = 1.1.1.1
[Peer]
PublicKey = $(cat ~/ai-server/config/wg-server-public.key)
Endpoint = your-mac-mini.myremotemac.com:51820
AllowedIPs = 10.66.66.0/24
PersistentKeepalive = 25
EOF
# Start WireGuard on the server
sudo wg-quick up ~/ai-server/config/wg0.conf
# Verify connection
sudo wg show
# interface: utun1
# public key:
# listening port: 51820
#
# peer:
# allowed ips: 10.66.66.2/32
Überwachung von Anmeldeversuchen (fail2ban-Äquivalent)
# ~/ai-server/scripts/monitor-ssh.sh
#!/bin/bash
# Simple SSH brute-force detection and blocking for macOS
# Run via cron every 5 minutes
LOG_FILE="/var/log/system.log"
BLOCK_THRESHOLD=5
BLOCK_FILE="$HOME/ai-server/config/blocked_ips.txt"
# Find IPs with failed SSH attempts in the last 10 minutes
failed_ips=$(log show --predicate 'process == "sshd" AND eventMessage CONTAINS "Failed"' \
--last 10m 2>/dev/null | \
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | \
sort | uniq -c | sort -rn)
echo "$failed_ips" | while read count ip; do
if [ "$count" -ge "$BLOCK_THRESHOLD" ] && [ -n "$ip" ]; then
# Check if already blocked
if ! grep -q "$ip" "$BLOCK_FILE" 2>/dev/null; then
echo "$(date): Blocking $ip ($count failed attempts)" >> ~/ai-server/logs/security.log
echo "$ip" >> "$BLOCK_FILE"
# Add pf block rule
echo "block in quick from $ip to any" | sudo pfctl -f - -a "blocked/$ip" 2>/dev/null
fi
fi
done
# Make executable and add to crontab:
# chmod +x ~/ai-server/scripts/monitor-ssh.sh
# crontab -e
# */5 * * * * ~/ai-server/scripts/monitor-ssh.sh
Sicherheits-Checkliste:
- Ausschließliche SSH-Schlüssel-Authentifizierung aktiviert, Passwörter deaktiviert
- Alle KI-Dienste ausschließlich an localhost gebunden (127.0.0.1)
- nginx ist der einzige öffentlich zugängliche Dienst (Ports 80/443)
- Die pf-Firewall blockiert jeglichen nicht essenziellen eingehenden Datenverkehr
- WireGuard VPN für die sichere Fernadministration
- Ratenbegrenzung an allen API-Endpunkten
- Basic Auth an den Endpunkten von Ollama und der RAG-API
- Automatisierte Brute-Force-Erkennung und IP-Blockierung
- Sicherheits-Header (HSTS, CSP, X-Frame-Options) bei allen Antworten
9. Kostenanalyse
Das überzeugendste Argument für einen privaten KI-Server sind die Kosten. Cloud-KI-APIs rechnen pro Token ab, und die Kosten steigen bei Skalierung rasant an. Ein Mac Mini M4 bietet unbegrenzte Inferenz zu einem festen monatlichen Tarif.
OpenAI-API vs. privater Mac Mini M4
Der Vergleich geht von der GPT-3.5-Turbo-Preisgestaltung aus (0,50 $/1M Eingabe-Tokens, 1,50 $/1M Ausgabe-Tokens) gegenüber einem dedizierten Mac Mini M4, der Llama 3 8B ausführt. Durchschnittliche Anfrage: 500 Eingabe-Tokens + 300 Ausgabe-Tokens.
| Monatliche Anfragen | OpenAI-API-Kosten | Mac-Mini-M4-Kosten | Ersparnis |
|---|---|---|---|
| 1,000 | $0.70 | $75 | -$74.30 (API cheaper) |
| 10,000 | $7.00 | $75 | -$68.00 (API cheaper) |
| 100,000 | $70.00 | $75 | ~Break-even |
| 500,000 | $350.00 | $75 | $275 (79% savings) |
| 1,000,000 | $700.00 | $75 | $625 (89% savings) |
Vergleich auf GPT-4-Niveau
Für Qualität auf GPT-4-Niveau vergleichen Sie GPT-4-Turbo (10 $/1M Eingabe, 30 $/1M Ausgabe) mit einem Mac Mini M4 Pro 48GB, der Llama 3 70B ausführt.
| Monatliche Anfragen | GPT-4-Turbo-Kosten | Mac-Mini-M4-Pro-Kosten | Ersparnis |
|---|---|---|---|
| 10,000 | $140.00 | $179 | ~Break-even |
| 100,000 | $1,400.00 | $179 | $1,221 (87% savings) |
| 1,000,000 | $14,000.00 | $179 | $13,821 (99% savings) |
Über die Kosten hinaus: Der wahre Wert eines privaten KI-Servers liegt nicht nur in den finanziellen Einsparungen. Es ist die Beseitigung der Anbieterabhängigkeit, die Garantie des Datenschutzes und die Freiheit, zu iterieren, ohne sich über die API-Abrechnung Gedanken machen zu müssen. Ihre Kosten bleiben konstant, egal ob Sie 100 oder 10 Millionen Anfragen ausführen.
10. Skalierung
Ein einzelner Mac Mini M4 kann 2-4 gleichzeitige Anfragen für ein 7B-Modell bewältigen. Wenn Sie mehr Durchsatz benötigen oder eine Modellspezialisierung wünschen, ist die horizontale Skalierung mit mehreren Mac Minis unkompliziert.
Multi-Node-Architektur
Node 1: Allgemeiner Chat
Mac Mini M4 16GB
Llama 3 8B für allgemeine Konversationen, Kundensupport und Content-Generierung.
$75/mo
Node 2: Code-Assistent
Mac Mini M4 24GB
CodeLlama 13B für Codegenerierung, Review und Refactoring-Aufgaben.
$95/mo
Node 3: RAG und Reasoning
Mac Mini M4 Pro 48GB
Llama 3 70B für komplexe Dokumentenanalyse, juristische Recherche und tiefgehende Reasoning-Aufgaben.
$179/mo
Lastverteilung mit nginx
# nginx upstream configuration for multi-node load balancing
# Define upstream groups by model type
upstream ollama_general {
# Round-robin across general chat nodes
server 10.66.66.10:11434; # Node 1
server 10.66.66.11:11434; # Node 1 replica (if needed)
keepalive 8;
}
upstream ollama_code {
server 10.66.66.20:11434; # Node 2 - Code models
keepalive 4;
}
upstream ollama_reasoning {
server 10.66.66.30:11434; # Node 3 - Large models
keepalive 4;
}
# Model routing based on request path
server {
listen 443 ssl;
server_name ai-cluster.local;
# Route general chat requests
location /v1/chat/ {
proxy_pass http://ollama_general;
proxy_read_timeout 300s;
}
# Route code generation requests
location /v1/code/ {
proxy_pass http://ollama_code;
proxy_read_timeout 300s;
}
# Route reasoning/analysis requests
location /v1/reasoning/ {
proxy_pass http://ollama_reasoning;
proxy_read_timeout 600s;
}
}
Skript zum Modell-Routing
# ~/ai-server/scripts/model_router.py
"""
Intelligent model router that directs requests to the appropriate
Mac Mini node based on the requested model and current load.
"""
from fastapi import FastAPI, Request
import httpx
import asyncio
app = FastAPI()
NODES = {
"general": {
"url": "http://10.66.66.10:11434",
"models": ["llama3:8b", "mistral:7b"],
},
"code": {
"url": "http://10.66.66.20:11434",
"models": ["codellama:7b", "codellama:13b"],
},
"reasoning": {
"url": "http://10.66.66.30:11434",
"models": ["llama3:70b", "mixtral:8x7b"],
},
}
def get_node_for_model(model: str) -> str:
"""Find which node hosts the requested model."""
for node_name, config in NODES.items():
if model in config["models"]:
return config["url"]
# Default to general node
return NODES["general"]["url"]
@app.post("/v1/chat/completions")
async def route_chat(request: Request):
body = await request.json()
model = body.get("model", "llama3:8b")
target_url = get_node_for_model(model)
async with httpx.AsyncClient(timeout=300) as client:
response = await client.post(
f"{target_url}/v1/chat/completions",
json=body,
)
return response.json()
@app.get("/v1/models")
async def list_all_models():
"""Aggregate model lists from all nodes."""
all_models = []
async with httpx.AsyncClient(timeout=10) as client:
for node_name, config in NODES.items():
try:
resp = await client.get(f"{config['url']}/api/tags")
models = resp.json().get("models", [])
for m in models:
m["node"] = node_name
all_models.extend(models)
except Exception:
pass
return {"models": all_models}
# Run: uvicorn model_router:app --host 0.0.0.0 --port 8080
11. Häufig gestellte Fragen
Ist ein selbst gehosteter KI-Server wirklich privat, wenn ich die Hardware miete?
Ja. Wenn Sie einen dedizierten Mac Mini bei My Remote Mac mieten, erhalten Sie exklusiven Zugriff auf die physische Hardware. Kein anderer Kunde teilt sich Ihre Maschine. Alle Daten werden auf der SSD Ihres Servers gespeichert und im Ruhezustand verschlüsselt. SSH-Schlüssel stellen sicher, dass nur Sie Zugriff haben. Wenn Sie Ihr Abonnement beenden, wird das Laufwerk sicher gelöscht. Dies unterscheidet sich grundlegend von geteilten Cloud-VMs, bei denen andere Mandanten auf demselben physischen Host laufen.
Kann ich mit diesem Setup die GDPR- und HIPAA-Compliance erfüllen?
Ein privater KI-Server erfüllt die zentralen technischen Anforderungen der GDPR (Daten bleiben unter Ihrer Kontrolle, keine Verarbeitung durch Dritte ohne Einwilligung) und der HIPAA (PHI wird nicht an Cloud-KI-Anbieter übertragen). Vollständige Compliance erfordert jedoch auch organisatorische Kontrollen, Audit-Protokollierung, Verschlüsselungsrichtlinien und möglicherweise eine BAA mit Ihrem Hosting-Anbieter. Nutzen Sie dieses Setup als technische Grundlage und arbeiten Sie mit Ihrem Compliance-Team am Gesamtbild.
Wie schneidet die Qualität lokaler Modelle im Vergleich zu GPT-4 oder Claude ab?
Für spezifische, klar definierte Aufgaben (dokumentbasierte Q&A, Codegenerierung, Zusammenfassung, Klassifizierung) erreichen Open-Source-Modelle wie Llama 3 8B und Mistral 7B 85-95 % der Qualität von GPT-3.5. Llama 3 70B nähert sich in vielen Benchmarks der Qualität von GPT-4 an. Die Qualitätslücke verringert sich mit RAG-Pipelines, bei denen das Abrufen des richtigen Kontexts wichtiger ist als die reine Modellleistung. Für allgemeines kreatives Schreiben oder komplexes mehrstufiges Reasoning haben führende Cloud-Modelle weiterhin einen Vorteil.
Was passiert, wenn der Server ausfällt?
Alle Dienste sind mit KeepAlive (Ollama über launchd) und restart: unless-stopped (Docker-Container) konfiguriert. Wenn der Mac Mini neu startet, starten alle Dienste automatisch neu. Für Produktionslasten sollten Sie den Betrieb von zwei Mac Minis mit nginx-Lastverteilung für Hochverfügbarkeit in Betracht ziehen. Die Infrastruktur von My Remote Mac umfasst eine Überwachung rund um die Uhr sowie redundante Strom- und Netzwerkanbindung.
Kann ich dieses Setup mit bestehenden Tools wie Cursor, Continue.dev oder VS Code verwenden?
Auf jeden Fall. Da Ollama eine OpenAI-kompatible API bereitstellt, kann jedes Tool, das sich mit einem OpenAI-Endpunkt verbinden kann, Ihren privaten Server nutzen. Richten Sie in Cursor oder Continue.dev die API-Basis-URL auf https://your-server/ollama/v1 mit Ihren Basic-Auth-Zugangsdaten. VS-Code-Erweiterungen wie Continue, Cody und Tabby unterstützen alle benutzerdefinierte Endpunkte. Ihr Code verlässt niemals Ihren Server.
Wie aktualisiere ich Modelle, wenn neue Versionen veröffentlicht werden?
Die Aktualisierung von Modellen erfolgt mit einem einzigen Befehl: ollama pull llama3:8b lädt die neueste Version herunter. Die alte Version bleibt erhalten, bis Sie sie explizit mit ollama rm entfernen. Sie können neue Modellversionen parallel zu den bestehenden testen und in der Produktion ohne Ausfallzeit umstellen, indem Sie den Modellnamen in Ihren API-Aufrufen oder der nginx-Konfiguration aktualisieren.
Was ist mit der Modelllizenzierung? Kann ich Llama 3 kommerziell nutzen?
Llama 3 wird unter der Meta Llama 3 Community License veröffentlicht, die die kommerzielle Nutzung für Organisationen mit weniger als 700 Millionen monatlich aktiven Nutzern erlaubt. Mistral-Modelle werden unter der Apache 2.0-Lizenz (vollständig permissiv) veröffentlicht. CodeLlama folgt der Llama 2 Community License. Prüfen Sie immer die spezifische Lizenz für jedes Modell, das Sie bereitstellen, aber für die überwiegende Mehrheit der Unternehmen sind diese Modelle in der Produktion frei nutzbar.
Verwandte Ratgeber
LLMs auf dem Mac Mini M4 betreiben
Detaillierter Ratgeber zum Betrieb von Llama, Mistral und Phi mit Ollama, llama.cpp und MLX. Inklusive Benchmarks.
Ratgeber zur CoreML-Bereitstellung
Stellen Sie CoreML-Modelle auf dedizierten Mac Mini M4-Servern für optimierte Produktionsinferenz bereit.
Mac Mini M4 vs. NVIDIA-GPU für KI
Detaillierter Leistungs- und Kostenvergleich zwischen Apple Silicon und NVIDIA für KI-Workloads.
KI und ML in der Cloud - Überblick
Überblick über die Mac-Mini-Cloud-Infrastruktur für KI- und Machine-Learning-Workloads.
Bauen Sie Ihren privaten KI-Server auf
Holen Sie sich einen dedizierten Mac Mini M4 und betreiben Sie LLMs mit vollständigem Datenschutz. Keine Cloud-APIs, keine Abrechnung pro Token, keine Anbieterbindung. Ab 85 $/Monat.