Despliegue en producción

Por: Artiko
paperclipproduccióndockerpostgresqldeployment

Cuándo pasar a producción

Durante el desarrollo y las pruebas, el modo local con base de datos embebida es suficiente. Pero cuando Paperclip es el sistema que orquesta trabajo real de agentes en tu empresa, necesitas una infraestructura que garantice:

Este capítulo cubre exactamente eso: cómo pasar de “funciona en mi máquina” a “funciona en producción”.

Modos de deployment

Paperclip soporta dos modos de autenticación/deployment:

Modo local_trusted

El modo por defecto. Sin autenticación, sin tokens JWT. Asume que el acceso al servidor implica confianza. Adecuado para:

# .env para modo local_trusted
AUTH_MODE=local_trusted
PORT=3100
DATABASE_URL=postgres://paperclip:password@localhost:5432/paperclip

Riesgos: Si el puerto 3100 es accesible desde internet sin VPN, cualquiera puede acceder al sistema. Nunca expongas local_trusted directamente a internet.

Modo authenticated

Requiere un token JWT para todas las llamadas a la API. Adecuado para:

# .env para modo authenticated
AUTH_MODE=authenticated
JWT_SECRET=una-clave-secreta-muy-larga-y-aleatoria-de-al-menos-32-caracteres
JWT_EXPIRY=7d        # Tokens expiran en 7 días
PORT=3100
DATABASE_URL=postgres://paperclip:[email protected]:5432/paperclip
BASE_URL=https://paperclip.miempresa.com

En modo authenticated, la primera vez que accedes a la UI se te pide crear una cuenta de Board admin. Después, todos los accesos requieren login.

Para generar tokens de API para acceso programático:

# Via la UI
Board Settings API Tokens Generate Token

# Via CLI
npx paperclipai token:generate --name "agente-externo" --expiry 30d

Configurar PostgreSQL externo

Para producción, PostgreSQL externo es obligatorio. La base de datos embebida no está diseñada para alta disponibilidad ni para volúmenes grandes de datos.

PostgreSQL en servidor propio

# Instalar PostgreSQL 16 en Ubuntu/Debian
sudo apt update
sudo apt install -y postgresql-16

# Crear usuario y base de datos
sudo -u postgres psql << 'EOF'
CREATE USER paperclip WITH PASSWORD 'tu-password-seguro-aqui';
CREATE DATABASE paperclip OWNER paperclip;
GRANT ALL PRIVILEGES ON DATABASE paperclip TO paperclip;
EOF

# Verificar la conexión
psql "postgres://paperclip:tu-password@localhost:5432/paperclip" -c "SELECT version();"

PostgreSQL en la nube

Neon (recomendado para empezar, tiene tier gratuito):

# Después de crear la instancia en neon.tech
DATABASE_URL=postgres://user:[email protected]/paperclip?sslmode=require

Supabase:

DATABASE_URL=postgres://postgres:[email protected]:5432/postgres

Railway:

DATABASE_URL=postgresql://postgres:[email protected]:5432/railway

AWS RDS:

DATABASE_URL=postgres://paperclip:[email protected]:5432/paperclip?sslmode=require

Configurar la base de datos

Paperclip ejecuta las migraciones automáticamente al arrancar. Si necesitas ejecutarlas manualmente:

# Ejecutar migraciones
DATABASE_URL=postgres://... pnpm db:migrate

# Verificar estado de las migraciones
DATABASE_URL=postgres://... pnpm db:migrate:status

# Rollback de la última migración (usa con cuidado)
DATABASE_URL=postgres://... pnpm db:migrate:rollback

Configuración de pooling

Para producción, configura el pool de conexiones según la carga esperada:

# Variables de entorno para el pool
DB_POOL_MIN=2              # Conexiones mínimas siempre activas
DB_POOL_MAX=20             # Máximo de conexiones simultáneas
DB_POOL_IDLE_TIMEOUT=10000 # Cerrar conexiones inactivas después de 10s
DB_POOL_CONNECTION_TIMEOUT=5000  # Timeout al intentar conectar

Para alta carga con muchos agentes activos, considera usar PgBouncer como proxy de pooling entre Paperclip y PostgreSQL.

Opciones de storage

