Saltar a contenido

Runbook — EcommerceV2Backend

Se lee con el sistema caído. Comandos copiables; la teoría está en arquitectura.md. El catálogo largo de errores puntuales está en troubleshooting.md.

Dónde corre

Ambiente Runner (label) Directorio del proyecto Tag de imagen
Staging [self-hosted, staging] /opt/medusa staging-<sha corto>
Producción [self-hosted, ecommerce-srv-production] /opt/medusa v<semver> (ej. v1.3.0)

En /opt/medusa el workflow escribe docker-compose.yml, deploy.sh y el .env en cada deploy. El .env es generado desde GCP Secret Manager (medusa-staging-env / medusa-prod-env): editarlo por SSH no sobrevive al próximo despliegue (ADR-0005). Para cambiar un valor: gcloud secrets versions add <secret> --data-file=.env y redesplegar.

Contenedores en ambos: medusa_backend, medusa_postgres, medusa_redis (definidos en docker-compose.yml).

Deploy

Staging — automático

git checkout staging
git merge develop
git push origin staging     # dispara build + deploy-staging

El workflow build-deploy.yml («Imagen») construye la imagen en ubuntu-latest con las variables públicas (VITE_*) y GIT_SHA del secret medusa-staging-env, la publica en GHCR (:staging-<sha corto>), y el job deploy-staging corre en el runner de staging: copia compose y deploy.sh a /opt/medusa, escribe el .env desde el secret y ejecuta ./deploy.sh. Al final verifica contra GET /version que quedó corriendo el commit recién desplegado.

Producción — por tag v* (ADR-0004)

git checkout main
git merge staging
git push origin main        # no dispara nada: main no construye ni despliega
git tag v1.3.0              # semver con prefijo v, sobre main
git push origin v1.3.0      # dispara build + deploy-prod

El Environment production solo acepta refs v* (tag policy) y el job de build verifica con git merge-base --is-ancestor que el tag esté sobre staging o sobre main (estándar 2026-08-28): un tag sobre otra rama falla con mensaje claro y sin construir imagen. El deploy es igual al de staging (secret medusa-prod-env, /opt/medusa, ./deploy.sh, verificación de SHA).

La base de datos corre en un servidor dedicado (DATABASE_URL del secret); el servicio postgres del compose queda como respaldo temporal y para desarrollo local, sin depends_on desde el backend. El backup previo de deploy.sh decide solo: si el DATABASE_URL apunta al servicio postgres del compose usa el contenedor; si apunta a otro host, hace pg_dump remoto.

Cutover de producción al pipeline (primera vez, una sola vez)

El despliegue viejo de producción corre desde otro directorio y con contenedores de un proyecto de compose distinto. Como los container_name son fijos, el primer deploy desde /opt/medusa choca con los contenedores viejos: hay que bajarlos antes. Con la base ya externa, el stack es sin estado y no hay volúmenes que migrar.

En el servidor de producción, antes del primer tag:

# 1. Directorio de operación, dueño = usuario del runner (dueño de /opt/github-runner*/_work)
sudo install -d -o <usuario_del_runner> /opt/medusa

# 2. Conectividad hacia la base dedicada (host y puerto REALES del DATABASE_URL)
timeout 5 bash -c '</dev/tcp/<host-db>/<puerto>' && echo ALCANZABLE || echo NO

# 3. En la ventana de despliegue: bajar el stack viejo (SIN -v; el volumen queda de respaldo)
cd <directorio viejo> && docker compose down

Después: git tag vX.Y.Z && git push origin vX.Y.Z. Consideraciones:

  • El secret medusa-prod-env debe incluir, además del DATABASE_URL dedicado, las variables POSTGRES_USER / POSTGRES_PASSWORD / DB_NAME: el compose las interpola al parsear el archivo aunque el servicio postgres no arranque (sirven además para resucitar el respaldo).
  • El primer deploy dirá «no hay contenedor previo; sin rollback disponible»: es normal (el stack viejo está abajo). El camino de vuelta durante la ventana es docker compose up -d en el directorio viejo.
  • Los backups históricos de deploy.sh quedaron en <directorio viejo>/backups/; los nuevos van a /opt/medusa/backups/.

Qué hace deploy.sh

  1. pg_dump de la base al directorio ./backups/ (aborta el deploy si el dump falla).
  2. Etiqueta la imagen que está corriendo como …:rollback-prev (known-good).
  3. docker compose pull medusa + up -d medusa con el tag objetivo.
  4. Health check: hasta 30 intentos cada 3 s (~90 s) contra http://localhost:9000/health.
  5. Si el health falla → vuelve a levantar rollback-prev y re-verifica.

Verificar que el deploy salió bien

curl -fsS http://localhost:9000/health          # debe responder 200
docker compose ps                               # 3 contenedores en "Up"
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"   # confirmar el tag desplegado
docker logs medusa_backend --tail 50

