create-botmesh: el archivo de
entrada es src/bot.js y se arranca con npm start
(node --env-file-if-exists=.env src/bot.js). Ajusta las rutas si las tuyas
son otras. Si escribes TypeScript, compílalo antes (tsc) y arranca el archivo
compilado.
Opción 1: un VPS con PM2
1. Preparar el servidor
2. Primera conexión (WhatsApp con Baileys)
./sessions/<session>. Mientras esa carpeta exista, el
bot se reconecta sin volver a escanear. Telegram, la Cloud API y los demás
canales con token se saltan este paso.
3. Mantenerlo activo con PM2
pm2 restart y pm2 stop solo apagan el bot de forma ordenada si atiende la
señal: añade el manejador de Apagado ordenado (ni el core
ni el bot.js generado instalan uno). PM2 mata el proceso kill_timeout ms
después de SIGINT (1600 ms por defecto), así que deja --kill-timeout por
encima del timeout del drain.
Opción 2: Docker
Dockerfile
RUN corepack enable && pnpm install --prod --frozen-lockfile y copia
pnpm-lock.yaml en lugar de package-lock.json. pnpm 10 y posteriores se
saltan el script de build de better-sqlite3 hasta que lo apruebes en
pnpm-workspace.yaml (allowBuilds: con better-sqlite3: true en pnpm 11,
onlyBuiltDependencies en pnpm 10; ver SQLiteStore).
Añade un .dockerignore con node_modules, sessions, data, .env y
*.qr.png, para que las credenciales nunca acaben en una capa de la imagen.
docker-compose.yml
SIGTERM con docker compose stop y espera stop_grace_period
antes de matar el contenedor: que sea mayor que el timeout del drain.
Variables de entorno
El framework no lee ninguna variable de entorno por su cuenta: cada credencial y ajuste es una opción de un constructor. Pasa tus variables de forma explícita, por ejemplo:.env local con node --env-file-if-exists=.env y, en producción,
toma los valores de tu orquestador o gestor de secretos. Comprobar las
obligatorias al arrancar (como hace env()) hace que un secreto ausente falle
con una línea clara en vez de con un error del provider más tarde.
Persistencia
Sesiones
El provider de Baileys guarda las credenciales de WhatsApp ensessionsDir/<session> (por defecto ./sessions/<session>). No borres esa
carpeta o tendrás que volver a escanear el QR:
Store
ElMemoryStore por defecto vive en RAM. En producción usa SQLiteStore
para un proceso (o RedisStore para varias réplicas), así los mensajes, chats,
contactos, checkpoints de flujos e historial de IA sobreviven a los reinicios:
file debe existir (SQLite crea el archivo, no sus carpetas).
Con un store persistente, los checkpoints de flow(...).persistent()
sobreviven a los reinicios. Ver Store persistente.
Copias de seguridad
Respalda la sesión y los datos juntos: son lo que permite que un bot redesplegado continúe sin escanear otro QR ni perder conversaciones.sqlite3 data/bot.db ".backup backup.db" mientras corre: con WAL activo, una
copia simple de una base de datos viva puede perder las últimas escrituras.
Haz una copia antes de actualizar @botmesh/sqlite-store, porque una versión
anterior no puede abrir un archivo migrado a un esquema más nuevo.
Apagado ordenado
bot.stop() desconecta el provider y termina las conversaciones activas. Con
{ drain: true } antes espera los envíos en vuelo, para que un despliegue
no corte una respuesta a la mitad:
Las respuestas que siguen enviándose cuando empieza
stop(), y las que el
provider ya aceptó, se registran en el log de mensajes (hooks after('send')
y escritura en el store, hasta 5 s) antes de cerrar el store, con o sin
drain.
Tras el drain no queda nada corriendo: los mensajes que esperaban en la cola de
un chat se descartan, no arranca ninguna conversación nueva hasta start(),
las respuestas en pausa que seguían esperando resuelven con { skipped: true }
y los ctx.stream() en curso se cancelan sin volver a tocar el transporte
(resuelven con cancelled: true). Ningún temporizador del bot mantiene vivo el
proceso después.
Con durabilidad, un mensaje descartado así sigue en el journal
y se procesa en el siguiente arranque.
Comprobaciones de salud
Un pequeño endpoint HTTP le dice a Docker, Kubernetes o tu balanceador si el bot está vivo y conectado.bot.getConnectionState() es 'connecting',
'authenticating', 'ready' o 'closed':
- Usa
/healthzpara liveness (reiniciar el contenedor cuando falla) y/readyzpara readiness (enviar tráfico solo mientras pasa). - Con réplicas y un transporte exclusivo (Baileys), las réplicas
en espera no están conectadas a propósito, así que su
/readyzresponde503. No las reinicies por eso. - Escucha en una interfaz privada o protege el puerto:
bot.metrics()no contiene datos personales, pero describe tu tráfico. @botmesh/serversirve/healthzy/readyzjunto a las rutas de webhooks: ver Servidor de webhooks.
Logging
Sinlogger, el bot registra con un logger pino integrado en nivel warn, así
que se ven los avisos de plugins y handlers (por ejemplo una respuesta de IA
fallida), y debug: true lo baja a debug. En producción, pasa tu propio
logger para elegir el nivel y el destino:
logIds: 'raw' solo para depurar en local.
Siguientes pasos
- Pasar a producción: la lista completa
- Varias réplicas: varios procesos del mismo bot (relevo ante caídas, volumen de webhooks)
- Operación: métricas y eventos
- Preguntas frecuentes: problemas comunes