1. Pourquoi construire un serveur IA privé ?
Envoyer des données sensibles à des API IA tierces comme OpenAI, Anthropic ou Google introduit des risques que de nombreuses organisations ne peuvent pas accepter. Un serveur IA privé garde chaque octet de données sous votre contrôle -- sur du matériel que vous possédez ou louez exclusivement -- sans aucun appel API externe.
Souveraineté des données
Vos prompts, documents et sorties de modèles ne quittent jamais votre infrastructure. Aucune journalisation par des tiers, aucun entraînement sur vos données, aucun risque de fuite de données via des endpoints API que vous ne contrôlez pas.
Conformité réglementaire
Respectez les exigences de conformité RGPD, HIPAA, SOC 2 et sectorielles en gardant le traitement IA au sein de votre périmètre de données. Aucun souci de transfert de données transfrontalier.
Prévisibilité des coûts
Coût mensuel fixe quel que soit l'usage. Pas de surprises de facturation par token, pas de changements soudains de limites de débit, pas d'augmentations de prix des fournisseurs d'API. Exécutez une inférence illimitée 24h/24 à un tarif forfaitaire.
Aucune limite de débit
Traitez autant de requêtes que votre matériel peut gérer. Pas de plafonds de tokens par minute, pas de mise en file d'attente côté fournisseur, pas de performances dégradées aux heures de pointe.
Le risque des API cloud : Lorsque vous envoyez des données à un fournisseur d'IA cloud, vous en perdez le contrôle. Même avec des garanties contractuelles, vos données traversent des réseaux que vous ne gérez pas, résident sur des serveurs que vous ne contrôlez pas et sont soumises à la posture de sécurité du fournisseur. Pour les industries réglementées -- santé, finance, juridique, défense -- c'est souvent rédhibitoire.
Pourquoi le Mac Mini M4 ? L'architecture mémoire unifiée d'Apple Silicon vous permet d'exécuter des modèles de 7 à 70 milliards de paramètres sur un seul appareil qui consomme moins de 15 W. La bande passante mémoire du M4 (jusqu'à 120 Go/s) alimente efficacement les poids du modèle vers le GPU, offrant 30 à 40 tokens/seconde pour les modèles 7B -- assez rapide pour le chat en temps réel. À partir de 85 $/mois pour du matériel dédié, c'est le moyen le plus rentable de construire un serveur IA privé.
2. Vue d'ensemble de l'architecture
La pile du serveur IA privé se compose de cinq couches, toutes exécutées localement sur votre Mac Mini M4. Aucun service externe n'est requis.
Architecture du système
# 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. Étape 1 : Configuration du serveur
Commencez par provisionner votre Mac Mini M4 et configurer l'accès distant sécurisé. Si vous utilisez My Remote Mac, l'accès SSH est fourni dès la mise en service.
Configuration 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
Mises à jour système et préparation
# 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
Créer un utilisateur de service dédié (optionnel)
# 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. Étape 2 : Installer Ollama et les modèles
Ollama est le socle de votre serveur IA privé. Il gère le téléchargement des modèles, la quantification et fournit une API compatible OpenAI -- le tout en local sans aucun appel externe.
Installer 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 &
Télécharger des modèles pour différents cas d'usage
# 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?"
Configurer Ollama comme service persistant
# 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"
Important : Notez que OLLAMA_HOST est défini sur 127.0.0.1:11434 (localhost uniquement). Cela garantit qu'Ollama n'est pas directement accessible depuis le réseau. Tout accès externe passera par le reverse proxy nginx configuré à l'étape 5.
5. Étape 3 : Construire un pipeline RAG
La génération augmentée par récupération (RAG) permet à votre IA de répondre aux questions en se basant sur vos propres documents -- wikis d'entreprise, contrats juridiques, documentation technique -- sans envoyer aucune de ces données à un fournisseur cloud. Nous construisons ici un pipeline RAG complet avec ChromaDB et LangChain.
Installer les dépendances
# 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 d'ingestion de documents
# ~/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 de requête 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
Exécuter le 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. Étape 4 : Déployer Open WebUI
Open WebUI fournit une interface soignée de type ChatGPT pour interagir avec vos modèles locaux. Il se connecte directement à Ollama et prend en charge les conversations, la génération d'images et le téléchargement de documents -- le tout fonctionnant de manière privée sur votre Mac Mini.
Installer 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 pour 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:
Lancer et configurer
# 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
Note sur la confidentialité : Open WebUI est configuré avec ENABLE_OPENAI_API=false et ENABLE_COMMUNITY_SHARING=false pour garantir qu'aucune donnée n'est envoyée à des services externes. Le paramètre ENABLE_SIGNUP=false empêche les utilisateurs non autorisés de créer des comptes. Toutes les conversations sont stockées localement dans le volume Docker.
7. Étape 5 : Passerelle API avec nginx
nginx sert de point d'entrée unique pour tous les services. Il gère la terminaison SSL/TLS, la limitation de débit, l'authentification et le routage du trafic vers Ollama, Open WebUI et l'API RAG.
Installer et configurer 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"
Configuration 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;
}
}
}
Configurer l'authentification et démarrer
# 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. Renforcement de la sécurité
Un serveur IA privé n'est aussi sécurisé que son point d'entrée le plus faible. Cette section couvre la configuration du pare-feu, le renforcement SSH, l'accès VPN et la détection d'intrusion pour créer une posture de sécurité en profondeur.
Pare-feu macOS avec 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
Authentification SSH par clé uniquement
# 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 pour l'accès distant sécurisé
# 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
Surveillance des tentatives de connexion (équivalent 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 de sécurité :
- Authentification SSH par clé activée, mots de passe désactivés
- Tous les services IA liés à localhost uniquement (127.0.0.1)
- nginx est le seul service exposé publiquement (ports 80/443)
- Le pare-feu pf bloque tout le trafic entrant non essentiel
- WireGuard VPN pour l'administration distante sécurisée
- Limitation de débit sur tous les endpoints API
- Authentification basique sur les endpoints Ollama et API RAG
- Détection automatisée des attaques par force brute et blocage d'IP
- En-têtes de sécurité (HSTS, CSP, X-Frame-Options) sur toutes les réponses
9. Analyse des coûts
L'argument le plus convaincant en faveur d'un serveur IA privé est le coût. Les API IA cloud facturent par token, et les coûts augmentent rapidement à grande échelle. Un Mac Mini M4 offre une inférence illimitée à un tarif mensuel fixe.
API OpenAI vs Mac Mini M4 privé
La comparaison suppose la tarification GPT-3.5-Turbo (0,50 $/1M tokens en entrée, 1,50 $/1M tokens en sortie) vs un Mac Mini M4 dédié exécutant Llama 3 8B. Requête moyenne : 500 tokens en entrée + 300 tokens en sortie.
| Requêtes mensuelles | Coût API OpenAI | Coût Mac Mini M4 | Économies |
|---|---|---|---|
| 1,000 | $0.70 | $85 | -$74.30 (API cheaper) |
| 10,000 | $7.00 | $85 | -$68.00 (API cheaper) |
| 100,000 | $70.00 | $85 | ~Break-even |
| 500,000 | $350.00 | $85 | $275 (79% savings) |
| 1,000,000 | $700.00 | $85 | $625 (89% savings) |
Comparaison niveau GPT-4
Pour une qualité de niveau GPT-4, comparez GPT-4-Turbo (10 $/1M en entrée, 30 $/1M en sortie) vs Mac Mini M4 Pro 48 Go exécutant Llama 3 70B.
| Requêtes mensuelles | Coût GPT-4 Turbo | Coût Mac Mini M4 Pro | Économies |
|---|---|---|---|
| 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) |
Au-delà du coût : La vraie valeur d'un serveur IA privé n'est pas seulement les économies financières. C'est l'élimination de la dépendance fournisseur, la garantie de confidentialité des données et la liberté d'itérer sans se soucier de la facturation API. Vos coûts restent fixes que vous exécutiez 100 requêtes ou 10 millions.
10. Montée en charge
Un seul Mac Mini M4 peut gérer 2 à 4 requêtes simultanées pour un modèle 7B. Lorsque vous avez besoin de plus de débit ou souhaitez spécialiser les modèles, la montée en charge horizontale avec plusieurs Mac Minis est simple.
Architecture multi-nœuds
Nœud 1 : Chat général
Mac Mini M4 16 Go
Llama 3 8B pour les conversations générales, le support client et la génération de contenu.
$85/mo
Nœud 2 : Assistant de code
Mac Mini M4 24 Go
CodeLlama 13B pour la génération de code, la revue et les tâches de refactoring.
$95/mo
Nœud 3 : RAG et raisonnement
Mac Mini M4 Pro 48 Go
Llama 3 70B pour l'analyse documentaire complexe, la recherche juridique et les tâches de raisonnement approfondi.
$179/mo
Répartition de charge avec 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 de routage de modèles
# ~/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. Questions fréquentes
Un serveur IA auto-hébergé est-il vraiment privé si je loue le matériel ?
Oui. Lorsque vous louez un Mac Mini dédié chez My Remote Mac, vous obtenez un accès exclusif au matériel physique. Aucun autre client ne partage votre machine. Toutes les données sont stockées sur le SSD de votre serveur, chiffrées au repos. Les clés SSH garantissent que seul vous avez accès. Lorsque vous résiliez votre abonnement, le disque est effacé de manière sécurisée. C'est fondamentalement différent des VM cloud partagées où d'autres locataires fonctionnent sur le même hôte physique.
Puis-je respecter la conformité RGPD et HIPAA avec cette configuration ?
Un serveur IA privé répond aux exigences techniques fondamentales du RGPD (les données restent sous votre contrôle, pas de traitement par des tiers sans consentement) et de la HIPAA (les informations de santé protégées ne sont pas transmises aux fournisseurs d'IA cloud). Cependant, la conformité complète nécessite également des contrôles organisationnels, une journalisation d'audit, des politiques de chiffrement et potentiellement un BAA avec votre hébergeur. Utilisez cette configuration comme fondation technique et travaillez avec votre équipe de conformité pour le tableau complet.
Comment la qualité des modèles locaux se compare-t-elle à GPT-4 ou Claude ?
Pour des tâches spécifiques et bien définies (Q&A documentaire, génération de code, résumé, classification), les modèles open source comme Llama 3 8B et Mistral 7B atteignent 85 à 95 % de la qualité de GPT-3.5. Llama 3 70B approche la qualité de GPT-4 sur de nombreux benchmarks. L'écart de qualité se réduit avec les pipelines RAG, où récupérer le bon contexte compte plus que la capacité brute du modèle. Pour l'écriture créative générale ou le raisonnement complexe en plusieurs étapes, les modèles cloud de pointe conservent un avantage.
Que se passe-t-il si le serveur tombe en panne ?
Tous les services sont configurés avec KeepAlive (Ollama via launchd) et restart: unless-stopped (conteneurs Docker). Si le Mac Mini redémarre, tous les services redémarrent automatiquement. Pour les charges de travail de production, envisagez d'utiliser deux Mac Minis avec une répartition de charge nginx pour la haute disponibilité. L'infrastructure My Remote Mac inclut une surveillance 24h/24, une alimentation et une connectivité réseau redondantes.
Puis-je utiliser cette configuration avec des outils existants comme Cursor, Continue.dev ou VS Code ?
Absolument. Comme Ollama fournit une API compatible OpenAI, tout outil pouvant se connecter à un endpoint OpenAI peut utiliser votre serveur privé. Dans Cursor ou Continue.dev, pointez l'URL de base de l'API vers https://votre-serveur/ollama/v1 avec vos identifiants d'authentification basique. Les extensions VS Code comme Continue, Cody et Tabby prennent toutes en charge les endpoints personnalisés. Votre code ne quitte jamais votre serveur.
Comment mettre à jour les modèles lorsque de nouvelles versions sont publiées ?
La mise à jour des modèles se fait en une seule commande : ollama pull llama3:8b téléchargera la dernière version. L'ancienne version est conservée jusqu'à ce que vous la supprimiez explicitement avec ollama rm. Vous pouvez tester les nouvelles versions de modèles en parallèle des existantes et basculer en production sans temps d'arrêt en mettant à jour le nom du modèle dans vos appels API ou la configuration nginx.
Qu'en est-il des licences des modèles ? Puis-je utiliser Llama 3 à des fins commerciales ?
Llama 3 est publié sous la licence Meta Llama 3 Community License qui autorise l'utilisation commerciale pour les organisations comptant moins de 700 millions d'utilisateurs actifs mensuels. Les modèles Mistral sont publiés sous la licence Apache 2.0 (entièrement permissive). CodeLlama suit la licence Llama 2 Community License. Vérifiez toujours la licence spécifique de chaque modèle que vous déployez, mais pour la grande majorité des entreprises, ces modèles sont librement utilisables en production.
Guides associés
Exécuter des LLM sur Mac Mini M4
Guide détaillé pour exécuter Llama, Mistral et Phi avec Ollama, llama.cpp et MLX. Inclut des benchmarks.
Guide de déploiement CoreML
Déployez des modèles CoreML sur des serveurs Mac Mini M4 dédiés pour l'inférence de production optimisée.
Mac Mini M4 vs GPU NVIDIA pour l'IA
Comparaison détaillée des performances et des coûts entre Apple Silicon et NVIDIA pour les charges de travail IA.
Cloud IA et ML - Vue d'ensemble
Vue d'ensemble de l'infrastructure cloud Mac Mini pour les charges de travail IA et machine learning.
Construisez votre serveur IA privé
Obtenez un Mac Mini M4 dédié et exécutez des LLM avec une confidentialité totale des données. Pas d'API cloud, pas de facturation par token, pas de dépendance fournisseur. À partir de 85 $/mois.