Saltar a contenido

⚠ Vigencia por confirmar. Plan de implementación no ejecutado en las ramas publicadas: al 2026-08-19 medusa-config.ts en develop sigue sin workerMode (equivale a shared) y docker-compose.yml sigue teniendo un único servicio medusa.

Plan: Split SERVER + WORKER de Medusa (+ diseño Fase 2 réplicas)

Documento de implementación. Reemplaza la versión previa de este plan (que se escribió antes de conocer el pipeline real de despliegue). Estado base: rama develop, Medusa 2.15.5.

Contexto / por qué

La API de Medusa en producción se vuelve lenta durante el resync masivo del integrador externo (<IP del integrador>) porque corre en workerMode: "shared" (un solo proceso Node, single-thread) que satura 1 core mientras el host (2 cores) tiene capacidad ociosa. Medido: CPU ~100–124% en 1 core, /health saltando de ~8ms a cientos de ms, Postgres/Redis ociosos.

El split server/worker es el paso habilitador hacia la solución real (múltiples réplicas de server). Importante / expectativa honesta: por sí solo da poco beneficio directo hoy, porque el background está casi vacío (1 subscriber que solo hace console.log en src/subscribers/product-created.ts, 0 jobs, 0 workflows custom) → el worker quedará casi ocioso. Su valor: - Resiliencia/aislamiento: el procesamiento async (eventos/jobs) deja de competir por CPU con el HTTP; un crash del worker no tumba el server. - Prerrequisito para escalar el server a N réplicas (la verdadera solución al pico): en shared no se puede replicar porque cada réplica correría también jobs/eventos → duplicados.

Recordatorio clave: el POST /admin/products/{id} del integrador ejecuta su workflow de forma síncrona en el proceso server (es HTTP), no en el worker. Por eso el split no mueve esa carga; las palancas que sí atacan el pico son throttle del integrador (equipo externo) y réplicas de server (Fase 2).

Decisiones tomadas

  • Alcance: Fase 1 split (implementar) + Fase 2 réplicas (diseñar; gated por infraestructura).
  • Migraciones: one-shot en deploy.sh (no en el arranque de los contenedores).
  • Flujo de ramas: cambios en develop → merge a staging (dispara pipeline staging) → main (prod con aprobación).

Estado actual relevante (verificado)

  • medusa-config.ts: sin workerMode (corre shared). Módulos Redis listos: cache-redis, event-bus-redis, workflow-engine-redis, locking-redis. Payment → ./src/modules/bank-transfer.
  • Dockerfile: multi-stage, CMD ["./start.sh"].
  • start.sh (horneado como CMD): db:migrate → seed → medusa user ... → npm run start.
  • docker-compose.yml: servicios postgres, redis, medusa (image: ghcr.io/compulandiati/ecommercev2backend:${MEDUSA_TAG:-latest}, container_name: medusa_backend, puerto 9000).
  • deploy.sh (versionado, corre en /opt/medusa): backup pg_dump → tag rollback-prev → compose pull/up solo medusa → health :9000/health → auto-rollback.
  • .github/workflows/build-deploy.yml: push staging→build+push GHCR→deploy-staging (runner self-hosted) ./deploy.sh; push main→deploy-prod con aprobación.