Además de la base de datos, Paperclip necesita almacenar archivos adjuntos, exports de empresas, y logs.

Almacenamiento local:

STORAGE_TYPE=local_disk
STORAGE_PATH=/var/paperclip/data

Simple y sin costo adicional. El problema: si el servidor falla, los archivos se pueden perder. Hacer backup del directorio regularmente.

AWS S3:

STORAGE_TYPE=s3
AWS_BUCKET=mi-bucket-paperclip
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

Cloudflare R2 (compatible con S3, sin egress fees):

STORAGE_TYPE=s3
S3_ENDPOINT=https://account-id.r2.cloudflarestorage.com
S3_BUCKET=paperclip-storage
S3_ACCESS_KEY_ID=tu-r2-access-key
S3_SECRET_ACCESS_KEY=tu-r2-secret-key
S3_FORCE_PATH_STYLE=true

Minio (self-hosted S3 compatible):

STORAGE_TYPE=s3
S3_ENDPOINT=http://minio.internal:9000
S3_BUCKET=paperclip
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_FORCE_PATH_STYLE=true

Deployment con Docker

Docker es la forma recomendada de desplegar Paperclip en producción. Aquí está una configuración completa para un deployment real:

# Dockerfile (el oficial del proyecto)
FROM node:20-alpine AS base
WORKDIR /app
RUN npm install -g [email protected]

# Dependencies
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# Build
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build

# Production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# Copiar solo lo necesario
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# Usuario no-root para seguridad
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 paperclip
USER paperclip

EXPOSE 3100
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:3100/api/health || exit 1

CMD ["node", "dist/server.js"]

docker-compose.yml para producción:

version: '3.8'

