Despliegue y backup
El motor es un solo proceso (motor) con su Postgres y, si usas multimedia, un MinIO o un bucket
compatible con S3. Es un servicio interno: lo llaman tus apps por la red privada y lo administran
los operadores por VPN o HTTPS privado. No se publica en internet.
Esta página cubre la imagen, un compose de ejemplo, un despliegue en Coolify, las cuatro variables de entorno, los Ajustes y el ciclo del backup: hacerlo, verificarlo y restaurarlo aislado.
La imagen
Sección titulada «La imagen»El Dockerfile del repositorio arma una imagen distroless, sin shell, con las imágenes base
fijadas por digest. Trae tres binarios en /app:
| Binario | Para qué |
|---|---|
/app/motor |
El servicio: la API /v1, la administración /admin/api, el panel /admin/ y los workers. Es el ENTRYPOINT. Aplica las migraciones propias pendientes al arrancar, bajo lock. motor salud es el healthcheck. |
/app/migrate |
Las migraciones: up, status, version, whatsmeow (el store del SDK), todo (las dos) y verificar (la revisión previa a una actualización). down solo corre con APP_ENV=desarrollo. |
/app/motorctl |
La línea de comandos del operador: crear el primer operador, diagnostico, los backups y el modo de mantenimiento. |
Además trae ffmpeg y ffprobe estáticos para convertir notas de voz, stickers y vídeos.
Cómo corre:
- Como
nonroot(UID 65532). - Pensada para un contenedor de solo lectura. Lo único escribible es
/tmp(untmpfspara la multimedia en curso) y el volumen/respaldos(los backups locales demotorctl). HEALTHCHECKcon["/app/motor", "salud"]cada 30 s: pideGET /health/readyal propio proceso y sale con 0 solo si responde 200. No necesitacurl.- Escucha en el puerto 8080 (o el de
PORT).
Variables de entorno
Sección titulada «Variables de entorno»El motor lee solo cuatro variables. Todo lo demás (límites, retención, MinIO, webhooks, backups,
proxies, duración de la sesión…) va en Ajustes, guardado en la base, con los secretos sellados.
Un test (TestSoloVariablesDeInfraestructura) falla si el código lee cualquier otra.
| Variable | Obligatoria | Qué es |
|---|---|---|
DATABASE_URL |
Sí | El Postgres del motor, con base y rol propios. |
APP_MASTER_KEY |
Sí, para motor |
32 caracteres o más. Es la raíz de todas las claves: sella los secretos de Ajustes y los payloads del ledger, deriva los HMAC de las keys, las sesiones y el CSRF, y cifra los backups. |
APP_ENV |
No | produccion (por omisión) o desarrollo. En producción las cookies del panel llevan Secure. |
PORT |
No | El puerto HTTP. Por omisión, 8080. |
migrate y casi todo motorctl no necesitan APP_MASTER_KEY. Los comandos de backup sí;
verificar y restaurar aceptan además otra clave por la entrada estándar con --clave-stdin.
Docker Compose
Sección titulada «Docker Compose»El repositorio trae los compose de producción en deploy/:
| Archivo | Qué corre |
|---|---|
deploy/compose.prod.yml |
Variante elegida. El motor con su Postgres 17 y su MinIO propios, aislados, con backup independiente. |
deploy/compose.prod.compartido.yml |
Alternativa: solo el motor y migrate, contra un Postgres y un MinIO que ya tengas (con base, rol y buckets propios). |
Para trabajar en local están deploy/compose.local.yml y el override deploy/compose.falso.yml, que
usa un driver falso sin WhatsApp. Lo esencial del compose de producción, con marcadores:
name: motor-whatsapp-prod
networks: coolify: # la red compartida con las apps que llaman al motor external: true
services: postgres: image: postgres:17-alpine@sha256:DIGEST environment: POSTGRES_USER: motor POSTGRES_PASSWORD: ${SERVICE_PASSWORD_POSTGRES} POSTGRES_DB: motor_whatsapp volumes: [postgres-data:/var/lib/postgresql/data] healthcheck: test: ["CMD-SHELL", "pg_isready -U motor -d motor_whatsapp"] mem_limit: 384m restart: unless-stopped
minio: # sin ningún puerto publicado: solo lo ve el motor image: IMAGEN_DE_MINIO@sha256:DIGEST command: ["server", "/data"] environment: MINIO_ROOT_USER: ${SERVICE_USER_MINIO} MINIO_ROOT_PASSWORD: ${SERVICE_PASSWORD_MINIO} volumes: [minio-data:/data] mem_limit: 384m restart: unless-stopped
migrate: # migra las tablas propias y el store de whatsmeow, y termina image: ghcr.io/TU_ORGANIZACION/motor-whatsapp:${MOTOR_TAG:-main} entrypoint: ["/app/migrate"] command: ["todo"] environment: DATABASE_URL: postgres://motor:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/motor_whatsapp?sslmode=disable APP_ENV: produccion depends_on: { postgres: { condition: service_healthy } } read_only: true cap_drop: ["ALL"] restart: "no"
motor: image: ghcr.io/TU_ORGANIZACION/motor-whatsapp:${MOTOR_TAG:-main} environment: DATABASE_URL: postgres://motor:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/motor_whatsapp?sslmode=disable APP_MASTER_KEY: ${SERVICE_PASSWORD_64_MOTORKEY} APP_ENV: produccion PORT: "8080" ports: # solo en loopback del host: el panel sale por Tailscale Serve - "${PANEL_BIND:-127.0.0.1}:${PANEL_PUERTO:-8088}:8080" networks: default: {} coolify: aliases: [motor-whatsapp-prod-api] # un alias único, nunca «api» depends_on: migrate: { condition: service_completed_successfully } minio: { condition: service_started } read_only: true cap_drop: ["ALL"] mem_limit: 512m pids_limit: 512 tmpfs: ["/tmp:size=192m,mode=1777"] volumes: [respaldos:/respaldos] healthcheck: test: ["CMD", "/app/motor", "salud"] interval: 30s start_period: 30s restart: unless-stopped
volumes: postgres-data: minio-data: respaldos:Notas sobre el compose:
- Los secretos se llaman como las variables «mágicas» de Coolify (
SERVICE_PASSWORD_…,SERVICE_USER_…), que la plataforma genera al leer el archivo. Con Docker Compose a secas, defínelos tú en un.envfuera del repositorio.APP_MASTER_KEYacepta cualquier cadena de 32 caracteres o más (se deriva con HKDF):SERVICE_PASSWORD_64_MOTORKEYda 64 al azar. - Fija la imagen.
MOTOR_TAG=sha-<commit>:mainse mueve sola. - El servicio
migratees necesario. El motor migra solo sus tablas propias al arrancar; el store de whatsmeow se migra únicamente conmigrate whatsmeow(omigrate todo). Si el store no está al día, el motor atiende la API sin transporte y/health/readyresponde 503 («El store de whatsmeow no está al día: corre migrate whatsmeow.»). - La memoria: medido, el motor usa de 109 a 140 MB con 1 a 10 instancias y de 133 a 167 MB de
pico con historial (Capacidad). Con
mem_limit: 512m, pon en Ajustes → Operación → Memoria del proceso unos 384 MiB (es elGOMEMLIMITdel recolector de Go). /tmpguarda la multimedia mientras se descarga o se convierte, y cuenta contra el límite de memoria. Si el espacio libre baja de Ajustes → Multimedia → Espacio libre mínimo, lo nuevo espera y se abre la alertapresion_disco; nunca se borra lo aceptado.- El reinicio (
unless-stopped) es de Docker: el motor nunca reinicia una instancia en bucle. - MinIO es opcional. Sin almacenamiento, la multimedia queda deshabilitada y el texto, los eventos
y las entregas funcionan igual. Su endpoint interno (
http://minio:9000) y sus claves van en Ajustes → Almacenamiento, no en una variable.
El primer operador
Sección titulada «El primer operador»El primer operador se crea con motorctl, dentro del contenedor del motor. La contraseña (12
caracteres o más) se lee de la entrada estándar, nunca de un argumento ni de una variable. Por
omisión el rol es superadmin.
# Con terminal, pide la contraseña dos veces sin mostrarla:docker compose exec motor /app/motorctl operador crear \ --email operador@empresa.com --nombre "Operador principal"
# Sin terminal (lee la primera línea):printf '%s\n' 'UNA_CONTRASEÑA_LARGA' | docker compose exec -T motor \ /app/motorctl operador crear --email operador@empresa.com --nombre "Operador principal"Después entra en https://motor.ejemplo.com/admin/ (por tu red privada) y sigue con
Crear la app y la key.
Para comprobar el despliegue sin abrir el panel:
docker compose exec motor /app/motorctl diagnosticoDice la versión y el commit, el entorno, si APP_MASTER_KEY tiene el largo mínimo (sin mostrarla),
la latencia de la base, las migraciones, el modo y algunos conteos, sin secretos ni datos personales.
Con Coolify
Sección titulada «Con Coolify»Así se despliega con Coolify (el runbook del repositorio, docs/runbooks/despliegue-coolify.md, tiene
cada comando):
-
Crea un servicio Docker Compose por la API de Coolify:
POST /api/v1/servicescon eldeploy/compose.prod.ymldel repositorio endocker_compose_raw(en base64) y las imágenes de GHCR. Coolify no lee el repositorio y nunca se pega el compose en el editor (puede cortar líneas). El token de la API no va en la línea de comandos. Sin dominio: el motor no pasa por el proxy de Coolify. Para actualizar, unPATCHdel mismo servicio con el compose nuevo y elMOTOR_TAG. -
Los secretos los genera Coolify al registrar el compose, y aparecen en las variables de entorno del recurso:
Variable generada La usa como SERVICE_PASSWORD_64_MOTORKEYAPP_MASTER_KEYSERVICE_PASSWORD_POSTGRESla contraseña del rol motor(y suDATABASE_URL)SERVICE_USER_MINIOySERVICE_PASSWORD_MINIOel usuario y la contraseña del MinIO propio A mano, solo si hace falta:
MOTOR_TAG,PANEL_BIND(por omisión127.0.0.1) yPANEL_PUERTO(por omisión8088; el 8080 del host suele ser el de Traefik). -
Antes del primer Deploy, guarda en tu gestor de contraseñas la
SERVICE_PASSWORD_64_MOTORKEYy las dos de MinIO. No cambies la clave después del primer arranque: sella los Ajustes y cifra los backups. -
Deploy. El orden esperado:
postgressano,migratetermina con 0 ymotorsano. -
El primer operador, con
motorctldentro del contenedor del motor (docker exec -i), con la contraseña por la entrada estándar. -
El panel por Tailscale Serve. El puerto queda en
127.0.0.1:8088del host; Tailscale Serve lo publica solo en tu tailnet, con HTTPS:Ventana de terminal sudo tailscale serve --bg --https=8443 http://127.0.0.1:8088tailscale serve status # https://TU_SERVIDOR.TU_TAILNET.ts.net:8443Es Serve, no Funnel (Funnel lo publicaría en internet). Hace falta HTTPS: en producción las cookies de sesión y CSRF son
Secure, y sobrehttp://(por ejemplo, conPANEL_BINDen la IP de Tailscale) el navegador las descarta y el login no entra. -
Verifica desde el servidor:
Ventana de terminal curl -fsS http://127.0.0.1:8088/health/ready # {"estado":"ok","migraciones_al_dia":true,…}docker exec CONTENEDOR_DEL_MOTOR /app/motorctl diagnostico -
Carga el almacenamiento en Ajustes → Almacenamiento: endpoint
http://minio:9000, bucketmotor-mediay las clavesSERVICE_USER_MINIOySERVICE_PASSWORD_MINIO, copiadas de Coolify al panel (que las sella). «Probar» dice «El bucket no existe» hasta el primer uso: el motor lo crea con la primera multimedia. Después, el backup en el bucketmotor-backups. -
Conecta tus apps por la red
coolify: llaman ahttp://motor-whatsapp-prod-api:8080/v1con su API key. Para los webhooks, el host de tu app va en Ajustes → Webhooks → Destinos permitidos.
El sitio de esta documentación tiene su propia receta, deploy/docs-estatico/ (el build de Astro
servido por un nginx sin root, en el puerto 8080). El CI publica su imagen privada en GHCR y un
servicio Docker Compose de Coolify, creado por la API como el del motor, la baja con
deploy/docs-estatico/compose.yml; el dominio público lo asigna la variable mágica
SERVICE_FQDN_DOCS_8080. Ese sitio sí puede ser público; el motor, nunca.
La red privada
Sección titulada «La red privada»- Nada del motor va a internet de entrada. Ni la API
/v1ni/admin. Publica el puerto solo en una interfaz privada o en la red de Docker que comparte con sus apps. - Sí necesita salida hacia WhatsApp (o hacia un proxy de transporte, si lo configuras en
Ajustes → Proxies). El motor no lee
HTTP_PROXYniHTTPS_PROXYdel entorno. - HTTPS para los operadores. Con
APP_ENV=produccionlas cookies del panel llevanSecure: el navegador no las guarda sobrehttp://salvo enlocalhost. Pon un HTTPS privado delante de/admin/: Tailscale Serve (como en Coolify), un proxy inverso interno o la VPN. - Los webhooks y los bridges solo salen a los hosts de la lista de destinos permitidos (global en Ajustes y por app). Las direcciones de metadatos de la nube se bloquean siempre. Lo detalla Seguridad.
Ajustes
Sección titulada «Ajustes»Ajustes es la configuración del producto: está en la base, la edita un superadmin en el panel (Ajustes) y vale para todo el motor. Algunas claves son el valor de fábrica y cada app o cada instancia puede tener el suyo (el cupo de instancias, la cuota de consultas, la cuota de multimedia, la política del historial). Los grupos Backups y Almacenamiento tienen un botón Probar que comprueba la conexión con lo guardado.
Un valor vacío vuelve al de fábrica. Los secretos se guardan sellados con APP_MASTER_KEY, nunca
salen por la API (se ven como «configurado») y se reemplazan escribiendo uno nuevo.
General y Seguridad
Sección titulada «General y Seguridad»| Clave | Para qué | Por omisión | Secreto |
|---|---|---|---|
general.nombre_visible |
El nombre que muestra la administración. | Motor WhatsApp |
No |
general.url_documentacion |
La dirección del sitio de la documentación; la entrada /docs del motor enlaza a él. |
vacío | No |
seguridad.sesion_horas |
Duración de la sesión de un operador (1 a 168 h). | 12 |
No |
seguridad.intentos_login |
Intentos de acceso cada 15 minutos, por correo y por IP (1 a 50). | 5 |
No |
Límites
Sección titulada «Límites»| Clave | Para qué | Por omisión | Secreto |
|---|---|---|---|
limites.cupo_instancias |
Instancias por app (cupo de fábrica; cada app puede tener el suyo). | 3 |
No |
limites.mensajes_por_segundo |
Mensajes por segundo por instancia (1 a 20). | 1 |
No |
limites.rafaga |
Envíos seguidos permitidos (1 a 20). | 2 |
No |
limites.operaciones_remotas |
Operaciones simultáneas contra WhatsApp en todo el motor (1 a 64). | 4 |
No |
limites.consultas_identidad_dia |
Consultas de identidad por app y día («¿está en WhatsApp?», perfiles, fotos de otros). | 500 |
No |
limites.numeros_por_consulta |
Números por consulta de identidad (1 a 50). | 20 |
No |
limites.envios_pendientes |
Envíos en cola por instancia; con la cola llena, un envío nuevo recibe 429 y no se guarda. | 200 |
No |
estados.max_destinatarios |
Destinatarios máximos de un status publicado. | 256 |
No |
Retención e historial
Sección titulada «Retención e historial»| Clave | Para qué | Por omisión | Secreto |
|---|---|---|---|
retencion.dias_entregados |
Días que se guarda lo ya entregado. Lo pendiente no vence. | 30 |
No |
historial.importar_mensajes |
Importar mensajes del historial (de fábrica para cada instancia). | false |
No |
historial.dias_maximos |
Lo más viejo que se importa, en días. | 90 |
No |
historial.mensajes_por_trabajo |
Cuota de mensajes de cada importación o pedido. | 5000 |
No |
historial.contrapresion_entregas |
Con más entregas de webhooks pendientes en la instancia, la importación espera. | 2000 |
No |
historial.espera_pedido_minutos |
Cuánto se espera la respuesta del teléfono a un pedido de historial. | 10 |
No |
historial.pedido_chat |
Con qué JID va el chat en un pedido de historial: telefono, lid, pn o primero. |
telefono |
No |
historial.pedido_ancla |
Desde qué mensaje se piden los anteriores: no_revocado, mas_viejo o del_telefono. |
no_revocado |
No |
historial.pedido_momento |
El momento del ancla en segundos o ms. |
segundos |
No |
historial.pedido_fuera_del_historial |
Qué hacer con chats que no vinieron en el historial inicial: auto, pedir o no_pedir. |
auto |
No |
historial.pedido_inline |
Si el pedido acepta la respuesta en el mismo aviso: no o si. |
no |
No |
estados.horas_retencion |
Horas que se guarda un status recibido (1 a 24). | 24 |
No |
Las cinco claves historial.pedido_* son variantes del pedido de historial para probar qué contesta
el teléfono; déjalas en su valor de fábrica salvo que sepas qué buscas. Ver Historial.
Webhooks, streams y bridges
Sección titulada «Webhooks, streams y bridges»| Clave | Para qué | Por omisión | Secreto |
|---|---|---|---|
webhooks.timeout_segundos |
Tiempo de espera de cada intento de entrega (1 a 60 s). | 10 |
No |
webhooks.destinos_permitidos |
Hosts (o host:puerto) a los que cualquier app puede entregar. Cada app suma los suyos. |
[] |
No |
webhooks.ventana_rotacion_minutos |
Al rotar un secreto, el anterior también firma durante este tiempo. | 60 |
No |
streams.conexiones_por_app |
Streams abiertos a la vez por app en este proceso; el siguiente recibe 429. | 5 |
No |
streams.segundos_keepalive |
Cada cuánto se avisa que el stream sigue vivo sin eventos. | 15 |
No |
streams.kb_buffer |
Lo que un stream guarda en memoria esperando al cliente. | 1024 |
No |
streams.segundos_cliente_lento |
Con el buffer lleno más que esto, el stream se corta con cliente_lento. |
10 |
No |
streams.dias_reanudacion |
Un cursor más viejo que esto está vencido (410 con el primer cursor disponible). | 30 |
No |
bridges.timeout_segundos |
Tiempo para conectar y para el ACK de cada publicación. | 10 |
No |
bridges.horas_vida |
Un evento que no sale en este tiempo pasa a la DLQ del bridge. | 72 |
No |
Catálogo comercial
Sección titulada «Catálogo comercial»| Clave | Para qué | Por omisión | Secreto |
|---|---|---|---|
comercio.segundos_cache |
Una consulta igual dentro de este tiempo se responde con lo ya leído. | 60 |
No |
comercio.segundos_cache_vacio |
Lo mismo para un resultado vacío (un error nunca se guarda). | 30 |
No |
comercio.minutos_cursor |
Vida de un next_cursor. |
15 |
No |
comercio.consultas_dia |
Intentos a WhatsApp por app y día (UTC), también los que fallan. | 500 |
No |
comercio.vistas_instancia |
Resultados guardados por instancia; con el cupo lleno, 429. | 100 |
No |
comercio.mb_instancia |
Tamaño guardado por instancia, en MB. | 10 |
No |
comercio.segundos_entre_consultas |
Pausa entre dos consultas de la misma instancia. | 2 |
No |
comercio.segundos_timeout |
Espera de la respuesta de WhatsApp; sin respuesta, 504. | 15 |
No |
comercio.segundos_enfriamiento |
Si WhatsApp limita, la instancia espera esto (se duplica si se repite). | 60 |
No |
comercio.minutos_enfriamiento_maximo |
Tope del enfriamiento. | 15 |
No |
comercio.via_catalogo |
Cómo se pide el catálogo: graphql, mex o iq. |
graphql |
No |
comercio.jid_catalogo |
Cómo va el negocio en el pedido: pn, lid o c_us. |
pn |
No |
comercio.doc_catalogo |
El número de la consulta GraphQL del catálogo (solo dígitos). | el de WhatsApp Web | No |
comercio.doc_colecciones |
El de la consulta de las colecciones. | el de WhatsApp Web | No |
comercio.token_graphql |
El token de la vía graphql; sin él, esa vía no consulta. |
— | Sí |
comercio.variables_catalogo |
La plantilla JSON de las variables del catálogo. | la de WhatsApp Web | No |
comercio.variables_colecciones |
La plantilla JSON de las variables de las colecciones. | la de WhatsApp Web | No |
Ver Catálogo para lo que se pudo comprobar con números reales.
Backups
Sección titulada «Backups»| Clave | Para qué | Por omisión | Secreto |
|---|---|---|---|
backups.activo |
Backup diario. «Hacer un backup ahora» funciona sin activarlo. | false |
No |
backups.hora_utc |
Hora del backup diario (0 a 23, UTC). | 3 |
No |
backups.dias_retencion |
Días que se guarda cada backup en el destino; siempre queda el último completo. | 14 |
No |
backups.destino |
almacenamiento (el MinIO de la multimedia, con su propio bucket) o s3 (otro bucket compatible). |
almacenamiento |
No |
backups.bucket |
El bucket, privado y solo para los backups. | motor-backups |
No |
backups.prefijo |
Prefijo de los objetos. | motor/ |
No |
backups.endpoint |
Endpoint S3 (solo con el destino s3). |
— | No |
backups.region |
Región. | us-east-1 |
No |
backups.access_key |
Clave de acceso. | — | No |
backups.secret_key |
Clave secreta. | — | Sí |
backups.incluir_medios |
Incluir los archivos listos de la multimedia, cifrados igual. | false |
No |
backups.medios_max_mb |
Tope de la multimedia en el backup; lo que no entra se cuenta como omitido. | 1024 |
No |
Proxies, perfil y grupos
Sección titulada «Proxies, perfil y grupos»| Clave | Para qué | Por omisión | Secreto |
|---|---|---|---|
proxy.destinos_permitidos |
Hosts de proxy aceptados. Sin ninguno, no se puede crear un proxy. | [] |
No |
proxy.timeout_segundos |
Tiempo para conectar con el proxy y para «Probar». | 10 |
No |
perfil.forma_info |
Cómo se escribe el «Info» del perfil: 24h, fija, sdk o clasico. |
24h |
No |
grupos.forma_comunidad |
Cómo se arma el pedido de crear una comunidad: minima, web, completa o sdk. |
minima |
No |
Cada proxy (HTTP o HTTPS con CONNECT, o SOCKS5) se crea en la sección Proxies del mismo grupo, con su credencial sellada, y se asigna en la ficha de la instancia.
Operación
Sección titulada «Operación»| Clave | Para qué | Por omisión | Secreto |
|---|---|---|---|
operacion.memoria_mb |
Límite blando de memoria del proceso (GOMEMLIMIT). 0: sin límite. |
0 |
No |
operacion.conexiones_base |
El pool de conexiones a Postgres del proceso (el store de whatsmeow incluido). | 12 |
No |
operacion.operaciones_por_instancia |
Cuántas de las operaciones simultáneas puede ocupar un mismo número. | 2 |
No |
operacion.segundos_trabada |
Una operación más larga que esto marca la instancia como trabada y abre una alerta. | 120 |
No |
La memoria y el pool se aplican al arrancar y cada minuto.
Almacenamiento y multimedia
Sección titulada «Almacenamiento y multimedia»| Clave | Para qué | Por omisión | Secreto |
|---|---|---|---|
almacenamiento.endpoint |
URL interna de MinIO, con http o https. |
— | No |
almacenamiento.region |
Región. | us-east-1 |
No |
almacenamiento.bucket |
Bucket privado y solo del motor (lo crea privado si no existe). | — | No |
almacenamiento.access_key |
Clave de acceso. | — | No |
almacenamiento.secret_key |
Clave secreta. | — | Sí |
multimedia.tamano_maximo_mb |
Tamaño máximo por archivo para subir, importar o descargar. | 100 |
No |
multimedia.base64_maximo_kb |
Un archivo más grande no se entrega en base64. | 5120 |
No |
multimedia.cuota_instancia_mb |
Lo guardado por número. | 2048 |
No |
multimedia.cuota_app_mb |
Cuota por app (de fábrica; cada app puede tener la suya). | 10240 |
No |
multimedia.dias_retencion |
Días hasta borrar un archivo (no lo que espera un envío). | 30 |
No |
multimedia.minutos_permiso |
Vigencia de un enlace temporal de descarga. | 15 |
No |
multimedia.espacio_libre_minimo_mb |
Espacio libre mínimo del disco temporal; por debajo, lo nuevo espera. | 256 |
No |
multimedia.conversiones_simultaneas |
Procesos de ffmpeg a la vez. |
2 |
No |
multimedia.segundos_conversion |
Tiempo máximo por conversión. | 120 |
No |
multimedia.memoria_conversion_mb |
Memoria máxima por conversión. | 1024 |
No |
Las URL de Ajustes no aceptan usuario, contraseña, query ni fragmento: las credenciales van en sus campos.
El backup es diario y cifrado: la base entera (incluidas las tablas del store de whatsmeow, con las sesiones de WhatsApp) y, si lo activas, la multimedia lista. El objetivo es RPO de 24 h y RTO de 1 h. Un RPO cero necesitaría replicación síncrona de Postgres, que queda fuera del motor.
Cómo es un backup
Sección titulada «Cómo es un backup»- Un archivo
.mwbkpor backup, cifrado por bloques con AES-256-GCM y una clave de datos al azar, envuelta por una clave que deriva deAPP_MASTER_KEY. La clave nunca está en la base ni en el destino: el historial guarda solo su huella (16 caracteres hex) para saber con cuál se selló. - El contenido sale de una foto consistente de la base (
REPEATABLE READyCOPY, tabla por tabla). - Lleva un manifiesto (tablas, filas, el SHA-256 de cada tabla) y el checksum del archivo entero.
- Con una clave equivocada,
verificaryrestaurarfallan con «la clave no corresponde a este backup» y las dos huellas. Un archivo cortado o alterado falla con «el backup está dañado o incompleto», nunca con un volcado a medias.
Configurar
Sección titulada «Configurar»- Ajustes → Backups: el destino (el MinIO de la multimedia con un bucket propio, o un bucket S3 compatible, mejor fuera de esta máquina), el bucket, el prefijo y, para S3, el endpoint, la región y las claves.
- Probar comprueba el endpoint, las credenciales y el bucket guardados. Si el bucket no existe todavía, lo crea el primer backup.
- Activa el backup diario y su hora UTC. La retención borra los viejos del destino y siempre deja el último completo.
- Incluir la multimedia es opcional: el texto y el estado se recuperan sin ella.
Hacer, verificar y bajar uno
Sección titulada «Hacer, verificar y bajar uno»| Qué | En el panel | Con motorctl |
|---|---|---|
| Hacer uno ahora | Backups → Hacer un backup ahora (superadmin) | motorctl backup crear (al destino) |
| Hacer uno local | — | motorctl backup crear --archivo /respaldos/AAAA-MM-DD.mwbk (no queda en el historial; imprime su SHA-256) |
| Ver el historial | Backups | motorctl backup listar |
| Verificar | Backups → ⋯ → Verificar | motorctl backup verificar --archivo RUTA |
| Bajarlo | — | motorctl backup descargar --id ID --archivo RUTA |
Solo corre un backup a la vez: otro pedido mientras uno está en curso responde 409.
Verificar es abrir el archivo entero: cada bloque cifrado, cada hash, el manifiesto y el fin real del archivo. Un byte de más, de menos o cambiado lo marca dañado. Un backup verificado queda Verificado con la fecha. Antes de cada actualización hace falta uno de menos de 24 h y verificado (Actualizaciones).
docker compose exec motor /app/motorctl backup crear --archivo /respaldos/2026-10-24.mwbkdocker compose exec motor /app/motorctl backup verificar --archivo /respaldos/2026-10-24.mwbkUn backup que falla abre la alerta backup_fallido (en Inicio y en Operación) y la métrica
motor_backup_fallido vale 1; se cierra sola con el siguiente backup completo. El runbook es
docs/runbooks/backup.md (Runbooks).
Restore aislado
Sección titulada «Restore aislado»El restore va siempre a una base nueva y vacía, y el motor restaurado arranca en modo restaurado: la API y el panel responden y los comandos se aceptan (quedan en cola), pero no abre el socket de ningún número ni manda ninguna entrega. Nunca hay dos motores con los mismos números vivos a la vez: el corte del original es un paso explícito.
Hace falta el archivo .mwbk, la clave maestra que lo selló y un binario de la misma versión o una
más nueva que la del backup.
-
Verifica el archivo con la clave que vas a usar y compara su SHA-256 con el del historial (o con el que imprimió
crear --archivo):Ventana de terminal motorctl backup verificar --archivo /respaldos/b.mwbk -
Crea una base vacía y apunta
DATABASE_URLa ella. El resto del entorno, igual. -
Restaura. Si la clave no es la
APP_MASTER_KEYdel entorno, pásala con--clave-stdin(una línea, sin eco), nunca como argumento:Ventana de terminal motorctl backup restaurar --archivo /respaldos/b.mwbkmotorctl backup restaurar --archivo /respaldos/b.mwbk --clave-stdinmotorctl backup restaurar --archivo /respaldos/b.mwbk --subir-mediosCrea el esquema, carga las filas, migra hasta la versión del binario (las tablas propias y el store de whatsmeow) y deja el modo restaurado. Se niega con el motivo si la base no está vacía o el backup es de un esquema más nuevo. Con
--subir-medios, la multimedia se sube después de las tablas, archivo por archivo (5 minutos cada uno;--plazo-medio 15mlo cambia). Si alguno falla, repite solo eso con--solo-medios, que no toca ninguna tabla. -
Arranca el motor. Inicio y Operación muestran «Modo restaurado» y
motorctl mantenimiento estadodicemodo: restauradoy de qué backup viene. -
Revisa lo restaurado: instancias, apps, suscripciones y Ajustes. Si era un simulacro, apaga y borra la base nueva aquí.
-
El corte del original. Para el motor original y confirma que no corre (sin el proceso y sin respuesta en su
/health/live). Después, en el restaurado:Ventana de terminal motorctl mantenimiento salir --confirmo-corte-del-originalEn la próxima vuelta (hasta 15 s) toma las sesiones y reanuda las entregas pendientes. La salida queda en la auditoría.
El procedimiento completo, con los síntomas de un restore que salió mal, está en
docs/runbooks/restore.md del repositorio.