Logs

Qué Dónde Cómo verlos
Aplicación contenedor medusa_backend docker logs -f medusa_backend
Migraciones mismo contenedor, al arrancar docker logs medusa_backend \| grep -i migration
Base de datos contenedor medusa_postgres docker logs medusa_postgres --tail 50
Runner de GitHub Actions systemd del servidor sudo journalctl -u actions-runner -f
Build y deploy GitHub Actions → workflow run → job build / deploy-*
Backups de base disco del servidor ls -lh ./backups/

Rollback

Antes de revertir: el rollback de código a una versión anterior a un cambio de schema exige restaurar también la base. Las migraciones corren solas al arrancar el contenedor (start.sh → medusa db:migrate), así que el pg_dump que hace deploy.sh es la red real.

Automático

deploy.sh revierte solo si el health check no pasa en ~90 s: re-levanta la imagen etiquetada rollback-prev y vuelve a verificar. En los logs: ↩️ Auto-rollback a la imagen anterior. Si el rollback tampoco pasa el health, requiere intervención manual.

Manual

Rollback rápido (recomendado, estándar 2026-08-28) — la imagen del tag anterior ya está en GHCR; volver a ella es un pull, del orden de segundos, y usa el .env que está en el host:

cd /opt/medusa
MEDUSA_TAG=v1.2.4 ./deploy.sh          # o staging-abc1234; ver GitHub → Packages
docker logs -f medusa_backend

Desde Actions («Re-run all jobs» de la corrida del tag): funciona, pero reconstruye la imagen y lee la versión actual del secret — si la configuración cambió desde entonces, se obtiene código viejo con configuración nueva, que no es un rollback. Usarlo solo cuando eso es lo que se busca (por ejemplo, aplicar un cambio de secret sobre la versión anterior).

Restaurar desde un backup

docker compose down
zcat ./backups/medusa-store-YYYYMMDD-HHMMSS.sql.gz | docker exec -i medusa_postgres psql -U <usuario> <base>
docker compose up -d
curl -fsS http://localhost:9000/health

Fallas frecuentes

Las tres con evidencia en el historial del repo. El resto está en troubleshooting.md.

1. El runner no puede autenticarse contra GHCR

Síntoma: el job de deploy falla en el paso Login to GHCR o en docker compose pull, con unauthorized: authentication required o denied.

  • Evidencia: da4e8f1 docker login en GitHub Actions sin TTY, 84cc5db base64 encoding en docker config.json, 3e4c505 generar token de GitHub App en cada deploy job, 14efeaa usar PAT en lugar de GitHub App para autenticación a GHCR (todos 2026-06-17).
  • Diagnóstico: en el servidor, docker pull ghcr.io/compulandiati/ecommercev2backend:<tag> como el usuario del runner.
  • Resolución: desde 2026-08 el login usa el GITHUB_TOKEN del propio job (no hay PAT que venza); el job necesita permissions: packages: read (deploy) o write (build). El login se guarda en ~/.docker/config.json del usuario que corre el runner, no de root. El secret GHCR_PAT quedó obsoleto (ADR-0005).

2. Nombre de imagen con mayúsculas → GHCR rechaza el push

Síntoma: el job build falla al publicar; GHCR solo acepta nombres en minúscula y el repo se llama CompulandiaTI/EcommerceV2Backend.

  • Evidencia: 7c32137 normalizar nombre del repositorio a minúsculas para GHCR y 7312888 normalizar nombre del repositorio en un step (no en env) (2026-06-17).
  • Diagnóstico: revisar el step Set image name (lowercase) del workflow.
  • Resolución: el nombre debe salir de ese step (tr '[:upper:]' '[:lower:]'), nunca de ${{ github.repository }} directo ni de un env: (los env: no se evalúan en ese contexto).

3. El deploy falla con Permission denied sobre /opt/medusa

Síntoma: el paso «Preparar /opt/medusa» falla con cannot create directory / Permission denied.

  • Contexto: desde 2026-08 ambos ambientes operan desde /opt/medusa (antes producción usaba /home/hquintero/ecommerce/backend; evidencia del problema viejo: a449a1e). /opt es de root: el directorio debe existir de antemano con dueño = usuario del runner.
  • Resolución: en el servidor, sudo mkdir -p /opt/medusa && sudo chown <usuario_del_runner>: /opt/medusa (el usuario correcto es el dueño de /opt/github-runner*/_work).

Otras fallas registradas en el historial

  • npm ci vs npm install en el build de la imagen: 4917f47, 89c57f0, 60b497d (2026-06-17) — terminó revertido a npm install --prefer-offline --no-audit.
  • SSH keys del runner en el directorio equivocado: a571a7f (2026-06-18). Ver la contradicción señalada en deploy-automatizado-setup.md.

Tareas frecuentes

# Migraciones a mano (normalmente corren solas al arrancar el contenedor)
docker exec medusa_backend npx medusa db:migrate

