# Incidente: imágenes de producto en blanco (proxy host de `media.cruzcloud.net` mal configurado en NPM) (2026-08)

!!! info "Origen"
    Migrado desde `apps-registry/docs/playbooks/incidente-medusa-imagenes-proxy-npm-2026-08.md`.
    Contenido técnico preservado sin cambios de fondo — solo formato adaptado a MkDocs Material.

| | |
|---|---|
| **Ventana del incidente** | 2026-08-13, resuelto el mismo día |
| **Servicio afectado** | `media.cruzcloud.net` (Nginx Proxy Manager → MinIO, `minio-svc` en namespace `ecommerce`) |
| **Impacto** | Las imágenes subidas a productos desde el Admin de Medusa no se visualizaban en el detalle de producto del storefront (`shop.cruzcloud.net`) — el resto del producto (título, precio, subtítulo, botón añadir al carrito) se renderizaba bien |

## Síntoma

Al agregar imágenes a un producto desde el Admin de Medusa (sección
Media), estas se veían correctamente cargadas en el Admin, pero en la
página de detalle de producto del frontend los thumbnails aparecían
vacíos/en blanco.

## Diagnóstico

1. **Storage:** confirmado que Medusa usa el provider
   `@medusajs/medusa/file-s3` apuntando a MinIO interno
   (`S3_ENDPOINT=http://minio-svc:9000`, bucket `ari-products`), con
   URLs públicas construidas sobre
   `S3_FILE_URL=https://media.cruzcloud.net/ari-products` (env en
   configmap `commerce-config`, namespace `ecommerce`).
2. **API:** el producto traía URLs absolutas válidas, ej.
   `https://media.cruzcloud.net/ari-products/<archivo>.jpg`.
3. **Prueba directa de la URL:**
   - Contra el ingress interno del cluster (bypaseando el proxy
     externo, `Host: media.cruzcloud.net` directo a la IP del
     `k3d-proxy`) → `200 OK`, MinIO sirve el JPEG correctamente.
   - Contra `https://media.cruzcloud.net/...` (la ruta real que usa el
     navegador del cliente, vía Cloudflare) → `500 Internal Server
     Error`, `Server: openresty`.

!!! tip "Patrón de diagnóstico clave"
    Ese contraste (funciona interno, falla público, con
    `Server: openresty` — el motor de Nginx Proxy Manager) aisló el
    problema al **proxy externo**, no a Medusa/MinIO/frontend.

## Causa raíz

En Nginx Proxy Manager (`/DATA/AppData/nginxproxymanager/`, contenedor
`nginxproxymanager`), el proxy host de `media.cruzcloud.net`
(`id=13` en la tabla `proxy_host` de `database.sqlite`) estaba mal
configurado:

```text
forward_host = "http://192.168.68.61/"   -- esquema y barra final metidos dentro del campo (formato inválido)
forward_port = 5005                       -- puerto donde no escucha ningún proceso en el host
```

Los proxy hosts que sí funcionan (`shop.cruzcloud.net` id 23,
`commerce.cruzcloud.net` id 28) apuntan correctamente a:

```text
forward_host = "192.168.68.61"
forward_port = 90   -- puerto publicado por k3d-lab-cluster-serverlb hacia Traefik
```

Se confirmó con `ss`/`docker ps` en el host que nada escuchaba en el
puerto `5005` — este proxy host nunca estuvo apuntando correctamente
al cluster, muy probablemente un typo desde que se creó.

## Fix aplicado

1. Backups previos: `database.sqlite.bak-pre-media-fix` y
   `13.conf.bak-pre-media-fix`.
2. Corrección directa en la base SQLite de NPM:
   ```sql
   UPDATE proxy_host SET forward_host='192.168.68.61', forward_port=90 WHERE id=13;
   ```
3. NPM regeneró automáticamente `nginx/proxy_host/13.conf` con los
   valores correctos (la app Node de NPM detecta el cambio en la base
   y reescribe el `.conf`; no hace falta editarlo a mano).
4. Recarga de nginx dentro del contenedor:
   ```bash
   docker exec nginxproxymanager nginx -t
   docker exec nginxproxymanager nginx -s reload
   ```

Adicionalmente, se seteó el campo `thumbnail` del producto de prueba
desde el Admin (estaba en `null`) — Medusa no lo autocompleta a partir
del array `images`, y el frontend lo usa como imagen hero en listados/
tarjetas de catálogo.

## Validación

Confirmado visualmente en
`shop.cruzcloud.net/product/cybertron-sentinel-figura-de-coleccion-test-revalidate`
que tanto la galería del detalle como el thumbnail en listados/tarjetas
se renderizan correctamente. También se validó que ediciones de
descripción/título/precio desde el Admin se reflejan automáticamente
en el storefront sin necesidad de restart ni intervención manual, vía
el subscriber `revalidate-storefront.ts`
(`workloads/commerce-backend/src/subscribers/`) que procesa el evento
`product.updated` de Medusa.

## Lecciones aprendidas

**Aislar interno vs. público antes de sospechar de la app.** Medusa,
MinIO y el frontend estaban sanos todo el tiempo — la falla estaba en
una capa de infraestructura fuera de los repos de GitOps (Nginx Proxy
Manager, gestionado por fuera de `apps-registry`/`platform-infra`, sin
versionar). Comparar la misma petición por la ruta interna del cluster
contra la ruta pública real fue lo que aisló la causa en minutos, sin
necesidad de tocar Medusa ni el frontend.

Ver [Red y exposición](../arquitectura/red-y-exposicion.md) para la
topología completa Cloudflare Tunnel → Nginx Proxy Manager → k3d y el
patrón a seguir si otro subdominio (`*.cruzcloud.net`) presenta el
mismo síntoma (`500`/`Server: openresty` público pero sano
internamente).