services:
  paperclip:
    build:
      context: .
      dockerfile: Dockerfile
    image: paperclip:latest
    container_name: paperclip
    restart: unless-stopped
    ports:
      - "3100:3100"
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://paperclip:${POSTGRES_PASSWORD}@postgres:5432/paperclip
      PORT: 3100
      AUTH_MODE: ${AUTH_MODE:-local_trusted}
      JWT_SECRET: ${JWT_SECRET}
      BASE_URL: ${BASE_URL:-http://localhost:3100}
      STORAGE_TYPE: ${STORAGE_TYPE:-local_disk}
      STORAGE_PATH: /app/data/storage
      LOG_LEVEL: ${LOG_LEVEL:-info}
      PAPERCLIP_TELEMETRY_DISABLED: ${PAPERCLIP_TELEMETRY_DISABLED:-0}
    volumes:
      - paperclip-data:/app/data
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3100/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "5"

  postgres:
    image: postgres:16-alpine
    container_name: paperclip-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: paperclip
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: paperclip
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8"
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./backups:/backups   # Para acceso a backups
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U paperclip -d paperclip"]
      interval: 10s
      timeout: 5s
      retries: 5
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "3"

  # Nginx como reverse proxy (opcional pero recomendado)
  nginx:
    image: nginx:alpine
    container_name: paperclip-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./ssl:/etc/ssl/paperclip:ro   # Certificados SSL
    depends_on:
      - paperclip

volumes:
  paperclip-data:
  postgres-data:

El archivo .env para el deployment:

# .env (NO commitear en git)
POSTGRES_PASSWORD=un-password-muy-seguro-aqui
AUTH_MODE=authenticated
JWT_SECRET=otra-clave-muy-larga-y-aleatoria-aqui
BASE_URL=https://paperclip.miempresa.com
LOG_LEVEL=info
PAPERCLIP_TELEMETRY_DISABLED=0
STORAGE_TYPE=local_disk

Arrancar:

docker-compose up -d
docker-compose logs -f paperclip

Actualizar:

git pull origin main
docker-compose build paperclip
docker-compose up -d paperclip

Nginx como reverse proxy

Para exponer Paperclip de forma segura con HTTPS:

# nginx.conf
server {
    listen 80;
    server_name paperclip.miempresa.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name paperclip.miempresa.com;
    
    ssl_certificate /etc/ssl/paperclip/fullchain.pem;
    ssl_certificate_key /etc/ssl/paperclip/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    
    # Seguridad adicional
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header X-Content-Type-Options nosniff always;
    
    # Proxy a Paperclip
    location / {
        proxy_pass http://paperclip:3100;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
        proxy_cache_bypass $http_upgrade;
        
        # Timeouts más largos para ejecuciones de agentes largas
        proxy_connect_timeout 60s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;
    }
    
    # Límite de tamaño para uploads
    client_max_body_size 50M;
}

Para los certificados SSL, usa Certbot con Let’s Encrypt:

sudo certbot certonly --webroot \
  -w /var/www/certbot \
  -d paperclip.miempresa.com \
  --email [email protected] \
  --agree-tos

Acceso remoto con Tailscale

Una alternativa más simple a Nginx + SSL es usar Tailscale para acceso remoto seguro. Con Tailscale, el servidor Paperclip está en tu red privada virtual y solo tú (y los miembros de tu Tailnet) pueden acceder.

# Instalar Tailscale en el servidor
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

# El servidor tendrá una IP de Tailscale tipo 100.x.y.z
# Accedes a Paperclip en: http://100.x.y.z:3100

Ventajas de Tailscale:

Los agentes que necesitan llamar a Paperclip desde otras máquinas también deben estar en el Tailnet:

# En la máquina donde corre el agente (ej: tu laptop o un CI/CD)
sudo tailscale up
# Ahora puede acceder a http://100.x.y.z:3100

Para los agentes configurados con HTTP adapter, usa la IP de Tailscale del servidor Paperclip:

adapterConfig:
  callbackUrl: http://100.x.y.z:3100/api/agents/callback

Variables de entorno completas

Aquí está la referencia completa de variables de entorno de Paperclip para producción:

# =============================================
# Base - Obligatorias
# =============================================
DATABASE_URL=postgres://user:pass@host:5432/db
PORT=3100

# =============================================
# Autenticación
# =============================================
AUTH_MODE=authenticated          # local_trusted | authenticated
JWT_SECRET=clave-secreta-32+chars
JWT_EXPIRY=7d                    # Expiración de tokens

# =============================================
# URLs y networking
# =============================================
BASE_URL=https://paperclip.example.com
CORS_ORIGINS=https://paperclip.example.com,https://app.example.com
SERVE_UI=false                   # true solo si UI está en repo separado

# =============================================
# Base de datos - Pooling
# =============================================
DB_POOL_MIN=2
DB_POOL_MAX=20
DB_POOL_IDLE_TIMEOUT=10000
DB_CONNECTION_TIMEOUT=5000
DB_SSL=true                      # Activar para conexiones remotas

# =============================================
# Storage
# =============================================
STORAGE_TYPE=local_disk          # local_disk | s3
STORAGE_PATH=/app/data/storage   # Para local_disk
# Variables S3 (si STORAGE_TYPE=s3)
S3_BUCKET=paperclip-storage
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=xxx
S3_SECRET_ACCESS_KEY=xxx
S3_ENDPOINT=                     # Para S3-compatible (Minio, R2, etc.)
S3_FORCE_PATH_STYLE=false

# =============================================
# Agentes y ejecución
# =============================================
AGENT_EXECUTION_TIMEOUT=1800000  # 30 minutos default
MAX_CONCURRENT_AGENTS=10         # Máximo de agentes corriendo simultáneamente
AGENT_CHECKOUT_TIMEOUT=3600000   # 1 hora antes de liberar checkout

# =============================================
# Logging
# =============================================
LOG_LEVEL=info                   # debug | info | warn | error
LOG_FORMAT=json                  # json | text
LOG_FILE=/var/log/paperclip/server.log
AGENT_LOG_FILE=/var/log/paperclip/agents.log

# =============================================
# Telemetría
# =============================================
PAPERCLIP_TELEMETRY_DISABLED=0   # 1 para deshabilitar

# =============================================
# Notificaciones
# =============================================
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=tu-sendgrid-api-key
SMTP_FROM=[email protected]

# Slack (opcional)
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzz
SLACK_CHANNEL=#paperclip-alerts

Multi-instance setup

Para alta disponibilidad, puedes correr múltiples instancias de Paperclip compartiendo la misma base de datos PostgreSQL. Paperclip usa el sistema de heartbeats y checkouts en la base de datos para coordinar entre instancias:

# docker-compose para multi-instance (detrás de un load balancer)
version: '3.8'

services:
  paperclip-1:
    image: paperclip:latest
    environment:
      DATABASE_URL: postgres://...
      INSTANCE_ID: paperclip-1
    
  paperclip-2:
    image: paperclip:latest
    environment:
      DATABASE_URL: postgres://...
      INSTANCE_ID: paperclip-2

  load-balancer:
    image: nginx:alpine
    # Configurar upstream con paperclip-1 y paperclip-2

Consideración importante: Los heartbeats en multi-instance usan distributed locking en PostgreSQL para garantizar que el mismo heartbeat no se dispara en dos instancias simultáneamente. Esto funciona correctamente con PostgreSQL, pero no con la base de datos embebida.

Telemetría: qué recopila y cómo deshabilitarla

Por defecto, Paperclip recopila métricas anónimas de uso para ayudar al equipo a entender cómo se usa el sistema:

No recopila: nombres de empresas, contenido de tareas, API keys, datos de presupuesto, o cualquier información identificable.

Para deshabilitar completamente:

PAPERCLIP_TELEMETRY_DISABLED=1

O en docker-compose:

environment:
  PAPERCLIP_TELEMETRY_DISABLED: "1"

Backup y recuperación

Para una empresa de agentes en producción, los backups son críticos. Aquí está la estrategia recomendada:

Backup automático diario de PostgreSQL:

#!/bin/bash
# backup-paperclip.sh - ejecutar desde cron diariamente

BACKUP_DIR=/var/backups/paperclip
DATE=$(date +%Y%m%d-%H%M%S)
DB_URL="postgres://paperclip:password@localhost:5432/paperclip"

mkdir -p "$BACKUP_DIR"

# Dump de la base de datos
pg_dump "$DB_URL" --format=custom --compress=9 \
  -f "$BACKUP_DIR/paperclip-$DATE.dump"

# Mantener solo los últimos 30 días
find "$BACKUP_DIR" -name "*.dump" -mtime +30 -delete

echo "Backup completado: paperclip-$DATE.dump"

Añadir al cron:

# Backup diario a las 2am
0 2 * * * /path/to/backup-paperclip.sh >> /var/log/paperclip-backup.log 2>&1

Restaurar desde backup:

# Restaurar en una base de datos nueva
createdb paperclip_restore
pg_restore --format=custom \
  -d "postgres://paperclip:password@localhost:5432/paperclip_restore" \
  /var/backups/paperclip/paperclip-20260405-020000.dump

# Verificar la restauración
psql "postgres://paperclip:password@localhost:5432/paperclip_restore" \
  -c "SELECT COUNT(*) FROM tasks;"

Backup de la configuración de empresas:

Además del backup de la base de datos, exporta regularmente la configuración de cada empresa en formato JSON. Esto actúa como un backup de configuración independiente del motor de base de datos:

# Script de export de configuraciones
for company in $(curl -s http://localhost:3100/api/companies | jq -r '.[].id'); do
  npx paperclipai export \
    --company "$company" \
    --type snapshot \
    --output "/backups/companies/$company-$(date +%Y%m%d).json"
done

Monitoring del servidor en producción

Para mantener visibilidad sobre la salud del servidor en producción, configura monitoring básico:

# Health check endpoint
curl http://localhost:3100/api/health
# {"status":"ok","version":"1.x.x","uptime":86400,"agents":{"active":3,"paused":0}}

# Métricas (si está habilitado el endpoint de métricas)
curl http://localhost:3100/api/metrics
# Formato Prometheus-compatible para scraping con Grafana

Un alert básico con cron:

#!/bin/bash
# monitor-paperclip.sh
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3100/api/health)

if [ "$RESPONSE" != "200" ]; then
  # Enviar alerta (slack, email, pagerduty, etc.)
  curl -X POST "$SLACK_WEBHOOK" \
    -d "{\"text\": \"🚨 Paperclip down! Health check returned HTTP $RESPONSE\"}"
fi

Con producción correctamente configurada, el siguiente y último capítulo cubre los casos avanzados: gestión multi-empresa, el plugin system, Skills Manager, y el ecosistema en evolución de Paperclip.