Operación

Desplegar la documentación

Runbook para publicar este sitio en Coolify, privado por Tailscale con un dominio .vpn., con basic auth opcional, despliegue continuo detrás de CI verde, rollback y dónde mirar los logs.

Especificado. Todo lo que este runbook usa está en el repositorio: la imagen docker/docs.Dockerfile, el compose deploy/coolify/docs.compose.yml y el workflow .github/workflows/deploy-docs.yml. La imagen se ha construido y sondeado en local y en CI (portada, búsqueda y healthcheck), pero el despliegue en Coolify no se ha hecho todavía: los pasos de la UI de Coolify son los de su v4 actual y pueden cambiar de nombre. Es un despliegue provisional del sitio de dec-0124. El adapter de Coolify de track/release-engineering (#24) lo absorberá.

Qué se despliega

PiezaQué es
Imagendocker/docs.Dockerfile: SSR de Fumadocs sobre Bun en el puerto 3010, sin node_modules, usuario no root, HEALTHCHECK contra GET /health
Credenciales de buildNinguna. El bun install de la imagen se limita a @styx/docs (--filter), que no depende de ningún SDK privado: Coolify construye desde el checkout sin SDK_READ_TOKEN
Compose de Coolifydeploy/coolify/docs.compose.yml: un servicio docs sin estado, read_only, sin capabilities y con tmpfs en /tmp. SERVICE_FQDN_DOCS_3010 le dice a Coolify a qué puerto enruta el dominio
AccesoPrivado. alpha.0 no se publica hasta que AppSec esté seco, y el sitio incluye los ADR internos. Lo da el dominio: con .vpn. en el nombre, el hook de Coolify de waxin limita el acceso a Tailscale. Basic auth, opcional
Despliegue continuodeploy-docs.yml: cuando el workflow guard de un push a master termina en verde, llama al webhook de despliegue de Coolify y espera al resultado

Hacer el sitio público es una decisión de waxin. Está en Hacerlo público.

Antes de empezar

  • Un Coolify v4 con su proxy Traefik (el de por defecto) en el VPS, y acceso de administrador al panel.
  • Un dominio cuyo DNS controles. En este runbook es docs.vpn.<tu-dominio>: el .vpn. activa el hook de Coolify que restringe el acceso a Tailscale (dec-0133, P3).
  • Acceso de administrador a MKS2508/styx en GitHub, para instalar la GitHub App y crear secretos de Actions.
  • Sólo si además quieres basic auth (opcional): htpasswd (paquete apache2-utils) o Bun, para generar el hash de la contraseña.

1. Proyecto y entorno

  1. En Coolify, Projects → + Add. Nombre: styx. Si el proyecto ya existe, úsalo.
  2. Dentro del proyecto, usa el entorno production que Coolify crea, o crea uno con + New Environment (por ejemplo docs). La aplicación de la documentación no comparte red ni datos con nada más, así que el entorno sólo sirve para ordenar.

2. GitHub App con acceso al repositorio

El repositorio es privado: Coolify lo clona con una GitHub App.

  1. Sources → + Add → GitHub App. Pon un nombre (por ejemplo coolify-mks2508) y sigue el asistente: Register Now te lleva a GitHub para crear la app.
  2. En GitHub, al instalarla, elige Only select repositories y marca MKS2508/styx. No hace falta ningún otro repositorio: la imagen no baja los SDK privados.
  3. De vuelta en Coolify, la fuente tiene que aparecer como conectada, con el repositorio en su lista.

3. Recurso Docker Compose

  1. En el entorno: + New → Private Repository (with GitHub App). Elige la app del paso 2 y el repositorio MKS2508/styx.
  2. Branch: master.
  3. Build Pack: Docker Compose.
  4. Base Directory: /deploy/coolify. Docker Compose Location: /docs.compose.yml.
    • No pongas / como Base Directory. Coolify ejecuta compose con --project-directory apuntando a la Base Directory, y compose resuelve build.context: ../.. contra ese directorio. Con /, el contexto saldría del checkout y el build fallaría. Con /deploy/coolify, ../.. es la raíz del repo, que es donde está docker/docs.Dockerfile.
  5. Continue. Coolify lee el compose y muestra el servicio docs.
  6. Deja el modo normal: no marques Raw Compose Deployment. Este compose cuenta con que Coolify añada su red y las etiquetas del proxy.

4. Dominio con TLS

  1. En tu DNS, crea el registro de docs.vpn.<tu-dominio> igual que los de tus otros dominios .vpn. (si un comodín *.vpn.<tu-dominio> ya lo cubre, no hay nada que hacer).
  2. En el recurso, servicio docs → Domains: https://docs.vpn.<tu-dominio>. Coolify puede haber rellenado un dominio sslip.io a partir de SERVICE_FQDN_DOCS_3010: sustitúyelo. No añadas :3010, porque la variable ya fija el puerto interno.
  3. Con el esquema https://, Traefik pide el certificado en la primera petición, como para el resto de tus dominios .vpn..

5. Acceso privado

Tailscale, por el dominio

El dominio docs.vpn.<tu-dominio> del paso 4 ya basta: el hook de Coolify de waxin restringe a Tailscale todo dominio con .vpn., así que fuera de la tailnet el sitio no responde. Cubre todo el dominio, también /health y /api/search. No hay credenciales que crear ni variables que poner.

Lo único obligatorio en el recurso es esto:

  • Recurso → Advanced → desmarca Escape special characters in labels. El compose define el middleware styx-docs-auth con una etiqueta que lleva ${DOCS_BASIC_AUTH_USERS…}; si el escape sigue marcado, Coolify convierte el $ en $$, compose no la sustituye y Traefik recibe un middleware roto que tumba el router del dominio.

Sin la variable DOCS_BASIC_AUTH_USERS, ese middleware es una cabecera X-Robots-Tag: noindex, nofollow inocua y no hay basic auth. Existe siempre porque Coolify lo engancha a sus routers leyendo la etiqueta sin interpolar: si desapareciera, el router apuntaría a un middleware que no existe.

Basic auth (opcional)

Para una capa más encima de Tailscale, o si algún día el sitio va en un dominio sin .vpn.. Coolify trae un interruptor HTTP Basic Authentication, pero sólo genera las etiquetas para las aplicaciones que no son Docker Compose; aquí va por la variable del compose. Las credenciales nunca entran en git.

  1. Genera el usuario y el hash bcrypt (no reutilices una contraseña):

    htpasswd -nbB waxin '<contraseña>'
    # sin htpasswd:
    bun -e 'console.log("waxin:" + await Bun.password.hash(process.argv[1], { algorithm: "bcrypt" }))' '<contraseña>'

    La salida tiene la forma waxin:$2y$05$... (o $2b$). Para varios usuarios, sepáralos con comas.

  2. Recurso → Environment Variables → + Add:

    • Name: DOCS_BASIC_AUTH_USERS. Value: la línea completa usuario:hash.
    • Marca Is Literal. Coolify la escribe entonces entre comillas simples en el .env y compose no interpreta los $ del hash.
    • Desmarca Available at Buildtime: la imagen no la necesita.
  3. Redespliega. Con la variable, el middleware styx-docs-auth pasa a ser basicauth. Para quitarla, borra la variable y redespliega.

Coolify añade al contenedor todas las variables del recurso (las vuelca en un env_file). Aquí como mucho hay un hash bcrypt, que no es la contraseña. No añadas al recurso ningún token.

6. Desactivar el auto-deploy de Coolify

Con el auto-deploy de la GitHub App, Coolify desplegaría cada push a master aunque CI esté en rojo. El despliegue continuo va por CI, así que:

  • Recurso → Advanced → desmarca Auto Deploy.

7. Primer despliegue y verificación

  1. Deploy. El build tarda unos minutos: instala las dependencias del sitio y genera ~2.500 páginas MDX con Vite. El log se ve en vivo en Deployments.

  2. Cuando termine, el contenedor tiene que estar Running (healthy). El healthcheck es el de la imagen, GET /health cada 30 s.

  3. Desde una máquina dentro de la tailnet:

    curl -s https://docs.vpn.<tu-dominio>/health                                         # {"status":"ok"}
    curl -s -o /dev/null -w '%{http_code}\n' https://docs.vpn.<tu-dominio>/docs          # 200
    curl -s 'https://docs.vpn.<tu-dominio>/api/search?query=styx' | head -c 200          # JSON con resultados

    Con basic auth, añade -u 'waxin:<contraseña>'; sin credenciales la respuesta es 401.

  4. Desde una máquina fuera de la tailnet (por ejemplo, el móvil con Tailscale apagado), la misma URL no tiene que servir el sitio: la conexión no llega o el hook la rechaza.

  5. En el navegador, desde la tailnet, https://docs.vpn.<tu-dominio> tiene que servir el sitio con un certificado válido (y pedir usuario y contraseña si activaste la basic auth).

8. Despliegue continuo desde CI

.github/workflows/deploy-docs.yml se dispara cuando termina el workflow guard de un push a master. Sólo despliega si ha terminado en verde, y guard incluye el job docs-site, que construye esta misma imagen y la sondea. El workflow hace tres cosas:

  • Llama al webhook de Coolify y sigue el despliegue hasta finished o failed. Si falla, el job se pone en rojo.
  • Si master ya avanzó a otro commit, no despliega. Coolify construye siempre la cabeza de la rama, así que desplegar en ese momento publicaría un commit sin CI verde. Lo despliega el run de ese commit.
  • Sin los secretos, el job termina en verde con un aviso y no despliega nada.
  1. Coolify → Settings → Configuration (o Settings → Advanced, según la versión): activa API Access. Si limitas las IPs de la API, deja pasar a los runners de GitHub, o la llamada desde CI no llegará.
  2. Keys & Tokens → API Tokens → Create. Nombre: github-actions-docs. Permisos: deploy y read. Con read, el workflow puede seguir el despliegue. Sin él, sólo lo encola y avisa. Copia el token: Coolify no lo vuelve a mostrar.
  3. Recurso → Webhooks: copia el Deploy Webhook, que tiene la forma https://<tu-coolify>/api/v1/deploy?uuid=<uuid>&force=false.
  4. GitHub → MKS2508/styx → Settings → Secrets and variables → Actions → New repository secret:
    • COOLIFY_TOKEN: el token del paso 2.
    • COOLIFY_DOCS_WEBHOOK: la URL del paso 3.
  5. Prueba: Actions → deploy-docs → Run workflow sobre master. El job tiene que terminar en verde después de un deploy-docs: finished, y en Coolify aparece un despliegue nuevo.

La alternativa: auto-deploy de la GitHub App

Con el Auto Deploy del paso 6 activado y sin secretos en GitHub, Coolify despliega en cuanto recibe el push. Es más simple y más rápido, pero no espera a CI: un push que rompe el build o la referencia generada llega igual a producción, y el fallo se ve en Coolify en vez de en el PR. Por eso el despliegue por defecto es el de CI. Si algún día se cambia, desactiva deploy-docs.yml (quita los secretos) para no desplegar dos veces.

Rollback

  • Lo normal: git revert del commit malo y push a master. Cuando guard esté verde, CI despliega la versión revertida.
  • Inmediato, sin esperar a CI: recurso → General → campo Commit SHA (por defecto HEAD) → pon el SHA del último commit bueno y pulsa Redeploy. Mientras el campo tenga un SHA, todos los despliegues, también los de CI, construyen ese commit. Cuando master tenga el arreglo, devuélvelo a HEAD.
  • Coolify guarda el historial en Deployments: cada entrada tiene el commit y el log de su build.

Dónde están los logs

QuéDónde
Build y arranque de cada despliegueCoolify → recurso → Deployments → la entrada
Salida del servidor (SSR, errores 5xx)Coolify → recurso → Logs → servicio docs
Por qué CI desplegó o noGitHub → Actions → deploy-docs → el run, paso «Webhook de despliegue de Coolify»
Router, certificado o 401 inesperadoLogs del contenedor coolify-proxy (Coolify → Servers → el servidor → Proxy → Logs)

Problemas frecuentes

SíntomaCausa
failed to read dockerfile o un contexto vacíoLa Base Directory no es /deploy/coolify (paso 3)
El sitio responde fuera de la tailnetEl dominio no lleva .vpn. y el hook de Tailscale no se aplica (pasos 4 y 5)
404 de Traefik o error del middleware styx-docs-authEscape special characters in labels sigue marcado (paso 5)
Siempre 401, también con la contraseña buenaCon basic auth: la variable no es Is Literal, o el escape de etiquetas sigue marcado (paso 5)
404 de Traefik o certificado autofirmadoDominio mal puesto en el servicio docs, o DNS que aún no resuelve (paso 4)
deploy-docs en verde sin desplegarFaltan COOLIFY_TOKEN/COOLIFY_DOCS_WEBHOOK, o master ya avanzó (el aviso del run lo dice)
deploy-docs en rojo con HTTP 401/403Token caducado o sin permiso deploy, o API Access desactivado (paso 8)
deploy-docs en rojo con HTTP 405El webhook no es el /api/v1/deploy?uuid=… del recurso

Hacerlo público

Es una decisión de waxin, que depende de que AppSec esté seco para alpha.0. Para hacerlo: cambia el dominio del servicio docs por uno sin .vpn. (por ejemplo docs.<tu-dominio>, con su DNS apuntando al VPS y los puertos 80 y 443 abiertos para Let's Encrypt) y borra la variable DOCS_BASIC_AUTH_USERS si la pusiste. El siguiente despliegue deja el sitio abierto; la etiqueta del compose se queda, inocua. Ten en cuenta que el sitio incluye el texto íntegro de los ADR y la referencia interna del data plane.