Runbook de la pila completa de styx como aplicación Raw Compose de Coolify — compose de un solo fichero, secretos como ficheros del host con un one-shot y su smoke, Base Directory, dominios .vpn. con TLS del proxy y acceso sólo desde la tailnet (proxy y QUIC), infraestructura sin exponer, verificación, actualización y rollback.
Especificado, no probado contra un Coolify real. Todo lo que este runbook usa está en el repositorio y está comprobado en local: el compose resuelve con la Base Directory que se indica y falla con la de por defecto; el one-shot de secretos y su smoke tienen tests con un mutante por regla; los routers de Traefik salen del modelo de despliegue y
check:deploy-renderlos compara con el compose. Que Coolify acepta el compose está comprobado leyendo su código (v4.x,validateDockerComposeForInjection), no desplegándolo. Lo que no se ha hecho es desplegarlo en la instancia de Coolify de waxin: los nombres de la UI son los de Coolify v4 y pueden cambiar (track/release-engineering#24, pendiente de verificación independiente).
| Pieza | Qué es |
|---|---|
| Compose | deploy/docker-compose.coolify.yml: docker-compose.infra.yml + docker-compose.apps.yml en un solo fichero, generado (bun run deploy:coolify-compose; un test lo compara byte a byte y comprueba que docker compose config lo resuelve igual que deploy/docker-compose.yml). Coolify rechaza deploy/docker-compose.yml: lo lee del repositorio al cargarlo y en cada despliegue, y un fichero sin services: (sólo include:) no pasa su validación. Aplicación Raw Compose Deployment (deploy/README.md, DS8-01): en el modo por defecto Coolify mete su red en todos los servicios y la infra deja de estar aislada |
| Imágenes | Desde el código: Coolify las construye del clon del repositorio. Con release: imágenes por digest del manifiesto, con styx render --runtime coolify (ver Con una release) |
| Secretos | Ficheros del host, fuera del clon, en STYX_SECRETS_DIR. Los crea una vez bun run deploy:coolify-secrets -- init. Ningún secreto va en las variables de Coolify |
| Infraestructura | Postgres, Valkey, NATS y el collector sólo en styx-internal (red internal sin gateway): sin puertos publicados, sin salida, inalcanzables desde el proxy |
| Entrada | El Traefik de Coolify, enganchado a styx-edge con la IP fija 10.254.254.2. Routers con TLS (ACME del proxy): /auth (identity-svc) y /api/upload (ingesta del daemon) en STYX_PUBLIC_HOST, y el sitio de docs en STYX_DOCS_HOST. Cada router empieza por el middleware styx-tailnet-only |
| Acceso | Sólo la tailnet. alpha.0 no se publica hasta que AppSec esté seco (dec-0133). Los dominios llevan .vpn. (el hook de Coolify de waxin), pero que ese hook cubra los routers escritos como labels en Raw Compose no está verificado, y si es sólo DNS no para a quien llegue por la IP pública. Por eso el corte lo hace el propio proxy: styx-tailnet-only (deploy/traefik/styx-tailnet.yml, ipAllowList de 100.64.0.0/10 y fd7a:115c:a1e0::/48 por la dirección del socket) responde 403 a cualquier otro origen |
| QUIC | UDP 4433-4435 del daemon publicados en el host en 0.0.0.0 (styx-quic, DS9-01). deploy/host-firewall.sh con STYX_QUIC_ALLOW_FROM=100.64.0.0/10 descarta en DOCKER-USER todo UDP y TCP hacia esa red que no venga de la tailnet; el smoke (--quic-firewall) lo exige. El origen TCP del daemon (4435/tcp, HTTPS HTTP/1.1 con Alt-Svc hacia h3, dec-0134) se publica igual, directo en el host y sin router de Traefik: el daemon termina su TLS con el certificado de la QUIC y ve la dirección real del cliente |
| Salud | Cada servicio tiene healthcheck en el compose; los *-migrate corren antes de su servicio (service_completed_successfully) |
https, resolvedor ACME letsencrypt).styx.vpn.<tu-dominio> para la API y
docs.vpn.<tu-dominio> para la documentación, los dos apuntando al VPS.MKS2508/styx (cualquiera; sólo se
usa para el one-shot y el smoke) con bun install hecho, y docker compose v2.STYX_MEDIA_WT_HOST) en
el host, legible por el uid 1001. Es también el del origen TCP (4435/tcp, dec-0134), al que un
navegador llega sin flags: de una CA pública (Let's Encrypt), con la cadena completa
(fullchain.pem). Los certificados ACME del Traefik de Coolify no sirven aquí (no son ficheros
del daemon). Tras renovarlo, reinicia el daemon: la QUIC no recarga el certificado en caliente.OIDC_ISSUER, OIDC_CLIENT_ID) y el SDK_READ_TOKEN de sólo
lectura para construir las imágenes Bun (deploy/README.md, «Secreto de build»).Coolify clona el repositorio de nuevo en cada despliegue, así que deploy/secrets/ y
deploy/.env de bun run deploy:secrets no sobreviven. El one-shot los deja en un directorio
fijo del host:
cd /ruta/al/checkout/styx
sudo STYX_OPERATOR_UID="$(id -u)" bun run deploy:coolify-secrets -- init \
--dir /data/styx/secrets \
--env-out /data/styx/coolify.env \
--traefik-dir /data/coolify/proxy/dynamic--dir: absoluto y fuera de todo checkout (el script lo exige). Cada fichero queda del uid de
su contenedor, 0400 o 0440; el directorio, 0700. Repetirlo no regenera nada: sólo crea lo que
falte.--env-out: lo público que el compose interpola (keyring y NKeys del bus, clave pública del
SCT) y STYX_SECRETS_DIR. Es lo que se pega en Coolify; no lleva ningún secreto.--traefik-dir: la configuración dinámica del proxy de Coolify. Deja styx-proxy-proof.yml
(los middlewares styx-proxy-proof y styx-ingest-proxy-proof, con las pruebas de proxy de
los secretos; 0600), styx-ingest.yml (los límites de la ingesta) y styx-tailnet.yml (el
corte a la tailnet). Sin ellos los routers apuntan a middlewares que no existen y Traefik no
los carga: falla cerrado. Coolify lista y enseña los ficheros de ese directorio en Proxy →
Dynamic Configurations: quien administre Coolify ve las pruebas de proxy en claro. Es
defensa en profundidad (esa persona ya es dueña del host), pero cuenta como un sitio más donde
viven.SIGNOZ_INGESTION_KEY en el entorno del init, si exportas a SigNoz; sin ella el collector
usa una clave que SigNoz rechaza.Copia de seguridad: el directorio de secretos y la clave privada de la QUIC van al backup del host (no están en Coolify ni en git). Perderlos es rotar las identidades del bus.
styx de Coolify: + New → Private Repository (with GitHub App) →
MKS2508/styx, rama master, Build Pack: Docker Compose./deploy y Docker Compose Location: /docker-compose.coolify.yml.
Coolify corre compose con --project-directory <Base Directory> y compose resuelve contra ese
directorio los build.context: .. y las rutas ./. Con /deploy coinciden con el directorio
del fichero (como a mano); con la Base Directory / por defecto Coolify no encuentra el
fichero, y los contextos de build caerían fuera del repositorio. No uses
/docker-compose.yml: es el de include: y Coolify lo rechaza («Docker Compose file must
contain a "services" section»). Lo mismo que se hizo con el sitio de docs
(deploy/coolify/docs.compose.yml, Base Directory /deploy/coolify).${STYX_PUBLIC_HOST} y ${STYX_DOCS_HOST} en sus labels).route en deploy/model/styx.deploy.yml.docs en STYX_DOCS_HOST. Si
la aplicación de docs suelta (deploy/coolify/docs.compose.yml, operacion/desplegar-docs)
sigue desplegada con el mismo dominio, Traefik tiene dos routers para el mismo Host y cuál
gana no está definido: para esa aplicación antes, o da a esta pila otro STYX_DOCS_HOST.En Environment Variables → Developer view, pega /data/styx/coolify.env y añade lo del
operador:
| Variable | Ejemplo | Para qué |
|---|---|---|
STYX_PUBLIC_HOST | styx.vpn.<tu-dominio> | dominio de /auth y /api/upload (router de Traefik) |
STYX_DOCS_HOST | docs.vpn.<tu-dominio> | dominio del sitio de docs |
IDENTITY_PUBLIC_URL, IDENTITY_ALLOWED_RETURN_ORIGINS | https://styx.vpn.<tu-dominio> | identity-svc |
OIDC_ISSUER, OIDC_CLIENT_ID | tu OP | login |
STYX_INGEST_PUBLIC_URL | https://styx.vpn.<tu-dominio>/api/upload | la ingesta |
STYX_MEDIA_WT_HOST, STYX_MEDIA_ALLOWED_ORIGINS | dominio de la QUIC, orígenes de la web | el daemon |
STYX_MEDIA_ROOTS, STYX_MEDIA_LIBRARY | library=/media/library:ro, /srv/media | biblioteca (bind de sólo lectura) |
STYX_MEDIA_TLS_CERT_FILE, STYX_MEDIA_TLS_KEY_FILE | rutas absolutas del host | TLS de la QUIC (secretos de fichero) |
SDK_READ_TOKEN | PAT de sólo lectura | sólo el build (BuildKit), nunca en runtime |
Ninguna variable lleva un secreto de runtime: un secreto en el entorno de un contenedor, en
cualquier valor del servicio o en un --env-file del smoke hace que el smoke falle y, en
producción, que el servicio no arranque.
SDK_READ_TOKEN sí vive en Coolify. Marcado como variable de build, Coolify lo guarda en
claro en su base de datos, lo enseña en la UI a todo el equipo y lo escribe en el .env de la
aplicación en el host (el de runtime lleva todas las variables). Además, si el recurso no
tiene activado Use Docker Build Secrets, Coolify añade cada variable de build como
--build-arg al docker compose build (ApplicationDeploymentJob, generate_build_env_variables):
actívalo para que sólo llegue como el secreto de BuildKit sdk_token que ya declaran los
Dockerfiles. Usa un token de sólo lectura y sólo para los repositorios de los SDK, y rótalo si
alguien deja el equipo de Coolify.
El cortafuegos va por subred (fija en el compose), así que se aplica antes de que exista la red:
sudo STYX_QUIC_ALLOW_FROM=100.64.0.0/10 sh deploy/host-firewall.sh applyCierra styx-quic y egress-oidc como siempre y, con STYX_QUIC_ALLOW_FROM, descarta toda QUIC
entrante que no venga de la tailnet. Hay que aplicarlo tras cada arranque del host (o
persistirlo con iptables-save). Hacer pública la QUIC es parte de la decisión de publicar
alpha.0 (waxin), no un ajuste del operador.
Guarda las variables del operador de la tabla (sin SDK_READ_TOKEN) en
/data/styx/operator.env y resuelve el compose exactamente como lo hará Coolify:
sudo bun run deploy:coolify-secrets -- check \
--dir /data/styx/secrets \
--env-file /data/styx/coolify.env --env-file /data/styx/operator.env \
--base-directory /deploy \
--traefik-dir /data/coolify/proxy/dynamic \
--quic-firewallVerde (check: OK, exit 0) es: el compose de Coolify resuelve con esa Base Directory; todo
build.context cae dentro del repositorio; cada secreto es un fichero absoluto bajo
STYX_SECRETS_DIR, regular, no vacío y 0400/0440 (0600 no vale); ningún valor de un servicio
(entorno, labels, command…) ni de los --env-file contiene el de un secreto; la infra no
publica puertos; un puerto publicado fuera de la tailnet sólo pasa con --quic-firewall y
deploy/host-firewall.sh check en verde con STYX_QUIC_ALLOW_FROM=100.64.0.0/10; cada router
se llama styx-<servicio>, tiene la regla exacta del render (Host(…) y, opcional,
PathPrefix(…): un STYX_PUBLIC_HOST que cuele un segundo Host no pasa), un dominio con la
etiqueta vpn y styx-tailnet-only@file como primer middleware; y los middlewares del proxy
llevan las pruebas de los secretos y el corte a la tailnet. Con --base-directory / falla: es la
comprobación de que el paso 2 está bien puesto. Con sudo, porque cada secreto es del uid de su
contenedor y iptables necesita root.
Tras el primer despliegue (la red styx-edge la crea compose):
docker network connect --ip 10.254.254.2 styx-edge coolify-proxy10.254.254.2 es la única cuyo X-Forwarded-For aceptan identity-svc y la ingesta
(deploy/README.md, «Proxy de entrada»). Coolify recrea coolify-proxy cuando cambias su
configuración o lo reinicias: repite el docker network connect después (sin él los routers
no llegan a styx y responden 502/504).styx-tailnet-only decide por la dirección del socket que ve Traefik. Si el tráfico de la
tailnet llega al proxy a través de otro proxy local (tailscale serve), esa dirección es
127.0.0.1 y todo responde 403: el corte falla cerrado. El camino previsto es el directo, al
443 publicado del host por su IP de Tailscale.Deploy en Coolify. Los *-migrate terminan antes de sus servicios; un despliegue sano deja
todos los contenedores healthy.
En el host, sobre el proyecto compose de la aplicación (el uuid de Coolify):
bun run check:infra-live -- --project <uuid-de-la-aplicación>Exige usuarios no root, rootfs de sólo lectura, sin capabilities, seccomp, sin puertos fuera de loopback salvo la QUIC, redes exactamente las del compose y rechazo del anónimo en NATS, Valkey, OTLP y Postgres. Rojo si Coolify añadió su red (el recurso no está en Raw Compose).
Desde un equipo en la tailnet:
curl -sS -o /dev/null -w '%{http_code}\n' https://styx.vpn.<tu-dominio>/auth/session # 401: routed a identity-svc
curl -sS -o /dev/null -w '%{http_code}\n' https://docs.vpn.<tu-dominio>/health # 200Un 503 IDENTITY_CLIENT_ADDRESS_UNKNOWN en /auth es la prueba de proxy que falta o no
coincide (paso 1, --traefik-dir).
Desde un equipo fuera de la tailnet, contra la IP pública aunque el DNS no la dé:
curl -sS -o /dev/null -w '%{http_code}\n' --resolve styx.vpn.<tu-dominio>:443:<IP-pública> https://styx.vpn.<tu-dominio>/auth/session # 403
curl -sS -o /dev/null -w '%{http_code}\n' --resolve docs.vpn.<tu-dominio>:443:<IP-pública> https://docs.vpn.<tu-dominio>/health # 403Un 401 o un 200 aquí es styx publicado: para y revisa styx-tailnet.yml en el directorio
dinámico del proxy y las labels del router.
Cuando haya releases (dec-0123: alfas 0.0.0-alpha.N), el compose no se construye: lo renderiza
el CLI desde el modelo con las imágenes por digest del manifiesto.
styx render --runtime coolify --manifest /opt/styx/current/styx-release.json --out ./styx-coolifystyx.toml lleva entonces STYX_PUBLIC_HOST y STYX_DOCS_HOST (además de lo de INSTALL.md
§8), y validate() se niega a dar el compose por bueno (exit 1, una línea por regla) si queda un
build:, un secreto por ruta relativa o sin resolver, un router sin dominio, un dominio sin la
etiqueta vpn, una regla fuera de la gramática del render, un router sin
styx-tailnet-only@file primero, un router que el modelo no declara (o uno TCP/UDP) o un router
sin el resolvedor ACME del proxy. validate() no ve el cortafuegos del host: la QUIC se limita
igual que arriba (paso 4). El compose renderizado se pega en el mismo
recurso Raw Compose. Hoy styx render necesita un styx install previo en esa máquina para
styx.toml, los secretos y las claves públicas (INSTALL.md §8).
*-migrate.contract, eso no basta: restaura antes el pg_dump del rol migrador hecho antes de
actualizar (styx backup no opera Coolify todavía, track/release-engineering#24).init (sólo crea lo que falta), repite el
--traefik-dir si era una prueba de proxy, y reinicia el servicio que lo lee.| Síntoma | Causa |
|---|---|
Docker Compose file must contain a "services" section | Docker Compose Location /docker-compose.yml (el de include:) en vez de /docker-compose.coolify.yml (paso 2) |
Docker Compose file not found at: /docker-compose.coolify.yml | Base Directory / en vez de /deploy (paso 2) |
deploy/docker-compose.coolify.yml desactualizado (tests) | Cambió un compose incluido: bun run deploy:coolify-compose y commit |
| 403 desde un equipo de la tailnet | El tráfico llega al proxy por otro proxy local (127.0.0.1), o el cliente sale por otra red (paso 5) |
SPIRE_KEYRING sin definir | Falta pegar coolify.env en las variables del recurso |
secret … file … no such file | STYX_SECRETS_DIR no es el --dir del init, o el init se hizo en otro host |
| 404 en el dominio | Escape de labels activo (el router queda con ${STYX_PUBLIC_HOST} literal) o falta traefik.enable |
| 502/504 en el dominio | coolify-proxy no está en styx-edge con 10.254.254.2 (paso 5) |
check:infra-live rojo por una red <uuid> | El recurso no está en Raw Compose Deployment |
track/release-engineering#24)styx status/update/rollback/backup contra Coolify (RE15) y un styx que genere styx.toml,
secretos y claves sin styx install en esa máquina.styx-edge pero el modelo no les
da router todavía (la web, que las consume, no está en el modelo de despliegue).<uuid> que conecta al proxy no rompa el
aislamiento de Raw Compose, y que docker compose acepte los secrets.file del host cuando
Coolify lo ejecuta dentro de su contenedor auxiliar (las rutas son del host; si compose las
comprueba en el cliente, no las vería).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.
Desplegar la nightly
Runbook de la nightly privada de master en Coolify — imágenes privadas en GHCR, credencial de pull en el host, aplicación de Coolify que fija las imágenes por digest, secretos del environment coolify-nightly, verificación, rollback y logs.