Desplegar Simmetric Chat con Docker: guía completa
El mapa completo del despliegue de Simmetric Chat - un stack Compose de seis servicios, un contenedor all-in-one único para hosts air-gapped y la ruta Coolify en un clic.
Un producto, tres formas de despliegue
Simmetric Chat es una plataforma multiservicio - un servidor API, un collector para el procesamiento de documentos, un servicio de widget embebible, un frontend y una base de datos PostgreSQL - así que la historia del despliegue consiste en elegir el empaquetado correcto para tu entorno, no en cablear servicios a mano. Existen tres vías de producción, y todos los artefactos viven en el directorio docker/ del repositorio:
- Compose multicontenedor - seis servicios, cada uno en su propio contenedor con healthcheck. El layout de producción recomendado, con aislamiento por servicio y reinicios independientes.
- Contenedor único all-in-one - una sola imagen que ejecuta todo: Postgres, servidor, collector y widget supervisados por
supervisord. Pensada para hosts air-gapped y máquinas de demo, donde mover varias imágenes a través del perímetro es la parte cara. - Coolify - una vía PaaS self-hosted con terminación TLS de Traefik, en la que la plataforma se clona y se compila por ti a partir de un archivo Compose ajustado a las convenciones de Coolify.
Todas las imágenes son builds multi-stage sobre bases Node 24, y cada servicio expone un healthcheck del que depende el orden de arranque del stack. Este artículo recorre cada vía, el modelo de configuración y los detalles que muerden en producción.
El stack Compose de seis servicios
El archivo Compose de producción define seis servicios activos:
- frontend - construido como app React 19 desde un builder Node, luego servido por nginx, que termina TLS con certificados autofirmados generados en el build de la imagen, sirve la SPA y hace proxy de
/apiy/widgethacia los servicios backend. - server - la API Express 5: autenticación, RBAC, orquestación de chats y el agente ReAct. Es el servicio con el que hablan tus usuarios.
- collector - el pipeline documental: parsing, chunking y embedding de las subidas. Construido sobre una base Node slim en lugar de Alpine, porque sus dependencias nativas (LanceDB, los transformers de embedding locales, los bindings de canvas) requieren glibc.
- widget - el servicio del widget de chat embebible para sitios de cara al exterior.
- postgres - PostgreSQL 16 con la extensión pgvector incluida, para que el provider vectorial por defecto no requiera un servicio de base vectorial separado.
- redis - la caché opcional de escalado, habilitada con persistencia append-only y eviction LRU. Cada consumidor de Redis degrada con gracia en memoria, así que este servicio puede estar dentro o fuera sin problemas.
Dos servicios están deliberadamente deshabilitados en el archivo estándar en lugar de ausentes: ollama y qdrant. Sus bloques llegan comentados, y habilitarlos consiste en descomentar una línea más una variable - OLLAMA_BASE_URL=http://ollama:11434 para el host de los modelos locales, o VECTOR_DB_PROVIDER=qdrant si quieres un store vectorial remoto en lugar del LanceDB sobre archivos por defecto. Los valores comentados por defecto mantienen una instalación mínima realmente mínima.
El orden de arranque lo gobiernan los healthchecks: sondas basadas en wget golpean GET /api/health en servidor y collector, /health en el widget, pg_isready en Postgres y redis-cli ping en Redis, y el frontend espera a que el servidor se declare healthy antes de arrancar.
Levantar el stack es un solo comando:
docker compose -f docker/docker-compose.yml up --build -dEl contenedor único all-in-one
La segunda vía comprime toda la plataforma en una sola imagen. Un único Dockerfile compila PostgreSQL 16 con la extensión pgvector compilada desde el código fuente en el build de la imagen, y supervisord ejecuta juntos, dentro del contenedor, base de datos, servidor, collector y widget. Los puertos 3000, 3210 y 3211 están expuestos, y el healthcheck consulta /api/health del servidor en localhost.
docker build -f docker/Dockerfile -t simmetric-chat-all-in-one .
docker run -d -p 3000:3000 -p 3210:3210 -p 3211:3211 simmetric-chat-all-in-oneLa imagen all-in-one es la historia del air-gap: un solo artefacto que hacer cruzar el perímetro, ningún pull de registry en el momento de la instalación, y todo - de la base de datos a la UI de chat - arrancando con un solo docker run. Las fuentes de pgvector se compilan e instalan offline durante el build precisamente para que la imagen resultante no tenga dependencias de red en la instalación. Sus valores de entorno por defecto son placeholders de andamiaje por diseño - el operador sobrescribe los secretos reales vía .env o -e, y la imagen nunca finge ser production-safe recién salida de la caja.
Hay una frontera que vale la pena conocer: el plugin enterprise nunca se hornea dentro de ninguna imagen. Es deliberado - mantiene las imágenes AGPL de la comunidad libres de código propietario y permite a los clientes air-gapped recibir el plugin como tarball separado y verificable, montado por bind en tiempo de ejecución.
La vía Coolify
Coolify convierte el stack en un despliegue PaaS: clona el repositorio, compila las imágenes, cablea la red interna y pone Traefik delante para la terminación TLS. Un archivo Compose dedicado automatiza lo que el Compose estándar deja al operador - builds de imágenes, networking interno, volúmenes, healthchecks y el montaje del plugin enterprise.
También aplica los secretos requeridos en el momento del despliegue con la sintaxis :?: JWT_SECRET, COLLECTOR_SECRET, WIDGET_API_KEY, POSTGRES_PASSWORD, APP_URL y ALLOWED_ORIGINS. Coolify se niega a desplegar cuando falta uno de ellos, lo que convierte una clase de despliegues a medio configurar en un error visible antes de que arranque nada.
Las diferencias con el stack estándar derivan de la presencia de Traefik: los servicios de aplicación no publican puertos en el host (asigna el dominio al servicio frontend, puerto 80), la configuración de nginx es solo HTTP porque Traefik termina TLS, y el socket Docker no se monta por defecto - solo hace falta para el flujo de login de Ollama Cloud. Un runbook paso a paso cubre los pasos manuales: rsync del árbol enterprise, relleno de secretos e inserción de la clave de licencia.
Configuración: un solo .env, montado por Compose
Los contenedores reciben la configuración vía el env_file de Compose, que apunta al .env en la raíz del repositorio - el mismo archivo único de configuración runtime que la plataforma usa en todas partes. El stack arranca con los solos defaults a nivel compose si el archivo falta: suficiente para un primer smoke test, no para producción.
Dos secretos son estrictamente requeridos, y el servidor sale sin ellos:
# .env en la raíz - requeridos en producción
JWT_SECRET=... # openssl rand -hex 32
COLLECTOR_SECRET=... # openssl rand -hex 32ENCRYPTION_KEY y API_KEY_HMAC_SECRET no necesitan preaprovisionarse en absoluto: en los arranques de producción, el entrypoint del servidor los genera una vez, los persiste en el volumen server-storage, los restaura en cada reinicio, y prefiere un valor aportado por el operador cuando existe. El provisioning nunca regenera sobre material de clave existente y falla en voz alta antes de cualquier paso sobre la base de datos, para que un volumen a medio escribir no pueda convertirse en silencio en una clave distinta.
Hay una trampa específica de Compose que vale la pena nombrar porque degrada despliegues en silencio: no añadas entradas de passthrough como LICENSE_KEY=${LICENSE_KEY:-} a los servicios de producción. La interpolación del shell resuelve una variable no definida a la cadena vacía, y una entrada environment explícitamente vacía sobrescribe el valor que llega vía env_file - así que un LICENSE_KEY interpolado vacío convierte un despliegue Enterprise en Community, y un ENCRYPTION_KEY interpolado vacío sobrescribe el auto-provisioning del entrypoint. Deja esas claves solo al env_file.
Los detalles que importan en producción
- El collector requiere glibc. El collector corre sobre una base slim (Debian), no Alpine, porque LanceDB, los transformers de embedding locales y canvas requieren todos glibc. No «simplifiques» a Alpine - el build falla.
- Las URL de base de datos tienen tres formas. Dentro de la red Compose el servidor habla con
postgres:5432(cableado en el environment del compose); el default del.envde la raíz apunta ahost.docker.internalpara servidores en el lado host; la imagen all-in-one usalocalhostporque Postgres corre en el mismo contenedor. - Las migraciones son solo aditivas. El entrypoint del servidor ejecuta
prisma migrate deployen cada arranque, así que hacer rodar las imágenes hacia delante es seguro por política; volver atrás significa redesplegar una imagen conocida como buena, y el rollback de código no restaura las migraciones aplicadas. - El escalado espera al provisioning. El auto-provisioning de secretos debe completarse una vez antes de que el servidor escale más allá de una réplica - réplicas en el primer arranque compitiendo podrían generar claves divergentes. La instancia única es el default documentado; el escalado multi-instancia tiene su propia guía dedicada.
- Los endpoints de salud son externos.
/api/healthdevuelve un objeto de estado con comprobaciones por dependencia y sirve para sondas de uptime externas y verificaciones de load balancers; los logs estructurados van a stdout paradocker compose logs -f <servicio>.
Las imágenes publicadas se publican en GHCR (server, frontend, collector, widget y all-in-one, etiquetadas latest más la versión), así que un despliegue puede fijar una versión en lugar de compilar en local - aunque docker compose up --build desde el repositorio sigue siendo la vía completamente self-hosted.
Del clon a la plataforma en marcha
El camino más corto, de principio a fin:
- Clona el repositorio y copia
.env.examplea.env. - Define
JWT_SECRETyCOLLECTOR_SECRET(el entrypoint provisiona por sí mismo las claves de cifrado). docker compose -f docker/docker-compose.yml up --build -d.- Sigue
docker compose logs -f serverhasta que terminen migraciones y seeding, luego abre el frontend y accede.
Para hosts air-gapped, sustituye la imagen all-in-one y el flujo de transferencia por tarball. Para Coolify, importa el repositorio y rellena los secretos requeridos - la plataforma hace el resto del despliegue.

