1. 驴Por Qu茅 Construir un Servidor Privado de AI?
Enviar datos sensibles a APIs de AI de terceros como OpenAI, Anthropic o Google introduce riesgos que muchas organizaciones no pueden aceptar. Un servidor AI privado mantiene cada byte de datos bajo tu control -- en hardware que posees o alquilas exclusivamente -- con cero llamadas a APIs externas.
Soberan铆a de Datos
Tus prompts, documentos y resultados del modelo nunca salen de tu infraestructura. Sin registro de terceros, sin entrenamiento con tus datos, sin riesgo de fugas de datos a trav茅s de endpoints API que no controlas.
Cumplimiento Normativo
Cumple con los requisitos de GDPR, HIPAA, SOC 2 y regulaciones espec铆ficas del sector manteniendo el procesamiento AI dentro de tu per铆metro de datos. Sin preocupaciones por transferencia de datos transfronteriza.
Previsibilidad de Costes
Coste mensual fijo independientemente del uso. Sin sorpresas en la facturaci贸n por token, sin cambios repentinos en los l铆mites de velocidad, sin aumentos de precios de los proveedores de API. Ejecuta inferencia ilimitada 24/7 a tarifa plana.
Sin L铆mites de Velocidad
Procesa tantas solicitudes como tu hardware pueda manejar. Sin l铆mites de tokens por minuto, sin cola de solicitudes del lado del proveedor, sin rendimiento degradado durante las horas pico.
El Riesgo de las APIs en la Nube: Cuando env铆as datos a un proveedor de AI en la nube, pierdes el control. Incluso con garant铆as contractuales, tus datos atraviesan redes que no gestionas, residen en servidores que no controlas y est谩n sujetos a la postura de seguridad del proveedor. Para industrias reguladas -- sanidad, finanzas, legal, defensa -- esto suele ser un impedimento.
驴Por Qu茅 Mac Mini M4? La arquitectura de memoria unificada de Apple Silicon te permite ejecutar modelos de 7B-70B par谩metros en un solo dispositivo que consume menos de 15W de potencia. El ancho de banda de memoria del M4 (hasta 120 GB/s) alimenta los pesos del modelo al GPU de forma eficiente, entregando 30-40 tokens/segundo para modelos de 7B -- suficientemente r谩pido para chat en tiempo real. A partir de $75/mes para hardware dedicado, es la forma m谩s rentable de construir un servidor AI privado.
2. Visi贸n General de la Arquitectura
El stack del servidor AI privado consta de cinco capas, todas ejecut谩ndose localmente en tu Mac Mini M4. No se requieren servicios externos.
Arquitectura 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. Paso 1: Configuraci贸n del Servidor
Comienza aprovisionando tu Mac Mini M4 y configurando el acceso remoto seguro. Si usas My Remote Mac, el acceso SSH se proporciona de serie.
Configuraci贸n 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
Actualizaciones del Sistema y Preparaci贸n
# 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
Crear un Usuario de Servicio Dedicado (Opcional)
# 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. Paso 2: Instalar Ollama y Modelos
Ollama es la columna vertebral de tu servidor AI privado. Gestiona la descarga de modelos, la cuantizaci贸n y proporciona una API compatible con OpenAI -- todo ejecut谩ndose localmente con cero llamadas externas.
Instalar 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 &
Descargar Modelos para Diferentes Casos de 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?"
Configurar Ollama como Servicio 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: Observa que OLLAMA_HOST est谩 configurado a 127.0.0.1:11434 (solo localhost). Esto asegura que Ollama no sea accesible directamente desde la red. Todo el acceso externo pasar谩 por el proxy inverso nginx configurado en el Paso 5.
5. Paso 3: Construir un Pipeline RAG
La Generaci贸n Aumentada por Recuperaci贸n (RAG) permite que tu AI responda preguntas bas谩ndose en tus propios documentos -- wikis de empresa, contratos legales, documentaci贸n t茅cnica -- sin enviar ninguno de esos datos a un proveedor en la nube. Aqu铆 construimos un pipeline RAG completo con ChromaDB y LangChain.
Instalar Dependencias
# 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 de Ingesta de Documentos
# ~/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 Consulta 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
Ejecutar el 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. Paso 4: Desplegar Open WebUI
Open WebUI proporciona una interfaz tipo ChatGPT pulida para interactuar con tus modelos locales. Se conecta directamente a Ollama y soporta conversaciones, generaci贸n de im谩genes y carga de documentos -- todo ejecut谩ndose de forma privada en tu Mac Mini.
Instalar 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 para 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:
Lanzar y Configurar
# 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 de Privacidad: Open WebUI est谩 configurado con ENABLE_OPENAI_API=false y ENABLE_COMMUNITY_SHARING=false para asegurar que no se env铆en datos a servicios externos. La configuraci贸n ENABLE_SIGNUP=false impide que usuarios no autorizados creen cuentas. Todas las conversaciones se almacenan localmente en el volumen Docker.
7. Paso 5: API Gateway con nginx
nginx sirve como punto de entrada 煤nico para todos los servicios. Maneja la terminaci贸n SSL/TLS, limitaci贸n de velocidad, autenticaci贸n y enruta el tr谩fico a Ollama, Open WebUI y la API RAG.
Instalar y Configurar 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"
Configuraci贸n de 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;
}
}
}
Configurar Autenticaci贸n e Iniciar
# 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. Refuerzo de Seguridad
Un servidor AI privado es tan seguro como su punto de entrada m谩s d茅bil. Esta secci贸n cubre configuraci贸n de firewall, refuerzo de SSH, acceso VPN y detecci贸n de intrusiones para crear una postura de seguridad de defensa en profundidad.
Firewall de 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
Autenticaci贸n SSH Solo por Clave
# 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 para Acceso Remoto Seguro
# 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
Monitorizaci贸n de Intentos de Inicio de Sesi贸n (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
Lista de Verificaci贸n de Seguridad:
- Autenticaci贸n SSH solo por clave habilitada, contrase帽as desactivadas
- Todos los servicios AI vinculados solo a localhost (127.0.0.1)
- nginx es el 煤nico servicio expuesto al p煤blico (puertos 80/443)
- El firewall pf bloquea todo el tr谩fico entrante no esencial
- WireGuard VPN para administraci贸n remota segura
- Limitaci贸n de velocidad en todos los endpoints API
- Autenticaci贸n b谩sica en los endpoints de Ollama y API RAG
- Detecci贸n automatizada de fuerza bruta y bloqueo de IP
- Cabeceras de seguridad (HSTS, CSP, X-Frame-Options) en todas las respuestas
9. An谩lisis de Costes
El argumento m谩s convincente para un servidor AI privado es el coste. Las APIs de AI en la nube cobran por token, y los costes escalan r谩pidamente a gran escala. Un Mac Mini M4 proporciona inferencia ilimitada a una tarifa mensual fija.
API de OpenAI vs. Mac Mini M4 Privado
La comparaci贸n asume precios de GPT-3.5-Turbo ($0,50/1M tokens de entrada, $1,50/1M tokens de salida) vs. un Mac Mini M4 dedicado ejecutando Llama 3 8B. Solicitud promedio: 500 tokens de entrada + 300 tokens de salida.
| Solicitudes Mensuales | Coste API OpenAI | Coste Mac Mini M4 | Ahorro |
|---|---|---|---|
| 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) |
Comparaci贸n Nivel GPT-4
Para calidad de nivel GPT-4, compara GPT-4-Turbo ($10/1M entrada, $30/1M salida) vs. Mac Mini M4 Pro 48GB ejecutando Llama 3 70B.
| Solicitudes Mensuales | Coste GPT-4 Turbo | Coste Mac Mini M4 Pro | Ahorro |
|---|---|---|---|
| 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) |
M谩s All谩 del Coste: El verdadero valor de un servidor AI privado no son solo los ahorros financieros. Es la eliminaci贸n de la dependencia de proveedores, la garant铆a de privacidad de datos y la libertad de iterar sin preocuparse por la facturaci贸n de APIs. Tus costes se mantienen fijos ya sea que ejecutes 100 solicitudes o 10 millones.
10. Escalado
Un solo Mac Mini M4 puede manejar 2-4 solicitudes concurrentes para un modelo de 7B. Cuando necesitas m谩s rendimiento o quieres especializaci贸n de modelos, escalar horizontalmente con m煤ltiples Mac Minis es sencillo.
Arquitectura Multi-Nodo
Nodo 1: Chat General
Mac Mini M4 16GB
Llama 3 8B para conversaciones de prop贸sito general, soporte al cliente y generaci贸n de contenido.
$75/mo
Nodo 2: Asistente de C贸digo
Mac Mini M4 24GB
CodeLlama 13B para generaci贸n de c贸digo, revisi贸n y tareas de refactorizaci贸n.
$95/mo
Nodo 3: RAG y Razonamiento
Mac Mini M4 Pro 48GB
Llama 3 70B para an谩lisis complejo de documentos, investigaci贸n legal y tareas de razonamiento profundo.
$179/mo
Balanceo de Carga 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 de Enrutamiento de Modelos
# ~/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. Preguntas Frecuentes
驴Un servidor AI autoalojado es verdaderamente privado si alquilo el hardware?
S铆. Cuando alquilas un Mac Mini dedicado de My Remote Mac, obtienes acceso exclusivo al hardware f铆sico. Ning煤n otro cliente comparte tu m谩quina. Todos los datos se almacenan en el SSD de tu servidor, cifrados en reposo. Las claves SSH aseguran que solo t煤 tienes acceso. Cuando finalizas tu suscripci贸n, el disco se borra de forma segura. Esto es fundamentalmente diferente de VMs compartidas en la nube donde otros inquilinos se ejecutan en el mismo host f铆sico.
驴Puedo cumplir con GDPR y HIPAA con esta configuraci贸n?
Un servidor AI privado aborda los requisitos t茅cnicos fundamentales de GDPR (los datos permanecen bajo tu control, sin procesamiento de terceros sin consentimiento) y HIPAA (la informaci贸n de salud protegida no se transmite a proveedores de AI en la nube). Sin embargo, el cumplimiento completo tambi茅n requiere controles organizacionales, registro de auditor铆a, pol铆ticas de cifrado y potencialmente un BAA con tu proveedor de hosting. Usa esta configuraci贸n como la base t茅cnica y trabaja con tu equipo de cumplimiento para el panorama completo.
驴C贸mo se compara la calidad de los modelos locales con GPT-4 o Claude?
Para tareas espec铆ficas y bien definidas (Q&A de documentos, generaci贸n de c贸digo, resumen, clasificaci贸n), los modelos de c贸digo abierto como Llama 3 8B y Mistral 7B alcanzan el 85-95% de la calidad de GPT-3.5. Llama 3 70B se acerca a la calidad de GPT-4 en muchos benchmarks. La brecha de calidad se reduce con pipelines RAG, donde recuperar el contexto correcto importa m谩s que la capacidad bruta del modelo. Para escritura creativa general o razonamiento complejo de m煤ltiples pasos, los modelos frontera en la nube todav铆a tienen ventaja.
驴Qu茅 pasa si el servidor se cae?
Todos los servicios est谩n configurados con KeepAlive (Ollama v铆a launchd) y restart: unless-stopped (contenedores Docker). Si el Mac Mini se reinicia, todos los servicios se reinician autom谩ticamente. Para cargas de trabajo de producci贸n, considera ejecutar dos Mac Minis con balanceo de carga nginx para alta disponibilidad. La infraestructura de My Remote Mac incluye monitorizaci贸n 24/7 y conectividad de red y energ铆a redundante.
驴Puedo usar esta configuraci贸n con herramientas existentes como Cursor, Continue.dev o VS Code?
Absolutamente. Dado que Ollama proporciona una API compatible con OpenAI, cualquier herramienta que pueda conectarse a un endpoint de OpenAI puede usar tu servidor privado. En Cursor o Continue.dev, apunta la URL base de la API a https://tu-servidor/ollama/v1 con tus credenciales de autenticaci贸n b谩sica. Las extensiones de VS Code como Continue, Cody y Tabby soportan endpoints personalizados. Tu c贸digo nunca sale de tu servidor.
驴C贸mo actualizo los modelos cuando se lanzan nuevas versiones?
Actualizar modelos es un solo comando: ollama pull llama3:8b descargar谩 la 煤ltima versi贸n. La versi贸n anterior se mantiene hasta que la elimines expl铆citamente con ollama rm. Puedes probar nuevas versiones de modelos junto a las existentes y cambiar en producci贸n con cero tiempo de inactividad actualizando el nombre del modelo en tus llamadas API o la configuraci贸n de nginx.
驴Qu茅 pasa con las licencias de los modelos? 驴Puedo usar Llama 3 comercialmente?
Llama 3 se lanza bajo la Meta Llama 3 Community License que permite uso comercial para organizaciones con menos de 700 millones de usuarios activos mensuales. Los modelos Mistral se lanzan bajo la licencia Apache 2.0 (totalmente permisiva). CodeLlama sigue la Llama 2 Community License. Siempre verifica la licencia espec铆fica de cada modelo que despliegues, pero para la gran mayor铆a de las empresas, estos modelos son de libre uso en producci贸n.
Gu铆as Relacionadas
Ejecutar LLMs en Mac Mini M4
Gu铆a detallada para ejecutar Llama, Mistral y Phi con Ollama, llama.cpp y MLX. Incluye benchmarks.
Gu铆a de Despliegue CoreML
Despliega modelos CoreML en servidores dedicados Mac Mini M4 para inferencia optimizada en producci贸n.
Mac Mini M4 vs GPU NVIDIA para AI
Comparaci贸n detallada de rendimiento y coste entre Apple Silicon y NVIDIA para cargas de trabajo AI.
Visi贸n General de AI y ML en la Nube
Visi贸n general de la infraestructura cloud Mac Mini para cargas de trabajo AI y machine learning.
Construye Tu Servidor Privado de AI
Obt茅n un Mac Mini M4 dedicado y ejecuta LLMs con total privacidad de datos. Sin APIs en la nube, sin facturaci贸n por token, sin dependencia de proveedores. Desde $75/mes con 7 d铆as de prueba gratuita.