⚠ Vigencia por confirmar. Plan de implementación no ejecutado en las ramas publicadas: al 2026-08-19
medusa-config.tsendevelopsigue sinworkerMode(equivale ashared) ydocker-compose.ymlsigue teniendo un único serviciomedusa.
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 astaging(dispara pipeline staging) →main(prod con aprobación).
Estado actual relevante (verificado)¶
medusa-config.ts: sinworkerMode(correshared). 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: serviciospostgres,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 → tagrollback-prev→compose pull/upsolomedusa→ health:9000/health→ auto-rollback..github/workflows/build-deploy.yml: pushstaging→build+push GHCR→deploy-staging (runner self-hosted)./deploy.sh; pushmain→deploy-prod con aprobación.
Hechos de Medusa (doc oficial)¶
workerModevive enprojectConfig:"shared"|"server"|"worker".- En modo
workerla instancia no sirve HTTP ni admin → no hay/healthen 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 -eaborta si falla; backup ya hecho). - Up:
compose pull medusa-worker; compose up -d medusa-server medusa-worker. - Health:
- Server: igual (
curl :9000/healthcon reintentos). - Worker (sin
/health):docker inspect -f '{{.State.Status}} {{.RestartCount}}' medusa_worker→runningy sin restart loop. - Auto-rollback: si falla server o worker →
compose up -dambos conMEDUSA_TAG=rollback-prevy 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_backendpara 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 composeen 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.