Despliegue en producción
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:
- Persistencia durable: Los datos de tareas, audit logs, y configuraciones no se pierden si el servidor se reinicia.
- Disponibilidad: El servidor está accesible para los agentes 24/7, no solo cuando tienes el laptop abierto.
- Seguridad: El acceso está autenticado, los secretos están protegidos.
- Observabilidad: Sabes cuándo algo va mal antes de que afecte a los agentes.
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:
- Máquina personal o servidor dedicado en red privada
- Acceso via Tailscale o VPN (el tunnel garantiza la autenticidad)
- Uso solo desde localhost
# .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:
- Servidores cloud accesibles desde internet
- Deployments multi-tenant donde diferentes equipos acceden al mismo servidor
- Cualquier escenario donde quieras auditar quién accede
# .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:
- Sin certificados SSL que gestionar (Tailscale usa su propia PKI)
- Sin exposición pública del servicio
- Funciona aunque el servidor esté en una red NAT
- Se puede usar
local_trustedauth mode de forma segura
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:
- Número de agentes activos (sin identificadores)
- Tipos de adapters usados
- Frecuencia de heartbeats
- Tasa de éxito/fallo de heartbeats
- Versión del servidor
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.