Hosting del portal — contrato con el Caddy del servidor
Por qué este documento existe
Sección titulada «Por qué este documento existe»El portal no tiene contenedor propio: ni Dockerfile, ni docker-compose.yml, ni
nginx. Los sitios son estáticos y el Caddy de la máquina los sirve directamente con
file_server.
La consecuencia incómoda es que el ruteo del portal no vive en este repo. El workflow deja los ficheros en su sitio; el dominio, las rutas y —cuando llegue— la autenticación son decisiones del Caddyfile del servidor. Este documento es la referencia de lo que esos Caddyfiles tienen que decir. Hay uno por entorno y no se parecen.
Los dos entornos no se parecen
Sección titulada «Los dos entornos no se parecen»| Staging | Producción | |
|---|---|---|
| Dominio | staging.prismgrp.com, compartido por todas las apps |
wiki.prismgrp.com, propio |
| Ruta del portal | /wiki |
raíz |
base de Astro |
/wiki/internal, /wiki/external |
/internal, /external |
| Caddy | handle_path /wiki/* → recorta /wiki |
sin recorte |
| Máquina | stag-debian-x86-64 |
prod-debian-x86-64 |
SITE_ROOT |
/apps/volumes/wiki |
/apps/volumes/wiki — la misma ruta |
La ruta sigue la convención de Prism, /apps/volumes/<app>, con el nombre corto de la
app — igual que /apps/volumes/eprcrm/cache en el CRM, /apps/volumes/coreidentity/logs o
/apps/volumes/vendors/tokens. (Los logs van aparte, en /apps/apps_logs/<app>; este
portal no genera ninguno: no tiene proceso propio.)
SITE_ROOT es idéntico en los dos entornos y no lleva sufijo, porque son máquinas
distintas y cada host tiene su propio /apps/volumes/wiki. Un wiki-stag solo haría falta
si staging y producción compartieran máquina.
El reparto es el mismo que ya usan las demás apps en staging: el artefacto se construye
con el prefijo en sus URLs (si no, el navegador pediría /internal/_astro/…, que cae
fuera del handle_path /wiki/* y da 404) y el árbol de ficheros va sin él, porque Caddy
ya lo ha recortado cuando llega a root.
Lo que el workflow deja en el disco
Sección titulada «Lo que el workflow deja en el disco»<SITE_ROOT>/├── releases/│ ├── <sha-anterior>/│ │ ├── internal/ # dist del sitio interno│ │ └── external/ # dist del help desk público│ └── <sha-actual>/│ ├── internal/│ └── external/├── current -> releases/<sha-actual> # symlink, se mueve con rename(2)└── .release-anterior # a qué apuntaba current antes (para revertir)SITE_ROOT es /apps/volumes/wiki por defecto en los dos entornos. Se puede cambiar por entorno
con las variables de repo SITE_ROOT_PROD y SITE_ROOT_STAG.
Se conservan las 5 releases más recientes; el resto se poda al final de cada
despliegue, saltándose siempre la que current apunta.
Caddyfile de PRODUCCIÓN — dominio propio
Sección titulada «Caddyfile de PRODUCCIÓN — dominio propio»Bloque de sitio nuevo, igual que cualquier otra app con dominio propio.
wiki.prismgrp.com { # La raíz es el SYMLINK, no la release. Así el cambio de versión no requiere # recargar Caddy: la siguiente petición ya resuelve `current` al árbol nuevo. root * /apps/volumes/wiki/current
# --- Sitio interno ------------------------------------------------------------ # HOY SIN AUTENTICACIÓN — ver "Autenticación (en stand-by)" más abajo. # Este bloque es el punto donde se añadirá la validación cuando toque; queda # separado del externo justo para eso. @internal path /internal /internal/* handle @internal { # MIENTRAS NO HAYA AUTH. El sitio interno emite un sitemap con TODAS sus # páginas y URLs absolutas, así que sin esto el modelo operativo de Prism # es indexable por buscadores: no solo "accesible si conoces la URL". # Cubre también el propio sitemap-*.xml, por estar bajo /internal. # # Al activar la autenticación esta cabecera se puede quitar: lo que no se # puede descargar no se indexa. header X-Robots-Tag "noindex, nofollow, noarchive" file_server }
# --- Help desk externo: público ------------------------------------------------ handle /external* { file_server }
# La raíz del dominio no tiene portada propia todavía. handle / { redir /internal/ 302 }
# Los dos sitios son estáticos con rutas reales (Astro genera un directorio por # página), NO SPAs: una ruta que no existe tiene que dar 404, no servir el index. handle_errors { @internal_err path /internal/* handle @internal_err { rewrite * /internal/404.html file_server } handle { rewrite * /external/404.html file_server } }}Caddyfile de STAGING — dominio compartido, bajo /wiki
Sección titulada «Caddyfile de STAGING — dominio compartido, bajo /wiki»Se añade dentro del bloque staging.prismgrp.com que ya existe, junto a las demás
apps. Sigue el mismo par redir + handle_path que /eprcrm y /customerhub.
staging.prismgrp.com {
# ... el resto de las apps ...
# ========================================== # N. Portal de documentación (/wiki) # ========================================== # Sin la barra, `/wiki` resolvería relativo al padre y rompería los enlaces. redir /wiki /wiki/
# `handle_path` RECORTA /wiki antes de resolver el fichero. Por eso el árbol en # disco es current/internal/... y no current/wiki/internal/... # # A diferencia de las otras apps de este dominio, aquí no hay `reverse_proxy`: # el portal no tiene contenedor, Caddy sirve los ficheros. Eso exige que # /apps/volumes/wiki esté bind-mounted en ESTE contenedor de Caddy. handle_path /wiki/* { root * /apps/volumes/wiki/current
# Sitio interno. HOY SIN AUTENTICACIÓN (ver más abajo); el bloque existe # separado para tener dónde añadirla. @internal path /internal /internal/* handle @internal { # Mientras no haya auth: que no lo indexe nadie. Ver la nota del # Caddyfile de producción. header X-Robots-Tag "noindex, nofollow, noarchive" file_server }
handle /external* { file_server }
# Sin matcher: es el ÚLTIMO recurso del bloque, y Caddy lo ordena al final # por ser el menos específico. Cubre `/wiki/` y cualquier ruta bajo /wiki # que no reclame ninguno de los dos sitios (`/wiki/favicon.svg`, por # ejemplo). # # Tiene que existir. Si un `handle_path` casa pero ningún handler de dentro # responde, la petición NO termina ahí: sigue por el resto del bloque de # sitio y acaba en el catch-all del dominio, o sea en «Bienvenido al portal # central de Prism Group» — un 200 que parece que el portal no está # desplegado cuando en realidad sí lo está. handle { redir /wiki/internal/ 302 } }}Cuando /wiki responde «Bienvenido al portal central de Prism Group»
Sección titulada «Cuando /wiki responde «Bienvenido al portal central de Prism Group»»Ese texto es el catch-all de staging.prismgrp.com: lo que el dominio responde a toda
ruta que ningún bloque reclama. Verlo en /wiki significa una sola cosa — el bloque del
portal no está en la configuración que Caddy tiene cargada.
El diagnóstico son dos curl, y el que manda es el del redir:
curl -sI https://staging.prismgrp.com/wiki # tiene que dar 302 -> /wiki/curl -sI https://staging.prismgrp.com/eprcrm # el de al lado, para comparar: 302 -> /eprcrm/redir se ordena antes que handle y handle_path, y Caddy ordena por directiva, no
por el orden del fichero. Así que si /wiki no da 302, ningún error de anidamiento ni de
orden lo explica: la línea redir /wiki /wiki/ no está en la config en ejecución.
Editar el Caddyfile no basta, y las causas habituales son estas cuatro:
# 1. ¿está el bloque en el fichero que LEE EL CONTENEDOR? (editar el del host no sirve# si no es el que está montado)docker compose exec caddy grep -n wiki /etc/caddy/Caddyfile
# 2. ¿se recargó Caddy? Un fichero editado y sin recargar no cambia nada.docker compose exec caddy caddy validate --config /etc/caddy/Caddyfiledocker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
# 3. ¿existe el bind mount DENTRO del contenedor? Sin él, root apunta al vacío# (esto da 404, no el catch-all — pero conviene descartarlo de una vez)docker compose exec caddy ls -l /apps/volumes/wiki/current/
# 4. ¿llegaron los ficheros? En el host de staging:ls -l /apps/volumes/wiki/current/internal/index.htmlEl caso 2 es el más frecuente y el más silencioso: docker compose up -d no recarga la
configuración si el contenedor ya estaba levantado y solo cambió un fichero montado.
Sobre root — lo que expone y lo que no
Sección titulada «Sobre root — lo que expone y lo que no»root no es un permiso: es el directorio base desde el que file_server resuelve los
ficheros, como el root de nginx. No concede privilegios ni cambia el usuario con el que
corre Caddy — lo que hace es limitar lo servible a ese subárbol.
Que el root sea current y no <SITE_ROOT> no es casual: así el árbol servido contiene
solo internal/ y external/. El .release-anterior y el propio releases/ (con
todas las versiones anteriores) quedan fuera de lo alcanzable por HTTP.
| Preocupación | Estado |
|---|---|
Escapar del root con ../ |
Caddy normaliza la ruta antes de unirla al root |
| Listado de directorios | file_server no lista sin browse, que no está puesto |
| Escritura desde Caddy | El bind mount es :ro |
| Releases antiguas o metadatos | Fuera del root, por apuntar a current y no al padre |
| Symlinks que apunten fuera | Caddy sí los sigue. El árbol lo genera nuestro tar desde el dist, así que está controlado: no meter nada a mano en releases/ |
Autenticación (en stand-by)
Sección titulada «Autenticación (en stand-by)»Decidido posponerla: hoy los dos sitios son públicos, incluido /internal.
Cuando toque implementarla, hay que decidir dónde valida, porque para un sitio estático no son equivalentes:
- En Caddy, antes de servir (
forward_auth): Caddy manda cada petición a un endpoint validador y solo entrega el fichero si responde 2xx. Es control de acceso real. - En la propia app, desde el navegador: la página se descarga primero y su JS decide
después si pinta o redirige. Para un sitio estático esto no protege el contenido:
cualquiera puede pedir el
.htmlo el índice de Pagefind directamente y leerlos, sin pasar por el JS. Sirve para la experiencia de usuario, no para la confidencialidad.
Si el corpus interno tiene que quedar de verdad restringido, la validación va en Caddy. El
punto de inserción son los bloques handle @internal de los dos Caddyfiles de arriba.
El workflow ya está preparado: la variable de repo EXPECT_INTERNAL_AUTH controla qué
espera el step de humo.
EXPECT_INTERNAL_AUTH |
Comportamiento del humo |
|---|---|
sin declarar / false |
Informa de que /internal es público y avisa. No bloquea. |
true |
Exige 401/403 sin cookie y falla el despliegue si responde 200. |
O sea: al activar la auth se pone esa variable a true y la red de seguridad se enciende
sola. Hasta entonces no hay ninguna comprobación que pueda pasar, así que no hay ninguna que
mienta.
Prerrequisitos en la máquina
Sección titulada «Prerrequisitos en la máquina»-
Caddy tiene que ver los ficheros. Es el precio de no tener contenedor: Caddy corre en Docker, así que
SITE_ROOTdebe estar bind-mounted en el contenedor de Caddy, en la misma ruta, en las dos máquinas. Montarlo en otra ruta rompe el symlink: su destino es relativo (releases/<sha>) perorootes absoluto, y las dos rutas tienen que coincidir.# en el compose de Caddy — la misma linea en las dos maquinasvolumes:- /apps/volumes/wiki:/apps/volumes/wiki:ro:roes suficiente y es lo correcto: Caddy solo lee. -
El runner tiene que poder escribir en
SITE_ROOT. El preflight del workflow lo comprueba de verdad (crea un fichero de prueba) en vez de mirar si existen binarios, porque el fallo real que se quiere atrapar es un runner en mododocker://: escribiría en la capa efímera de un contenedor y el despliegue saldría en verde sin cambiar nada. -
DNS del dominio apuntando a la máquina. En staging no hace falta DNS nuevo: el portal cuelga de
staging.prismgrp.com, que ya existe.
Variables de Gitea
Sección titulada «Variables de Gitea»Las que gobiernan el ruteo. Las dos primeras tienen que ser coherentes con el Caddyfile o el portal se publica roto:
| Variable | Producción | Staging |
|---|---|---|
SITE_PATH_PREFIX_PROD / _STAG |
vacía (dominio propio) | /wiki (default si no se declara) |
SITE_URL_PROD / _STAG |
https://wiki.prismgrp.com |
https://staging.prismgrp.com/wiki |
SITE_ROOT_PROD / _STAG |
/apps/volumes/wiki (default en ambos) |
/apps/volumes/wiki (default en ambos) |
SMOKE_URL_PROD / _STAG |
https://wiki.prismgrp.com |
https://staging.prismgrp.com/wiki |
EXPECT_INTERNAL_AUTH |
sin declarar mientras la auth esté en stand-by | ídem |
SMOKE_URL lleva el prefijo incluido: el step de humo le concatena /external/ y
/internal/ —las portadas de cada portal, que desde que el sitio publica un solo idioma
viven en la raíz y no bajo /es/—. Las dos tienen default (https://staging.prismgrp.com/wiki y
https://wiki.prismgrp.com), así que el humo nunca se salta: cuando dependía de que la
variable estuviera declarada, no estarlo dejaba el despliegue en verde sin que nadie
comprobara que Caddy sirviera nada.
Revertir a la versión anterior
Sección titulada «Revertir a la versión anterior»No hace falta el CI. En la máquina:
cd /apps/volumes/wikicat .release-anterior # a qué apuntaba antesln -sfnT releases/<sha-anterior> current.tmp && mv -T current.tmp currentEl mv -T sobre el symlink es un rename(2), o sea atómico: ninguna petición ve un estado
intermedio. Es la misma operación que hace el workflow, y por eso revertir cuesta lo mismo
que desplegar.
Cuando cada portal quiera su propio dominio
Sección titulada «Cuando cada portal quiera su propio dominio»Hoy los dos sitios comparten dominio y se separan por ruta, lo que se aparta del
“un portal = un proyecto, con su propio dominio” de architecture.md §8.3. Volver a un
dominio por portal son dos cambios:
base: '/'en elastro.config.mjsdel portal que se separa.- Su propio bloque de sitio en el Caddyfile, con
rootapuntando a<SITE_ROOT>/current/<portal>.
El workflow no cambia: seguiría publicando el mismo árbol de releases.