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)

PlanPrecioRAMCPUIncluye
Free$0/mes512 MB0.1 vCPUSleep después de 15 min inactividad
Starter$7/mes512 MB0.5 vCPUNo sleep, custom domains
Standard$25/mes2 GB1 vCPUAuto-scaling horizontal
Pro$85/mes4 GB2 vCPUMás recursos, health checks avanzados
Pro Plus$175/mes8 GB4 vCPUPara 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ónImpacto en AIWorkaround
Free tier sleep (15 min)Cold start de ~30s después de inactividadUsar Starter ($7/mes) o cron job de ping
512 MB RAM (Free/Starter)No cabe ChromaDB + FastAPI + dependenciasUsar Standard ($25/mes) o API externa de vectores
Request timeout: 30sStreaming largo o inferencia compleja puede fallarOptimizar prompts, usar streaming para respuestas largas
No WebSocket nativo (Free)Streaming de tokens limitadoUsar SSE (Server-Sent Events) en vez de WebSocket
No GPUNo puedes correr modelos locales grandesUsar APIs de inferencia (OpenAI, Anthropic, Together AI)
Disco efímeroArchivos escritos se pierden en cada deployUsar 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:

  1. Optimiza el prompt para reducir tokens de salida
  2. Usa streaming (SSE) para enviar tokens progresivamente
  3. 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:

  1. Upgrade a Starter ($7/mes) — No sleep.
  2. Cron job de ping — Un servicio externo (UptimeRobot, cron-job.org) hace GET a tu /health cada 14 minutos.
  3. 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

  1. Render Documentation — Documentación oficial completa
  2. Render YAML Reference — Especificación completa de render.yaml
  3. Render Docker Deployments — Guía de deployment con Docker
  4. Render PostgreSQL — Bases de datos managed
  5. Render Environment Variables — Gestión de variables y secrets
  6. Render Pricing — Pricing actualizado y comparativa de planes