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-envdebe incluir, además delDATABASE_URLdedicado, las variablesPOSTGRES_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 -den el directorio viejo. - Los backups históricos de
deploy.shquedaron en<directorio viejo>/backups/; los nuevos van a/opt/medusa/backups/.
Qué hace deploy.sh¶
pg_dumpde la base al directorio./backups/(aborta el deploy si el dump falla).- Etiqueta la imagen que está corriendo como
…:rollback-prev(known-good). docker compose pull medusa+up -d medusacon el tag objetivo.- Health check: hasta 30 intentos cada 3 s (~90 s) contra
http://localhost:9000/health. - Si el health falla → vuelve a levantar
rollback-prevy 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:
da4e8f1docker login en GitHub Actions sin TTY,84cc5dbbase64 encoding en docker config.json,3e4c505generar token de GitHub App en cada deploy job,14efeaausar 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_TOKENdel propio job (no hay PAT que venza); el job necesitapermissions: packages: read(deploy) owrite(build). El login se guarda en~/.docker/config.jsondel usuario que corre el runner, no de root. El secretGHCR_PATquedó 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:
7c32137normalizar nombre del repositorio a minúsculas para GHCR y7312888normalizar 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 unenv:(losenv: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)./optes 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 civsnpm installen el build de la imagen:4917f47,89c57f0,60b497d(2026-06-17) — terminó revertido anpm 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.
- 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.
- Cargar los valores nuevos en Secret Manager (
BANCARD_VPOS_PUBLIC_KEY,BANCARD_VPOS_PRIVATE_KEY; para el QR,BANCARD_QR_PUBLIC_KEYyBANCARD_QR_PRIVATE_KEY). - Redesplegar para que el
.envse regenere; el proceso las lee al arrancar. - Pago de control por el monto mínimo y reversión el mismo día.
- 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/healthresponde 200. - [ ]
docker compose psmuestramedusa_backend,medusa_postgresymedusa_redisenUp. - [ ]
docker logs medusa_backend --tail 200 | grep -i errorsin errores nuevos. - [ ] El admin carga y permite login.
- [ ] El tag desplegado es el esperado (
docker ps --format "{{.Names}} {{.Image}}").