1. Perché costruire un server IA privato?
Inviare dati sensibili ad API IA di terze parti come OpenAI, Anthropic o Google introduce rischi che molte organizzazioni non possono accettare. Un server IA privato mantiene ogni byte di dati sotto il tuo controllo -- su hardware che possiedi o affitti in esclusiva -- senza alcuna chiamata API esterna.
Sovranità dei dati
I tuoi prompt, documenti e output dei modelli non lasciano mai la tua infrastruttura. Nessuna registrazione da parte di terzi, nessun addestramento sui tuoi dati, nessun rischio di fughe di dati tramite endpoint API che non controlli.
Conformità normativa
Soddisfa i requisiti di conformità di GDPR, HIPAA, SOC 2 e specifici del settore mantenendo l'elaborazione IA all'interno del tuo perimetro dati. Nessuna preoccupazione per il trasferimento transfrontaliero dei dati.
Prevedibilità dei costi
Costo mensile fisso indipendentemente dall'utilizzo. Nessuna sorpresa di fatturazione per token, nessuna variazione improvvisa dei limiti di frequenza, nessun aumento di prezzo dai fornitori di API. Esegui inferenza illimitata 24/7 a una tariffa forfettaria.
Nessun limite di frequenza
Elabora tutte le richieste che il tuo hardware è in grado di gestire. Nessun limite di token al minuto, nessuna coda di richieste sul lato del fornitore, nessun degrado delle prestazioni nelle ore di punta.
Il rischio delle API cloud: Quando invii dati a un fornitore di IA cloud, perdi il controllo. Anche con garanzie contrattuali, i tuoi dati attraversano reti che non gestisci, risiedono su server che non controlli e sono soggetti alla postura di sicurezza del fornitore. Per i settori regolamentati -- sanità, finanza, legale, difesa -- questo è spesso inaccettabile.
Perché il Mac Mini M4? L'architettura di memoria unificata di Apple Silicon ti permette di eseguire modelli da 7B a 70B parametri su un singolo dispositivo che consuma meno di 15 W. La larghezza di banda della memoria dell'M4 (fino a 120 GB/s) alimenta in modo efficiente i pesi del modello verso la GPU, offrendo 30-40 token/secondo per i modelli 7B -- abbastanza veloce per la chat in tempo reale. A partire da 85 $/mese per hardware dedicato, è il modo più conveniente per costruire un server IA privato.
2. Panoramica dell'architettura
Lo stack del server IA privato è composto da cinque livelli, tutti eseguiti localmente sul tuo Mac Mini M4. Non è richiesto alcun servizio esterno.
Architettura del sistema
# 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. Passo 1: Configurazione del server
Inizia con il provisioning del tuo Mac Mini M4 e la configurazione dell'accesso remoto sicuro. Se usi My Remote Mac, l'accesso SSH è fornito da subito.
Configurazione SSH
# 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
Aggiornamenti di sistema e preparazione
# 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
Crea un utente di servizio dedicato (opzionale)
# 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. Passo 2: Installa Ollama e i modelli
Ollama è la spina dorsale del tuo server IA privato. Gestisce il download dei modelli, la quantizzazione e fornisce un'API compatibile con OpenAI -- tutto eseguito localmente senza alcuna chiamata esterna.
Installa Ollama
# 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 &
Scarica i modelli per diversi casi d'uso
# 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?"
Configura Ollama come servizio persistente
# 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"
Importante: Nota che OLLAMA_HOST è impostato su 127.0.0.1:11434 (solo localhost). Questo garantisce che Ollama non sia direttamente accessibile dalla rete. Tutti gli accessi esterni passeranno attraverso il reverse proxy nginx configurato nel Passo 5.
5. Passo 3: Costruisci una pipeline RAG
La Retrieval-Augmented Generation (RAG) permette alla tua IA di rispondere a domande basandosi sui tuoi documenti -- wiki aziendali, contratti legali, documentazione tecnica -- senza inviare alcuno di quei dati a un fornitore cloud. Qui costruiamo una pipeline RAG completa con ChromaDB e LangChain.
Installa le dipendenze
# 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 di ingestione dei documenti
# ~/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!")
API di query RAG
# ~/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
Esegui la pipeline RAG
# 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. Passo 4: Distribuisci Open WebUI
Open WebUI fornisce un'interfaccia curata in stile ChatGPT per interagire con i tuoi modelli locali. Si connette direttamente a Ollama e supporta conversazioni, generazione di immagini e caricamento di documenti -- tutto eseguito in modo privato sul tuo Mac Mini.
Installa Docker
# 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 per 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:
Avvia e configura
# 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
Nota sulla privacy: Open WebUI è configurato con ENABLE_OPENAI_API=false e ENABLE_COMMUNITY_SHARING=false per garantire che nessun dato venga inviato a servizi esterni. L'impostazione ENABLE_SIGNUP=false impedisce agli utenti non autorizzati di creare account. Tutte le conversazioni sono memorizzate localmente nel volume Docker.
7. Passo 5: Gateway API con nginx
nginx funge da unico punto di ingresso per tutti i servizi. Gestisce la terminazione SSL/TLS, la limitazione della frequenza, l'autenticazione e instrada il traffico verso Ollama, Open WebUI e l'API RAG.
Installa e configura nginx
# 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"
Configurazione nginx
# /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;
}
}
}
Configura l'autenticazione e avvia
# 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. Rafforzamento della sicurezza
Un server IA privato è sicuro solo quanto il suo punto di ingresso più debole. Questa sezione copre la configurazione del firewall, il rafforzamento SSH, l'accesso VPN e il rilevamento delle intrusioni per creare una postura di sicurezza a difesa in profondità.
Firewall macOS con 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
Autenticazione SSH solo tramite chiave
# 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 per l'accesso remoto sicuro
# 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
Monitoraggio dei tentativi di accesso (equivalente a fail2ban)
# ~/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
Checklist di sicurezza:
- Autenticazione SSH solo tramite chiave abilitata, password disabilitate
- Tutti i servizi IA associati solo a localhost (127.0.0.1)
- nginx è l'unico servizio esposto pubblicamente (porte 80/443)
- Il firewall pf blocca tutto il traffico in entrata non essenziale
- WireGuard VPN per l'amministrazione remota sicura
- Limitazione della frequenza su tutti gli endpoint API
- Autenticazione di base sugli endpoint di Ollama e dell'API RAG
- Rilevamento automatizzato degli attacchi brute-force e blocco degli IP
- Header di sicurezza (HSTS, CSP, X-Frame-Options) su tutte le risposte
9. Analisi dei costi
L'argomento più convincente a favore di un server IA privato è il costo. Le API IA cloud fatturano per token, e i costi aumentano rapidamente su larga scala. Un Mac Mini M4 offre inferenza illimitata a una tariffa mensile fissa.
API OpenAI vs Mac Mini M4 privato
Il confronto presuppone il prezzo di GPT-3.5-Turbo (0,50 $/1M token in ingresso, 1,50 $/1M token in uscita) vs un Mac Mini M4 dedicato che esegue Llama 3 8B. Richiesta media: 500 token in ingresso + 300 token in uscita.
| Richieste mensili | Costo API OpenAI | Costo Mac Mini M4 | Risparmio |
|---|---|---|---|
| 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) |
Confronto a livello GPT-4
Per una qualità di livello GPT-4, confronta GPT-4-Turbo (10 $/1M in ingresso, 30 $/1M in uscita) vs Mac Mini M4 Pro 48GB che esegue Llama 3 70B.
| Richieste mensili | Costo GPT-4 Turbo | Costo Mac Mini M4 Pro | Risparmio |
|---|---|---|---|
| 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) |
Oltre il costo: Il vero valore di un server IA privato non è solo il risparmio economico. È l'eliminazione della dipendenza dal fornitore, la garanzia della riservatezza dei dati e la libertà di iterare senza preoccuparti della fatturazione delle API. I tuoi costi restano fissi che tu esegua 100 richieste o 10 milioni.
10. Scalabilità
Un singolo Mac Mini M4 può gestire 2-4 richieste simultanee per un modello 7B. Quando hai bisogno di maggiore throughput o vuoi specializzare i modelli, la scalabilità orizzontale con più Mac Mini è semplice.
Architettura multi-nodo
Nodo 1: Chat generale
Mac Mini M4 16GB
Llama 3 8B per conversazioni generiche, supporto clienti e generazione di contenuti.
$75/mo
Nodo 2: Assistente di codice
Mac Mini M4 24GB
CodeLlama 13B per la generazione di codice, la revisione e le attività di refactoring.
$95/mo
Nodo 3: RAG e ragionamento
Mac Mini M4 Pro 48GB
Llama 3 70B per l'analisi documentale complessa, la ricerca legale e le attività di ragionamento approfondito.
$179/mo
Bilanciamento del carico con 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;
}
}
Script di routing dei modelli
# ~/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. Domande frequenti
Un server IA self-hosted è davvero privato se affitto l'hardware?
Sì. Quando affitti un Mac Mini dedicato da My Remote Mac, ottieni l'accesso esclusivo all'hardware fisico. Nessun altro cliente condivide la tua macchina. Tutti i dati sono memorizzati sull'SSD del tuo server, cifrati a riposo. Le chiavi SSH garantiscono che solo tu abbia accesso. Quando termini il tuo abbonamento, il disco viene cancellato in modo sicuro. Questo è fondamentalmente diverso dalle VM cloud condivise in cui altri tenant girano sullo stesso host fisico.
Posso soddisfare la conformità GDPR e HIPAA con questa configurazione?
Un server IA privato risponde ai requisiti tecnici fondamentali del GDPR (i dati restano sotto il tuo controllo, nessuna elaborazione da parte di terzi senza consenso) e dell'HIPAA (i dati sanitari protetti non vengono trasmessi ai fornitori di IA cloud). Tuttavia, la piena conformità richiede anche controlli organizzativi, registrazione di audit, politiche di cifratura e potenzialmente un BAA con il tuo provider di hosting. Usa questa configurazione come base tecnica e lavora con il tuo team di conformità per il quadro completo.
Come si confronta la qualità dei modelli locali con GPT-4 o Claude?
Per attività specifiche e ben definite (Q&A documentale, generazione di codice, riassunto, classificazione), i modelli open source come Llama 3 8B e Mistral 7B raggiungono l'85-95 % della qualità di GPT-3.5. Llama 3 70B si avvicina alla qualità di GPT-4 in molti benchmark. Il divario di qualità si riduce con le pipeline RAG, dove recuperare il contesto giusto conta più della capacità grezza del modello. Per la scrittura creativa generica o il ragionamento complesso in più fasi, i modelli cloud di punta mantengono un vantaggio.
Cosa succede se il server si guasta?
Tutti i servizi sono configurati con KeepAlive (Ollama tramite launchd) e restart: unless-stopped (container Docker). Se il Mac Mini si riavvia, tutti i servizi si riavviano automaticamente. Per i carichi di lavoro in produzione, considera l'esecuzione di due Mac Mini con bilanciamento del carico nginx per l'alta disponibilità. L'infrastruttura My Remote Mac include il monitoraggio 24/7 e alimentazione e connettività di rete ridondanti.
Posso usare questa configurazione con strumenti esistenti come Cursor, Continue.dev o VS Code?
Assolutamente. Poiché Ollama fornisce un'API compatibile con OpenAI, qualsiasi strumento in grado di connettersi a un endpoint OpenAI può usare il tuo server privato. In Cursor o Continue.dev, imposta l'URL di base dell'API su https://your-server/ollama/v1 con le tue credenziali di autenticazione di base. Le estensioni di VS Code come Continue, Cody e Tabby supportano tutte endpoint personalizzati. Il tuo codice non lascia mai il tuo server.
Come aggiorno i modelli quando vengono rilasciate nuove versioni?
L'aggiornamento dei modelli avviene con un singolo comando: ollama pull llama3:8b scaricherà l'ultima versione. La vecchia versione viene mantenuta finché non la rimuovi esplicitamente con ollama rm. Puoi testare le nuove versioni dei modelli insieme a quelle esistenti e passare in produzione senza tempi di inattività aggiornando il nome del modello nelle tue chiamate API o nella configurazione nginx.
E per quanto riguarda le licenze dei modelli? Posso usare Llama 3 a scopo commerciale?
Llama 3 è rilasciato sotto la Meta Llama 3 Community License che consente l'uso commerciale per le organizzazioni con meno di 700 milioni di utenti attivi mensili. I modelli Mistral sono rilasciati sotto la licenza Apache 2.0 (completamente permissiva). CodeLlama segue la Llama 2 Community License. Controlla sempre la licenza specifica di ogni modello che distribuisci, ma per la grande maggioranza delle aziende questi modelli sono liberamente utilizzabili in produzione.
Guide correlate
Esegui LLM su Mac Mini M4
Guida dettagliata per eseguire Llama, Mistral e Phi con Ollama, llama.cpp e MLX. Include benchmark.
Guida al deployment di CoreML
Distribuisci modelli CoreML su server Mac Mini M4 dedicati per un'inferenza di produzione ottimizzata.
Mac Mini M4 vs GPU NVIDIA per l'IA
Confronto dettagliato di prestazioni e costi tra Apple Silicon e NVIDIA per i carichi di lavoro IA.
IA e ML nel cloud - Panoramica
Panoramica dell'infrastruttura cloud Mac Mini per i carichi di lavoro di IA e machine learning.
Costruisci il tuo server IA privato
Ottieni un Mac Mini M4 dedicato ed esegui LLM con totale riservatezza dei dati. Niente API cloud, niente fatturazione per token, niente vincoli con il fornitore. A partire da 85 $/mese.