# Crear un usuario admin
docker exec medusa_backend npx medusa user -e <email> -p '<password>'

# Backup manual
docker exec medusa_postgres pg_dump -U <usuario> <base> | gzip > ./backups/manual-$(date +%Y%m%d-%H%M%S).sql.gz

# Estado del runner
sudo systemctl status actions-runner

Intentos de pago (Bancard QR y tarjeta)

Desde ADR-0006 el pedido se crea recién cuando Bancard confirma el pago. Lo que no se confirma se ve en el admin, en Intentos de pago (barra lateral), por defecto los no confirmados. Cada mañana llega a SALES_DEPARTMENT_EMAIL un resumen con alertas.

Estado Qué significa Qué hacer
Pendiente / Iniciado El cliente está pagando (QR: 5 min; tarjeta: 10 min) Nada. Si quedó trabado más tiempo, el job de conciliación lo vence solo; "Revertir" lo cierra a mano
Rechazado / Fallido La pasarela rechazó o no pudo iniciar el pago Contactar al cliente si tiene valor; el carrito quedó editable
Vencido / Revertido Se venció o se canceló; el stock se liberó Nada
Pagado sin pedido Bancard cobró pero el pedido no se pudo crear (stock, carrito cambiado) Urgente. "Crear el pedido igual" (stock comprometido, reponer después) o "Cerrar sin pedido" y devolver por el portal de Bancard
Devolución pendiente Hay que devolver el dinero a mano Anular en el portal de comercios de Bancard (mismo día) y avisar al cliente

Reembolsos con Bancard: solo se puede devolver el total y solo el mismo día, antes de que la transacción esté cuponada. Si el admin avisa que Bancard no pudo revertir, hay que anular la operación en el portal de comercios (Soporte → Anulaciones) y registrar el reembolso en Medusa cuando esté confirmada; hasta entonces el pedido no muestra reembolso, que es lo correcto.

Alertas en el log (para Grafana/Loki): [bancard-vpos] ALERTA: monitoreo de Bancard sin señal (la URL de confirmación no recibe el POST vacío de Bancard: revisar túnel y WAF), hay intentos pagados sin pedido, Reversión manual pendiente, token inválido, [payment-attempts] resumen diario … ALERTAS. Detalle en RF-010.

Conciliación de intentos sin confirmación

Si Bancard no avisa (corte de red, backend caído en ese momento), el intento no queda colgado: un job lo cierra solo.

Job Cada Qué hace
bancard-vpos-reconcile 2 min Consulta a Bancard (single_buy/confirmations) los intentos pendientes más viejos que BANCARD_VPOS_CONFIRMATION_TIMEOUT_MIN (10 por defecto): si hubo pago crea el pedido, si no revierte y libera el stock
bancard-qr-reconcile 2 min El QR no tiene consulta de estado: vence y revierte los más viejos que BANCARD_QR_TIMEOUT_MS
# Ver qué hizo la conciliación
docker logs medusa_backend --tail 500 | grep -i "reconcile\|bancard-vpos\]"

Un intento que sigue pendiente pasado el tiempo de espera y con la conciliación corriendo indica que Bancard no responde: revisar el log por NetworkError o timeout. El botón Consultar en la pasarela del admin hace la misma consulta a pedido, para un intento puntual.

Rotación de claves de Bancard

Las claves (pública y privada) se piden en el portal de comercios y viven en Secret Manager (ADR-0005). La privada solo se usa para calcular y verificar los tokens md5.

  1. Elegir una ventana sin pagos en curso: un intento abierto con la clave anterior no valida su confirmación y termina como pago sin pedido. Confirmar en el admin, en Intentos de pago, que no haya pendientes.
  2. Cargar los valores nuevos en Secret Manager (BANCARD_VPOS_PUBLIC_KEY, BANCARD_VPOS_PRIVATE_KEY; para el QR, BANCARD_QR_PUBLIC_KEY y BANCARD_QR_PRIVATE_KEY).
  3. Redesplegar para que el .env se regenere; el proceso las lee al arrancar.
  4. Pago de control por el monto mínimo y reversión el mismo día.
  5. Si algo falla, volver a la clave anterior (sigue siendo válida hasta que Bancard la dé de baja) y repetir.

Señales de clave equivocada: InvalidPublicKeyError o PublicKeyNotFoundError al abrir el pago, y token inválido en el log del webhook cuando la privada no coincide.

Chequeo diario / post-deploy

  • [ ] curl -fsS http://localhost:9000/health responde 200.
  • [ ] docker compose ps muestra medusa_backend, medusa_postgres y medusa_redis en Up.
  • [ ] docker logs medusa_backend --tail 200 | grep -i error sin errores nuevos.
  • [ ] El admin carga y permite login.
  • [ ] El tag desplegado es el esperado (docker ps --format "{{.Names}} {{.Image}}").