Hechos de Medusa (doc oficial)

  • workerMode vive en projectConfig: "shared"|"server"|"worker".
  • En modo worker la instancia no sirve HTTP ni admin → no hay /health en el worker.
  • Admin se apaga con admin.disable.
  • Migraciones son un paso aparte (medusa db:migrate), a correr una vez antes de levantar server+worker.
  • En worker mode no mapear puerto 9000 (evita EADDRINUSE; ref issue medusajs/medusa#10378).

FASE 1 — Split server/worker (implementación)

1. medusa-config.ts

workerMode env-driven en projectConfig + admin.disable env-driven. Default "shared" preserva dev/local.

admin: {
  disable: process.env.DISABLE_MEDUSA_ADMIN === "true",
  vite: () => ({ server: { allowedHosts: ["dev-medusa.compulandia.com.py"] } }),
},
projectConfig: {
  workerMode: (process.env.MEDUSA_WORKER_MODE as "shared" | "server" | "worker") || "shared",
  // ...resto sin cambios
},

2. start.sh — arranque puro (sacar DB ops)

Las migraciones pasan a deploy.sh; el seed se elimina de producción; la creación de admin se vuelve manual (§5).

#!/bin/sh
set -e
echo "Starting Medusa in mode: ${MEDUSA_WORKER_MODE:-shared}"
exec npm run start
exec para que SIGTERM llegue a Node (shutdown limpio).

3. docker-compose.yml — dos servicios, misma imagen

Reemplazar medusa por medusa-server + medusa-worker (mismo image, mismo env_file, depends_on: [postgres, redis]): - medusa-server: container_name: medusa_backend (mantener → menos cambios en deploy.sh), MEDUSA_WORKER_MODE=server, DISABLE_MEDUSA_ADMIN=false, ports: ["9000:9000"], límites cpus: "1.5" / memory: 2560M. - medusa-worker: container_name: medusa_worker, MEDUSA_WORKER_MODE=worker, DISABLE_MEDUSA_ADMIN=true, sin ports, límites cpus: "0.75" / memory: 1024M. - Confirmar sintaxis de límites según versión de docker compose en los runners (deploy.resources.limits vs cpus/mem_limit legacy).

4. deploy.sh — migración one-shot + doble servicio + health diferenciado

  • Vars: SERVER_SERVICE=medusa-server, WORKER_SERVICE=medusa-worker, SERVER_CONTAINER=medusa_backend, WORKER_CONTAINER=medusa_worker.
  • Backup pg_dump: igual.
  • Rollback tag: inspeccionar medusa_backend → rollback-prev (ambos usan el mismo tag).
  • NUEVO — migración one-shot antes de levantar: compose pull medusa-server → compose run --rm -e MEDUSA_WORKER_MODE=server medusa-server npx medusa db:migrate (set -e aborta si falla; backup ya hecho).
  • Up: compose pull medusa-worker; compose up -d medusa-server medusa-worker.
  • Health:
  • Server: igual (curl :9000/health con reintentos).
  • Worker (sin /health): docker inspect -f '{{.State.Status}} {{.RestartCount}}' medusa_worker → running y sin restart loop.
  • Auto-rollback: si falla server o worker → compose up -d ambos con MEDUSA_TAG=rollback-prev y re-verificar. El pg_dump es la red real (rollback de código vs schema ya migrado).

5. Creación de admin

Sacarla del arranque. Manual one-shot cuando se necesite: docker compose run --rm medusa-server npx medusa user -e <email> -p <pass>.

6. .github/workflows/build-deploy.yml

Sin cambios funcionales (una sola imagen para ambos servicios; la lógica nueva vive en deploy.sh). Opcional: agregar docker ps de ambos contenedores al step de resumen.

Archivos a tocar (Fase 1)

medusa-config.ts · start.sh · docker-compose.yml · deploy.sh · (build-deploy.yml solo cosmético).


FASE 2 — Réplicas de server (diseño; gated por infraestructura)

Objetivo: N réplicas de medusa-server detrás de un reverse proxy; worker sigue uno. Así el burst del integrador y el tráfico de tienda se reparten entre cores/procesos.

La Fase 1 ya deja casi todo listo: worker = único consumidor async (replicar server no duplica jobs), migraciones one-shot (sin carrera con N réplicas), HTTP/admin por env.

Cambios adicionales de Fase 2: - Reverse proxy (Nginx o Traefik) como servicio nuevo, balanceando a las réplicas en 9000; el proxy expone el puerto público y las réplicas dejan de mapear 9000 al host. - Quitar container_name fijo del server (incompatible con replicas > 1); deploy.sh deja de referenciar medusa_backend por nombre y health-checkea vía el proxy; rollback por imagen del servicio, no por contenedor nombrado. - Rate-limit del integrador en el proxy (opcional, alto valor).

⚠️ Restricción dura: con 2 cores y Postgres+Redis en el mismo host, las réplicas reales no ayudan (competirían por los mismos 2 cores). Fase 2 está gated por: - mover Postgres y/o Redis a otro host, y/o - aumentar cores del host del server.

Por eso Fase 2 = diseño ahora, implementación cuando la infra lo permita. Palanca de mayor impacto inmediato: throttle del integrador (independiente del split).


Verificación en staging (Fase 1)

Flujo: commit en develop → merge a staging → pipeline corre deploy.sh en runner staging. 1. Migración una sola vez: logs del job muestran el run --rm con db:migrate; NO hay migraciones al arrancar medusa_backend/medusa_worker. 2. Admin solo en server: /app responde en server; medusa_worker no abre 9000. 3. Eventos sin duplicar (en worker): crear un producto → product-created.ts loguea 1 vez y en medusa_worker: docker logs medusa_worker | grep -i product = 1; en medusa_backend = 0. 4. Smoke server: /health 200, /store/products ok, login admin ok. 5. Worker estable: docker inspect -f '{{.State.Status}} {{.RestartCount}}' medusa_worker → running, sin restart loop tras 1–2 min. 6. Rollback drill (opcional): forzar health-fail (tag inexistente) y confirmar re-levantado con rollback-prev.

Solo tras pasar staging → merge staging→main y aprobar el deploy de prod.


Decisiones abiertas menores (defaults aplicados)

  • start.sh único (no scripts separados) — simplicidad.
  • container_name: medusa_backend para el server en Fase 1 — minimiza cambios en deploy.sh (en Fase 2 hay que quitarlo para replicar).
  • Admin creation manual/one-shot.
  • Confirmar versión de docker compose en runners (sintaxis de límites CPU/mem).
  • (Fase 2) Nginx vs Traefik; restore automático de pg_dump si falla la migración one-shot.