Módulo 7: Alternative Platforms (Render, Railway, Fly.io)
2. Render: Deployment para AI Apps
Descripción
En esta cápsula vas a desplegar tu app AI en Render paso a paso. Render se posiciona como "el Heroku moderno" — una plataforma que elimina la complejidad de infraestructura y te deja enfocarte en tu código. Conectas un repositorio de GitHub, Render detecta tu Dockerfile (o tu framework), y en minutos tienes una URL pública con HTTPS, auto-deploy en cada push, y logs accesibles desde el dashboard.
Contexto: Vienes de Módulos 3-6 donde todo era AWS: IAM roles, API Gateway, CloudWatch, S3 policies. Render es el contraste directo. No hay roles, no hay policies, no hay VPCs. Hay un formulario web y un botón de "Deploy." La pregunta no es si es más fácil (lo es), sino si esa simplicidad sacrifica algo que tu app AI necesita. En esta cápsula lo descubrirás.
Render: Overview de la Plataforma
Qué es Render
Render es una plataforma cloud que despliega aplicaciones web directamente desde Git. Soporta:
- Web Services: Apps con servidor (FastAPI, Flask, Express, Go, etc.)
- Static Sites: Frontend estático (React, Vue, HTML)
- Background Workers: Procesos sin HTTP (colas, scrapers, pipelines)
- Cron Jobs: Tareas programadas
- PostgreSQL: Base de datos managed
- Redis: Cache managed
Para AI Engineers, lo relevante es: Web Services (tu API de inferencia), PostgreSQL (metadata, logs), y Redis (cache de respuestas).
Modelo de deployment
Tu código (GitHub/GitLab)
↓ push a main
Render detecta cambio
↓
Build automático
├── Detecta Dockerfile → docker build
├── Detecta requirements.txt → buildpack Python
└── Detecta package.json → buildpack Node
↓
Deploy en container
↓
URL pública: https://tu-app.onrender.com
Pricing (datos actualizados 2026)
| Plan | Precio | RAM | CPU | Incluye |
|---|---|---|---|---|
| Free | $0/mes | 512 MB | 0.1 vCPU | Sleep después de 15 min inactividad |
| Starter | $7/mes | 512 MB | 0.5 vCPU | No sleep, custom domains |
| Standard | $25/mes | 2 GB | 1 vCPU | Auto-scaling horizontal |
| Pro | $85/mes | 4 GB | 2 vCPU | Más recursos, health checks avanzados |
| Pro Plus | $175/mes | 8 GB | 4 vCPU | Para workloads intensivos |
Para AI workloads:
- Free tier: Solo para demos. 512 MB no carga un modelo embeddings + FastAPI.
- Starter: Viable para apps que usan APIs externas (OpenAI, Anthropic) sin modelos locales.
- Standard/Pro: Necesarios si cargas ChromaDB en memoria o modelos locales.
Limitaciones para AI
| Limitación | Impacto en AI | Workaround |
|---|---|---|
| Free tier sleep (15 min) | Cold start de ~30s después de inactividad | Usar Starter ($7/mes) o cron job de ping |
| 512 MB RAM (Free/Starter) | No cabe ChromaDB + FastAPI + dependencias | Usar Standard ($25/mes) o API externa de vectores |
| Request timeout: 30s | Streaming largo o inferencia compleja puede fallar | Optimizar prompts, usar streaming para respuestas largas |
| No WebSocket nativo (Free) | Streaming de tokens limitado | Usar SSE (Server-Sent Events) en vez de WebSocket |
| No GPU | No puedes correr modelos locales grandes | Usar APIs de inferencia (OpenAI, Anthropic, Together AI) |
| Disco efímero | Archivos escritos se pierden en cada deploy | Usar S3 o Render Disk ($0.25/GB/mes) para persistencia |
Deploy Step-by-Step: App AI en Render
Paso 1: Preparar el repositorio
Tu repositorio necesita un Dockerfile funcional. Si seguiste la cápsula 01, ya lo tienes. Verifica que funciona localmente:
cd deployment-cloud-guide/module-07/app
# Build local
docker build -t docusearch-ai .
# Test local
docker run -p 8000:8000 -e OPENAI_API_KEY=sk-test docusearch-ai
# Verificar health
curl http://localhost:8000/health
# {"status":"healthy","version":"1.0.0","platform":"local"}
Paso 2: Configurar Render desde el Dashboard
1. Ve a https://dashboard.render.com
2. Click "New" → "Web Service"
3. Conecta tu repositorio de GitHub
4. Selecciona el repositorio con tu app AI
5. Configuración:
- Name: docusearch-ai
- Region: Oregon (US West) o Frankfurt (EU)
- Branch: main
- Runtime: Docker
- Plan: Starter ($7/mes) o Free (para pruebas)
6. Environment Variables:
- OPENAI_API_KEY = tu-api-key
- PLATFORM = render
7. Click "Create Web Service"
Paso 3: Render YAML (Infrastructure as Code)
En lugar del dashboard, puedes definir tu servicio en un archivo render.yaml en la raíz del repositorio:
# render.yaml
services:
- type: web
name: docusearch-ai
runtime: docker
dockerfilePath: ./Dockerfile
dockerContext: .
region: oregon
plan: starter
healthCheckPath: /health
envVars:
- key: OPENAI_API_KEY
sync: false
- key: PLATFORM
value: render
- key: LOG_LEVEL
value: info
autoDeploy: true
buildFilter:
paths:
- app/**
- Dockerfile
- requirements.txt
# Commit y push
git add render.yaml
git commit -m "Add Render configuration"
git push origin main
Paso 4: Monitorear el deployment
Dashboard de Render → tu servicio → Events
Verás:
1. "Build started" — Render clona tu repo y ejecuta docker build
2. "Build succeeded" — La imagen se construyó correctamente
3. "Deploy started" — Render lanza el container
4. "Deploy live" — Tu app está online
Tiempo típico: 2-5 minutos para el primer deploy
Paso 5: Verificar que funciona
# Tu URL será algo como:
RENDER_URL="https://docusearch-ai.onrender.com"
# Health check
curl $RENDER_URL/health
# {"status":"healthy","version":"1.0.0","platform":"render"}
# Test de inferencia
curl -X POST $RENDER_URL/ask \
-H "Content-Type: application/json" \
-d '{"question": "¿Qué es FastAPI?", "max_tokens": 200}'
Paso 6: Configurar custom domain (opcional)
Dashboard → tu servicio → Settings → Custom Domains
1. Add Custom Domain: api.tu-dominio.com
2. Render te da un CNAME record
3. Configura el DNS en tu proveedor:
- Type: CNAME
- Name: api
- Value: docusearch-ai.onrender.com
4. Render genera certificado SSL automáticamente
Render: Databases y Add-ons
PostgreSQL en Render
Dashboard → New → PostgreSQL
Pricing:
- Free: 256 MB storage, 97 días de retención, luego se borra
- Starter: $7/mes, 1 GB
- Standard: $20/mes, 10 GB, backups diarios
Conexión desde tu app:
import os
import psycopg2
DATABASE_URL = os.environ.get("DATABASE_URL")
conn = psycopg2.connect(DATABASE_URL)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS inference_logs (
id SERIAL PRIMARY KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
model VARCHAR(50),
tokens_used INTEGER,
latency_ms INTEGER,
created_at TIMESTAMP DEFAULT NOW()
)
""")
conn.commit()
Redis en Render
Dashboard → New → Redis
Pricing:
- Free: 25 MB, 97 días de retención
- Starter: $7/mes, 100 MB
- Standard: $20/mes, 1 GB
Uso para cache de respuestas AI:
import os
import json
import hashlib
import redis
REDIS_URL = os.environ.get("REDIS_URL")
cache = redis.from_url(REDIS_URL)
CACHE_TTL = 3600 # 1 hora
def get_cached_answer(question: str) -> dict | None:
key = f"answer:{hashlib.sha256(question.encode()).hexdigest()[:16]}"
cached = cache.get(key)
if cached:
return json.loads(cached)
return None
def cache_answer(question: str, answer: dict) -> None:
key = f"answer:{hashlib.sha256(question.encode()).hexdigest()[:16]}"
cache.setex(key, CACHE_TTL, json.dumps(answer))
Render Disk (persistencia)
Si tu app necesita escribir archivos persistentes (logs, uploads, modelos descargados):
# render.yaml con disco persistente
services:
- type: web
name: docusearch-ai
runtime: docker
plan: starter
disk:
name: ai-data
mountPath: /data
sizeGB: 1
Patterns de Deployment en Render para AI
Pattern 1: API de inferencia stateless
El patrón más común. Tu app recibe requests, llama a un LLM externo, retorna respuesta.
@app.post("/ask")
async def ask(query: Query):
cached = get_cached_answer(query.question)
if cached:
return Answer(**cached, source="cache")
response = await call_openai(query.question, query.max_tokens)
cache_answer(query.question, response)
return Answer(**response, source="llm")
- ✅ Funciona en Free/Starter tier
- ✅ Auto-deploy en push
- ❌ Cold start en Free tier
Pattern 2: App con base de datos para RAG metadata
Tu app usa PostgreSQL para guardar metadata de documentos y logs de inferencia.
@app.post("/index")
async def index_document(doc: Document):
embedding = await generate_embedding(doc.content)
save_to_postgres(doc, embedding)
return {"status": "indexed", "doc_id": doc.id}
@app.post("/search")
async def search(query: SearchQuery):
query_embedding = await generate_embedding(query.text)
results = search_postgres(query_embedding, limit=5)
answer = await generate_answer(query.text, results)
log_inference(query, answer)
return answer
- ✅ PostgreSQL managed por Render
- ✅ Backups automáticos en plan Standard+
- ❌ pgvector extension requiere plan Standard+
Pattern 3: Background worker para procesamiento
Procesamiento asíncrono de documentos, generación de embeddings, etc.
# render.yaml con worker
services:
- type: web
name: docusearch-api
runtime: docker
plan: starter
- type: worker
name: docusearch-worker
runtime: docker
dockerCommand: python worker.py
plan: starter
envVars:
- key: REDIS_URL
fromService:
name: docusearch-redis
type: redis
property: connectionString
Environment Variables y Secrets
Configuración desde el dashboard
Dashboard → tu servicio → Environment
Variables:
- OPENAI_API_KEY = sk-proj-xxx (secret, no visible después de guardar)
- PLATFORM = render
- LOG_LEVEL = info
- DATABASE_URL = (generado automáticamente si usas Render PostgreSQL)
- REDIS_URL = (generado automáticamente si usas Render Redis)
Grupos de environment variables
Render permite crear grupos reutilizables:
Dashboard → Env Groups → New Env Group
Nombre: ai-api-keys
Variables:
- OPENAI_API_KEY = sk-proj-xxx
- ANTHROPIC_API_KEY = sk-ant-xxx
Luego vinculas el grupo a múltiples servicios.
render.yaml con variables
services:
- type: web
name: docusearch-ai
envVars:
- key: OPENAI_API_KEY
sync: false # No se sincroniza desde YAML, se configura en dashboard
- key: PLATFORM
value: render # Valor fijo en YAML
- key: DATABASE_URL
fromDatabase:
name: docusearch-db
property: connectionString
- key: REDIS_URL
fromService:
name: docusearch-redis
type: redis
property: connectionString
Troubleshooting
Problema 1: "Build failed — Dockerfile not found"
Solución: Render busca el Dockerfile en la raíz del repositorio por defecto. Si está en otro directorio, especifícalo en render.yaml o en el dashboard:
# render.yaml
services:
- type: web
dockerfilePath: ./app/Dockerfile
dockerContext: ./app
Problema 2: "Deploy failed — Port mismatch"
Solución: Render espera que tu app escuche en el puerto definido por la variable PORT (que Render inyecta automáticamente, default 10000). Si tu Dockerfile usa un puerto diferente:
# Opción A: Usar la variable PORT de Render
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "${PORT:-8000}"]
# Opción B: Script de entrypoint
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}"]
Problema 3: "App funciona local pero falla en Render — out of memory"
Solución: Tu plan no tiene suficiente RAM. El Free tier tiene 512 MB. FastAPI + uvicorn + openai SDK ya consumen ~200 MB. Si cargas embeddings o ChromaDB, necesitas Standard ($25/mes) o superior.
# Verificar uso de memoria local
docker stats docusearch-ai
# CONTAINER CPU % MEM USAGE / LIMIT
# docusearch 0.5% 180MiB / 512MiB
Problema 4: "Request timeout después de 30 segundos"
Solución: Render tiene un timeout de 30 segundos para requests HTTP. Si tu inferencia tarda más:
- Optimiza el prompt para reducir tokens de salida
- Usa streaming (SSE) para enviar tokens progresivamente
- Para procesamiento largo, usa un background worker + polling
from fastapi.responses import StreamingResponse
@app.post("/ask/stream")
async def ask_stream(query: Query):
async def generate():
async for chunk in stream_openai(query.question):
yield f"data: {json.dumps({'token': chunk})}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
Problema 5: "Free tier — la app duerme y tarda 30s en responder"
Solución: El Free tier pone tu app a dormir después de 15 minutos de inactividad. Opciones:
- Upgrade a Starter ($7/mes) — No sleep.
- Cron job de ping — Un servicio externo (UptimeRobot, cron-job.org) hace GET a tu
/healthcada 14 minutos. - Aceptar el cold start — Si es una demo o proyecto personal, 30 segundos de wait inicial es aceptable.
Ejercicios Prácticos
Ejercicio 1: Deploy básico en Render
Despliega la app AI de la cápsula 01 en Render. Configura la variable OPENAI_API_KEY y verifica que el endpoint /health y /ask funcionan.
Ver solución
# 1. Asegúrate de que tu repo tiene la estructura correcta
ls app/
# main.py Dockerfile requirements.txt
# 2. Push a GitHub
git add -A
git commit -m "Prepare app for Render deployment"
git push origin main
# 3. En Render Dashboard:
# - New → Web Service
# - Conectar repositorio
# - Runtime: Docker
# - Plan: Free (para prueba) o Starter
# - Environment: OPENAI_API_KEY = tu-key, PLATFORM = render
# - Create Web Service
# 4. Espera al deploy (2-5 minutos)
# 5. Verificar
RENDER_URL="https://tu-servicio.onrender.com"
curl $RENDER_URL/health
# {"status":"healthy","version":"1.0.0","platform":"render"}
curl -X POST $RENDER_URL/ask \
-H "Content-Type: application/json" \
-d '{"question": "¿Qué es deployment?", "max_tokens": 100}'
# {"answer":"...","model":"gpt-4o-mini","tokens_used":85}
Ejercicio 2: Agregar PostgreSQL para logging
Agrega una base de datos PostgreSQL en Render y modifica tu app para guardar cada inferencia como un log con pregunta, respuesta, modelo, tokens usados, y timestamp.
Ver solución
# Agregar a requirements.txt:
# psycopg2-binary==2.9.9
# app/database.py
import os
import psycopg2
from contextlib import contextmanager
DATABASE_URL = os.environ.get("DATABASE_URL")
def init_db():
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS inference_logs (
id SERIAL PRIMARY KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
model VARCHAR(50) NOT NULL,
tokens_used INTEGER,
latency_ms INTEGER,
created_at TIMESTAMP DEFAULT NOW()
)
""")
conn.commit()
@contextmanager
def get_connection():
conn = psycopg2.connect(DATABASE_URL)
try:
yield conn
finally:
conn.close()
def log_inference(question: str, answer: str, model: str, tokens: int, latency_ms: int):
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""INSERT INTO inference_logs
(question, answer, model, tokens_used, latency_ms)
VALUES (%s, %s, %s, %s, %s)""",
(question, answer, model, tokens, latency_ms),
)
conn.commit()
# En main.py, agregar al endpoint /ask:
import time
from database import init_db, log_inference
@app.on_event("startup")
def startup():
if os.environ.get("DATABASE_URL"):
init_db()
@app.post("/ask", response_model=Answer)
async def ask_question(query: Query):
start = time.time()
# ... llamada a OpenAI ...
latency_ms = int((time.time() - start) * 1000)
if os.environ.get("DATABASE_URL"):
log_inference(
query.question, response_text, "gpt-4o-mini", tokens, latency_ms
)
return Answer(answer=response_text, model="gpt-4o-mini", tokens_used=tokens)
# En Render Dashboard:
# 1. New → PostgreSQL → Free plan
# 2. Copiar Internal Database URL
# 3. En tu Web Service → Environment → Add:
# DATABASE_URL = postgresql://user:pass@host:5432/dbname
# 4. Redeploy
# Verificar:
curl -X POST $RENDER_URL/ask \
-H "Content-Type: application/json" \
-d '{"question": "Test con logging", "max_tokens": 50}'
# Los logs se guardan en PostgreSQL automáticamente
Ejercicio 3: Configurar render.yaml completo
Crea un render.yaml que defina tu web service con health check, auto-deploy filtrado por paths, y conexión a PostgreSQL y Redis.
Ver solución
# render.yaml
databases:
- name: docusearch-db
plan: free
databaseName: docusearch
user: docusearch
services:
- type: redis
name: docusearch-redis
plan: free
maxmemoryPolicy: allkeys-lru
ipAllowList: []
- type: web
name: docusearch-ai
runtime: docker
dockerfilePath: ./app/Dockerfile
dockerContext: ./app
region: oregon
plan: starter
healthCheckPath: /health
numInstances: 1
autoDeploy: true
buildFilter:
paths:
- app/**
- Dockerfile
- requirements.txt
- render.yaml
envVars:
- key: OPENAI_API_KEY
sync: false
- key: PLATFORM
value: render
- key: LOG_LEVEL
value: info
- key: DATABASE_URL
fromDatabase:
name: docusearch-db
property: connectionString
- key: REDIS_URL
fromService:
name: docusearch-redis
type: redis
property: connectionString
# Commit y push — Render detecta render.yaml automáticamente
git add render.yaml
git commit -m "Add complete Render IaC configuration"
git push origin main
# Render creará la base de datos, Redis, y el web service
# con todas las variables de entorno conectadas
Ejercicio 4: Medir cold start y latencia en Render
Escribe un script que mida el cold start (primera request después de inactividad) y la latencia promedio de tu app en Render. Ejecuta 10 requests y calcula estadísticas.
Ver solución
# measure_render.py
import time
import requests
import statistics
RENDER_URL = "https://tu-servicio.onrender.com"
RESULTS = []
def measure_request(endpoint: str, payload: dict = None) -> dict:
start = time.time()
if payload:
resp = requests.post(
f"{RENDER_URL}{endpoint}",
json=payload,
timeout=60,
)
else:
resp = requests.get(f"{RENDER_URL}{endpoint}", timeout=60)
elapsed = (time.time() - start) * 1000
return {
"endpoint": endpoint,
"status": resp.status_code,
"latency_ms": round(elapsed, 1),
"response_size": len(resp.content),
}
print("=== Cold Start Test ===")
print("(Espera 20 minutos de inactividad antes de ejecutar esto)")
cold = measure_request("/health")
print(f"Cold start: {cold['latency_ms']}ms (status: {cold['status']})")
print("\n=== Latencia de Health Check (10 requests) ===")
health_latencies = []
for i in range(10):
result = measure_request("/health")
health_latencies.append(result["latency_ms"])
print(f" Request {i+1}: {result['latency_ms']}ms")
time.sleep(1)
print(f"\nPromedio: {statistics.mean(health_latencies):.1f}ms")
print(f"Mediana: {statistics.median(health_latencies):.1f}ms")
print(f"P95: {sorted(health_latencies)[8]:.1f}ms")
print("\n=== Latencia de Inferencia (5 requests) ===")
inference_latencies = []
for i in range(5):
result = measure_request("/ask", {"question": "¿Qué es Python?", "max_tokens": 50})
inference_latencies.append(result["latency_ms"])
print(f" Request {i+1}: {result['latency_ms']}ms")
time.sleep(2)
print(f"\nPromedio inferencia: {statistics.mean(inference_latencies):.1f}ms")
print(f"Mediana inferencia: {statistics.median(inference_latencies):.1f}ms")
python measure_render.py
# Output esperado (Starter plan, sin cold start):
# Cold start: ~200-500ms (warm) o ~15000-30000ms (cold, Free tier)
# Health check promedio: 50-200ms
# Inferencia promedio: 1000-3000ms (depende del LLM)
Resumen
- Render simplifica deployment: Git push → app online con HTTPS en minutos.
- render.yaml es Infrastructure as Code para Render — define servicios, databases, variables en un archivo versionable.
- Free tier es útil para demos pero tiene cold start (30s) y memoria limitada (512 MB) — insuficiente para muchos AI workloads.
- Starter ($7/mes) es el mínimo viable para una app AI que usa APIs externas (OpenAI, Anthropic).
- PostgreSQL y Redis managed simplifican la stack, pero los free tiers tienen retención de 97 días.
- Request timeout de 30s es la limitación más impactante para AI — usa streaming (SSE) para respuestas largas.
- Environment variables se gestionan desde dashboard o render.yaml — los secrets nunca se commitean.
- El patrón más común para AI en Render es API stateless + LLM externo + cache Redis.
Recursos Adicionales
- Render Documentation — Documentación oficial completa
- Render YAML Reference — Especificación completa de render.yaml
- Render Docker Deployments — Guía de deployment con Docker
- Render PostgreSQL — Bases de datos managed
- Render Environment Variables — Gestión de variables y secrets
- Render Pricing — Pricing actualizado y comparativa de planes