diff --git a/workloads/docs-portal/docs/plantillas/platform-templates.md b/workloads/docs-portal/docs/plantillas/platform-templates.md
new file mode 100644
index 0000000..f42e969
--- /dev/null
+++ b/workloads/docs-portal/docs/plantillas/platform-templates.md
@@ -0,0 +1,74 @@
+# platform-templates: dar de alta una app nueva
+
+!!! info "Qué problema resuelve"
+ Antes de este repo, cada app nueva significaba escribir un
+ Dockerfile, un pipeline con los 5 pasos de DevSecOps (Gitleaks,
+ Trivy, Semgrep, Syft, Cosign), y los manifiestos de Kubernetes —
+ todo desde cero, copiando y adaptando a mano lo que ya existía en
+ otra app. `platform-templates` es el repo donde ese trabajo repetido
+ quedó hecho una sola vez: copiás una plantilla, completás unos
+ pocos valores (nombre, imagen, puerto), y tenés el mismo nivel de
+ seguridad y buenas prácticas que ya tienen `ecommerce-frontend`,
+ `commerce-backend` y `docs-portal` — sin tener que redescubrirlas.
+
+Repo: [`devops/platform-templates`](https://gitea.cruzcloud.net/devops/platform-templates)
+
+## De "tengo código" a "está corriendo en producción"
+
+```mermaid
+flowchart TD
+ A["Tengo código
(una app nueva)"] --> B["Elegir arquetipo:
Next.js / API Node / sitio estático"]
+ B --> C["Copiar Dockerfile de
platform-templates/dockerfiles/"]
+ C --> D["Agregar .gitea/workflows/build.yaml
que llama al reusable workflow del arquetipo"]
+ D --> E["Copiar k8s-manifests/overlays-ejemplo/
y ajustar nombre/imagen/puerto"]
+ E --> F["Generar par de llaves Cosign
(cosign.pub commiteado, privada como secret)"]
+ F --> G["git push"]
+ G --> H["Pipeline: Gitleaks → build →
Trivy → Semgrep → Syft → Cosign"]
+ H -->|"CRITICAL o firma inválida"| I["❌ Pipeline se detiene
(nada llega al registry)"]
+ H -->|"todo pasa"| J["Imagen firmada
sube al registry"]
+ J --> K["Pipeline actualiza el
manifiesto GitOps automáticamente"]
+ K --> L["Argo CD detecta el cambio
y sincroniza"]
+ L --> M["✅ App corriendo,
con probes + resource limits +
securityContext ya puestos"]
+
+ style A fill:#e8f4fd,stroke:#4a90d9
+ style M fill:#e6f7e6,stroke:#4ba84b
+ style I fill:#fde8e8,stroke:#d94a4a
+```
+
+Los pasos A→F son manuales (una vez, al dar de alta la app). De G en
+adelante es automático — cada `git push` a `main` repite H→M solo.
+
+## Los 3 arquetipos
+
+| Arquetipo | Para qué | Basado en |
+|---|---|---|
+| `archetype-nextjs-frontend` | Frontend Next.js con SSR (`output: standalone`) | `ecommerce-frontend` |
+| `archetype-node-api` | API backend Node/Express, sin UI | `commerce-backend` (Medusa) |
+| `archetype-static-site` | Sitio estático servido por nginx (MkDocs, Hugo, Astro) | `docs-portal` |
+
+Cada uno ya trae integrado el pipeline completo de DevSecOps — no hay
+que agregar los 5 pasos por separado, vienen adentro del reusable
+workflow.
+
+## Un detalle real de Gitea que no es como GitHub
+
+Armar este repo requirió confirmar en vivo (no asumir) cómo Gitea
+Actions maneja las referencias cross-repo, porque **no es 1:1 igual a
+GitHub Actions** en dos puntos concretos:
+
+1. Los reusable workflows (`workflow_call`) tienen que vivir en
+ `.gitea/workflows/` — no en cualquier carpeta.
+2. Cualquier `uses:` que apunte a otro repo de esta misma instancia
+ necesita la URL completa (`https://gitea.cruzcloud.net/owner/repo/...`)
+ — la forma corta de GitHub (`owner/repo/...`) se resuelve contra
+ **github.com** acá, no contra Gitea local.
+
+El detalle completo, con el error real del servidor que confirmó cada
+punto, está en el README del repo
+([`pipelines/archetype-nextjs-frontend/README.md`](https://gitea.cruzcloud.net/devops/platform-templates/src/branch/main/pipelines/archetype-nextjs-frontend/README.md)).
+
+## Ver también
+
+- [Migración de docs-portal/frontend/medusa a platform-templates](../playbooks/migracion-platform-templates-2026-08.md)
+ — qué cambió en las 3 apps que ya existían, y los bugs reales que
+ aparecieron aplicándolo en producción.
diff --git a/workloads/docs-portal/docs/playbooks/migracion-platform-templates-2026-08.md b/workloads/docs-portal/docs/playbooks/migracion-platform-templates-2026-08.md
new file mode 100644
index 0000000..cba5acb
--- /dev/null
+++ b/workloads/docs-portal/docs/playbooks/migracion-platform-templates-2026-08.md
@@ -0,0 +1,112 @@
+# Migración a platform-templates: docs-portal, frontend, medusa (2026-08)
+
+| | |
+|---|---|
+| **Ventana** | 2026-08-17 |
+| **Servicios migrados** | `docs-portal`, `ecommerce-frontend` (`frontend-deploy`), `commerce-backend` (`medusa-deploy`) |
+| **Impacto en producción** | Cero downtime en las 3 — cada rollout mantuvo el pod viejo sirviendo tráfico hasta que el nuevo estuvo sano |
+
+## Qué cambió
+
+Las 3 apps pasaron de manifiestos Kustomize sueltos y propios de cada
+una a un overlay sobre el base común de
+[platform-templates](../plantillas/platform-templates.md)
+(`k8s-manifests/base/`, referenciado por URL de git remota, no una
+copia local). El base agrega por default lo que ninguna de las 3 tenía
+completo: `readinessProbe`/`livenessProbe` (docs-portal y frontend no
+tenían ninguna) y `securityContext` a nivel pod y contenedor.
+
+Ningún nombre de recurso cambió (`docs-portal-deployment`,
+`frontend-deploy`, `medusa-deploy`, sus Services, todo se mantuvo
+idéntico) — importante para no romper el tracking de Argo CD ni la
+resolución DNS interna de otros servicios que dependen de esos nombres
+(ej. `MEDUSA_BACKEND_URL: http://medusa-svc:9000` en el frontend).
+
+## Por qué: 3 bugs reales encontrados aplicando esto en producción
+
+Los 3 se encontraron con el pod viejo todavía sirviendo tráfico
+(`maxUnavailable: 0` en la estrategia de rollout, y Kubernetes excluye
+automáticamente del Service cualquier pod que no pase su
+`readinessProbe`) — cero impacto real en ningún momento, pero sí
+bugs de verdad, no hipotéticos.
+
+### 1. `runAsNonRoot: true` contra una imagen que corre como root (docs-portal)
+
+`docs-portal` usa `nginx:1.27-alpine` sin ningún `USER` en su
+Dockerfile — corre como root. Aplicar el default del base
+(`runAsNonRoot: true`) rompió el pod al instante:
+`CreateContainerConfigError`. Fix: override explícito
+`runAsNonRoot: false` para este container puntual, documentado como
+temporal hasta que `docs-portal` adopte
+[`dockerfiles/static-nginx.Dockerfile`](https://gitea.cruzcloud.net/devops/platform-templates/src/branch/main/dockerfiles/static-nginx.Dockerfile)
+(`nginxinc/nginx-unprivileged`, no-root de verdad) — cambio de imagen
+aparte, no hecho todavía.
+
+### 2. `capabilities: drop: [ALL]` rompe el propio arranque de nginx (docs-portal)
+
+Con `runAsNonRoot: false` ya resuelto, el pod siguió fallando:
+
+```
+2026/08/17 05:30:33 [emerg] 1#1: chown("/var/cache/nginx/client_temp", 101) failed (1: Operation not permitted)
+```
+
+El entrypoint oficial de la imagen de nginx hace un `chown()` sobre su
+propio directorio de cache en cada arranque — necesita `CAP_CHOWN`.
+El base saca **todas** las capabilities por default; incluso corriendo
+como root, sin `CAP_CHOWN` ese `chown()` falla igual. Fix: sacar la
+restricción de capabilities entera para este container
+(`capabilities: null` en el patch — es la única forma real de anular
+un `drop: [ALL]` heredado, ver nota técnica más abajo). No es peor que
+el comportamiento actual en producción, solo no está endurecido
+todavía — se resuelve del todo con el mismo cambio de imagen del punto
+1.
+
+### 3. `runAsNonRoot: true` sin `runAsUser` numérico (frontend)
+
+`ecommerce-frontend` sí corre no-root (`USER nextjs` en su Dockerfile)
+— pero el pod nuevo falló igual:
+
+```
+Error: container has runAsNonRoot and image has non-numeric user (nextjs),
+cannot verify user is non-root
+```
+
+El Dockerfile define el usuario por **nombre** (`useradd ... nextjs`),
+no por UID numérico. Kubernetes no puede confirmar que ese nombre
+resuelve a un UID distinto de 0 sin arrancar el contenedor, así que por
+seguridad se niega a intentarlo. Fix: `runAsUser: 1001` explícito
+(el UID real del `useradd --uid 1001` del Dockerfile).
+
+**Aplicado ya con este aprendizaje:** para `medusa-deploy`, se confirmó
+el UID real del usuario `node` de la imagen con `docker run --rm
+ id node` (→ 1000) *antes* de escribir el overlay, en vez de
+asumirlo — la migración de Medusa no tuvo ningún bug, arrancó sana en
+el primer intento, `initContainers` (`wait-for-postgres`, `migrations`)
+incluidos.
+
+## Nota técnica: 2 gotchas de Kustomize (no de Kubernetes) encontrados armando esto
+
+- **Renombrar un recurso vía patch de merge normal no funciona** —
+ aunque uses `target:` para seleccionar el recurso del base por su
+ nombre original, cambiar `metadata.name` dentro del contenido del
+ patch no lo renombra en el resultado final. Hace falta un patch
+ JSON6902 aparte (`op: replace, path: /metadata/name`).
+- **Anular un campo heredado necesita `null` explícito** — para sacar
+ el `capabilities: {drop: [ALL]}` del base (bug #2 de arriba), ni
+ omitir el campo ni un objeto vacío `{}` alcanzan; solo
+ `capabilities: null` en el patch realmente lo borra.
+
+Ambos confirmados corriendo `kubectl kustomize` antes de aplicar nada
+al cluster — no son suposiciones.
+
+## Validación
+
+Cada una de las 3 migraciones se confirmó con un `curl` real al dominio
+público correspondiente después del rollout:
+
+- `docs.cruzcloud.net` → `200`
+- `shop.cruzcloud.net` → `200`
+- `commerce.cruzcloud.net/app` → `200`
+
+Y con el estado del pod nuevo (`kubectl get pods`, sin reinicios,
+`1/1 Ready`) antes de dar cada migración por cerrada.
diff --git a/workloads/docs-portal/mkdocs.yml b/workloads/docs-portal/mkdocs.yml
index 1889766..bc9fcc6 100644
--- a/workloads/docs-portal/mkdocs.yml
+++ b/workloads/docs-portal/mkdocs.yml
@@ -78,6 +78,8 @@ markdown_extensions:
nav:
- Inicio: index.md
+ - Templates:
+ - platform-templates: plantillas/platform-templates.md
- Arquitectura:
- Visión general: arquitectura/vision-general.md
- Red y exposición: arquitectura/red-y-exposicion.md
@@ -99,6 +101,7 @@ nav:
- Realm HTTP y Cosign: playbooks/incidente-cosign-realm-http-tunnel-2026-08.md
- Contención de recursos Gitea/runner: playbooks/incidente-contencion-recursos-gitea-runner-2026-08.md
- Blackhole MTU cross-node: playbooks/incidente-mtu-blackhole-cross-node-2026-08.md
+ - "Migración a platform-templates": playbooks/migracion-platform-templates-2026-08.md
- Aprendizajes:
- Notas sueltas: aprendizajes/notas-sueltas.md
- Guía del estudiante: