Despliegue en Data Clerk
Esta aplicación se ejecuta en Data Clerk, un pequeño clúster Kubernetes gestionado por un operador (una persona, cuyos datos de contacto están en PROJECT.md). El dueño de este repositorio normalmente no es una persona de infraestructura. Nunca debería tener que tocar servidores, Kubernetes o una consola en la nube: hace push a main, y el clúster hace el resto. Tu trabajo es mantener las cosas así: trabaja dentro del repositorio, sigue el contrato a continuación, y cuando algo requiera genuinamente del lado del clúster, escribe al operador una solicitud clara en lugar de improvisar.
Explícale las cosas al usuario en lenguaje sencillo ("tu sitio se actualizará en unos cinco minutos"), no en términos de Kubernetes.
Empieza aquí
- Lee
deploy-data-clerk/PROJECT.md. Contiene los valores de este proyecto: URL pública, la carpeta de despliegue, nombre de la imagen, puerto, comando de migración, límites, quién es el operador. Todo lo que a continuación diga "desde PROJECT.md" significa ese archivo. Si el nombre de la carpeta en PROJECT.md difiere dedeploy-data-clerk/(proyectos más antiguos usandeploy/), usa lo que diga PROJECT.md. - ¿No hay
PROJECT.md, o todavía tiene valores__PLACEHOLDER__? El proyecto no está incorporado aún: siguereferences/new-project.md. - ¿Escribir o cambiar un Dockerfile, elegir cómo estructurar la aplicación, o el stack no es Node? Lee
references/stacks.md(cualquier lenguaje, full-stack vs frontend separado + API, FastAPI, Go). - Antes de la primera tarea con secretos en una máquina, asegúrate de que las herramientas funcionan:
sops --versiony un descifrado de prueba (sops decrypt <carpeta de despliegue>/secrets.env > /dev/null). Si falla alguno, siguereferences/setup-tools.md.
Cómo un cambio llega al sitio en vivo
| Qué cambió | Qué ocurre | Tiempo hasta estar en vivo |
|---|---|---|
Código (push a main) | GitHub Actions construye una imagen Docker y la sube; el clúster nota la nueva etiqueta de imagen y la despliega | ~3–7 min (tiempo de compilación + ~1 min) |
secrets.env (push a main) | El clúster relee la carpeta de despliegue cada 5 min, la descifra, actualiza las variables de entorno de la aplicación y reinicia la aplicación | ~5 min |
Un nuevo archivo en migrations/ | Se ejecuta automáticamente antes de que la nueva versión arranque | con el despliegue de código |
Cualquier cosa fuera de main | No ocurre nada. Las ramas y PRs no se despliegan | — |
El despliegue es cuidadoso: la nueva versión arranca junto a la anterior, las migraciones se ejecutan primero, y el tráfico se mueve solo cuando la nueva versión responde a su health check. Si la compilación, una migración o el health check fallan, la versión anterior sigue sirviendo y el sitio permanece activo. Un despliegue fallido significa "el cambio no está en vivo", no "el sitio está caído".
Para comprobar un despliegue: gh run list --workflow "Deploy to cluster" --limit 3 (o la pestaña Actions en GitHub), luego abre la URL pública. Para redesplegar sin un cambio de código: gh workflow run "Deploy to cluster", que construye una etiqueta fresca, por lo que la aplicación se reinicia con un entorno nuevo.
El contrato (lo que mantiene los despliegues funcionando)
Cada regla existe porque romperla provoca fallos silenciosos o pérdida de datos. Mantenlas, e informa al usuario si una solicitud las rompería.
- Escucha en
$PORT(el puerto en PROJECT.md, establecido por el clúster), en0.0.0.0, nolocalhost. - La imagen se construye desde
<carpeta de despliegue>/Dockerfileparalinux/amd64y se ejecuta como un usuario no root. Sin root, sin puertos por debajo de 1024, sin capacidades adicionales de Linux. - Las etiquetas de imagen tienen el formato
YYYYMMDDHHMMSS-<sha corto>. El clúster despliega el número más alto e ignora silenciosamente cualquier etiqueta que no tenga este formato. Nunca cambies el paso de la etiqueta en.github/workflows/deploy-cluster.yml. - El disco del contenedor se elimina en cada despliegue y reinicio. Nunca guardes subidas de usuarios, archivos SQLite o nada valioso en el disco local. Pon los datos en la base de datos; para archivos (fotos, vídeo), pídele al operador almacenamiento de objetos.
- Health check: el clúster sondea la ruta de salud en PROJECT.md (se recomienda
/healthz). Haz que devuelva 200 rápidamente, sin llamada a la base de datos ni renderizado de página, o una BD lenta hará que el clúster piense que la aplicación está muerta. DATABASE_URLproviene del clúster. Nunca la pongas ensecrets.env(sobrescribiría la real) y nunca la establezcas en tiempo de compilación de la imagen.- Tiempo de ejecución vs tiempo de compilación.
secrets.envllega a la aplicación solo en tiempo de ejecución. La imagen se construye en GitHub Actions sin él. Cualquier cosa incrustada en el frontend en tiempo de compilación (VITE_*,NEXT_PUBLIC_*,REACT_APP_*) no puede provenir desecrets.env: lee esos valores en el servidor en tiempo de ejecución y pásalos a la página, o (si no son secretos) confírmalos en el código. - Secretos que la aplicación genera por sí misma cuando faltan (secreto de autenticación/sesión, sales de cifrado, claves de push): establécela en
secrets.env. De lo contrario, cada reinicio crea unos nuevos, los usuarios se desconectan, las suscripciones push se rompen, etc. - Las migraciones deben ser seguras para la versión anterior. Durante aproximadamente un minuto, ambas versiones se ejecutan contra el nuevo esquema. Agrega columnas y tablas libremente. Renombra o elimina en dos despliegues: primero deja de usarlo, luego elimínalo. Detalles en
references/database.md.
Variables de entorno y secretos
Toda la configuración en tiempo de ejecución reside en <carpeta de despliegue>/secrets.env, confirmado cifrado con sops. Solo los valores están cifrados; los nombres de las claves permanecen legibles. Cámbialo con el propio sops. Nunca lo descifres a un archivo que puedas confirmar, y nunca imprimas valores secretos en el chat o en los registros.
sops set deploy-data-clerk/secrets.env '["STRIPE_SECRET_KEY"]' '"sk_live_..."'
sops unset deploy-data-clerk/secrets.env '["OLD_KEY"]'
sops decrypt deploy-data-clerk/secrets.env | cut -d= -f1
Luego confirma y haz push a main; estará en vivo en ~5 min. Cuando el usuario tenga que proporcionar el valor (una clave API de algún panel), haz que ejecuten ellos mismos la línea sops set, o que peguen solo el valor en su propia terminal. La guía completa (claves, dónde viven, rotación, trampas) está en references/secrets.md.
Base de datos
Postgres 18, gestionado por el operador. La aplicación recibe su propia base de datos y usuario a través de DATABASE_URL. Los cambios de esquema son archivos de migración que se aplican solos en el despliegue (references/database.md).
Copias de seguridad: una copia de seguridad completa cada noche a las 03:00 UTC más un registro continuo de cada cambio, almacenado fuera del servidor en Google Cloud Storage. El operador puede restaurar la base de datos a cualquier momento dentro de los últimos 7 días. Así que si se eliminan datos o una migración sale mal, no te asustes ni intentes arreglarlo a mano: anota la hora en que ocurrió y pídele al operador una restauración (plantilla en references/troubleshooting.md). Una restauración retrocede toda la base de datos, por lo que los cambios realizados después de ese momento se pierden.
Datos de red que tu código puede necesitar
- Ruta del tráfico: visitante → Cloudflare → Traefik (ingress del clúster) → aplicación. Los certificados HTTPS son automáticos. La aplicación misma habla HTTP plano en
$PORT;X-Forwarded-Proto: httpsindica que el original era HTTPS. Confía en las cabeceras del proxy y no redirijas a HTTPS en la aplicación (se crearía un bucle). - IP real del visitante: lee la cabecera
CF-Connecting-IP.X-Forwarded-Forcontiene una dirección de Cloudflare, por lo que limitar la velocidad basándose en ella pondría a todos los visitantes en el mismo grupo. - Traefik agrega estas cabeceras: HSTS (2 años, preload),
X-Content-Type-Options: nosniff,X-Frame-Options: SAMEORIGIN(el sitio no se puede incrustar en un iframe en otros dominios). Cabeceras adicionales (CSP, COOP/COEP paraSharedArrayBuffer, permitir iframes) las establece el operador a petición. - El ingress no comprime las respuestas. Cloudflare comprime tipos de texto comunes; para control total, comprime en la aplicación o envía activos precomprimidos.
- Conexiones largas (SSE, websockets) están bien, pero Cloudflare corta una conexión inactiva durante ~100 s. Envía un heartbeat cada ~30 s y reconecta en el cliente.
- Saliente: la aplicación puede llamar a cualquier endpoint público de Internet en cualquier puerto. Esto incluye APIs de LLM, Stripe, correo electrónico, S3 y bases de datos gestionadas o Redis (Neon, Supabase, Atlas, Upstash) en sus endpoints públicos. No puede alcanzar redes privadas (VPC peering, PrivateLink, IPs privadas) ni otros proyectos en el clúster. El tráfico entrante solo llega a través de la URL pública.
- Servicios de terceros: pon sus claves y URLs en
secrets.env. Si un servicio necesita una lista de IP permitidas, el clúster tiene una IP saliente estática: pídesela al operador (no está publicada) e informa al usuario que se comparte con otros proyectos en el clúster. El servidor está en el centro de EE. UU., por lo que es preferible usar regiones US-central/us-east para bases de datos externas (una base de datos en la UE agrega ~100 ms por consulta). Una base de datos externa es propiedad del usuario: las copias de seguridad de Data Clerk no la cubren. Para llamadas lentas a LLM, transmite la respuesta o ejecútalas como trabajos en segundo plano; una solicitud del navegador que espere más de ~100 s se corta. - Los recursos están limitados (memoria/CPU en PROJECT.md). Superar el límite de memoria mata y reinicia la aplicación. Si eso ocurre, pídele al operador más espacio en lugar de luchar contra ello en el código.
- Se ejecuta una copia de la aplicación (una réplica). No confíes en que el estado en memoria sobreviva a un despliegue y mantén las sesiones en la BD o en cookies firmadas.
Lo que solo el operador puede hacer
El usuario no tiene acceso al clúster, y eso es por diseño. Escríbele al operador una solicitud breve (consulta references/troubleshooting.md para la plantilla) para:
nuevo dominio o subdominio · más memoria/CPU · almacenamiento de archivos/objetos · una restauración de base de datos · registros de aplicación o causas de caídas · cabeceras de seguridad modificadas · cambiar el comando de migración o inicio, puerto o ruta de salud · trabajos programados (cron) · servicios adicionales (Redis, colas) · una segunda aplicación en el mismo proyecto · incorporar un nuevo repositorio.
Más detalles
references/stacks.md: el contrato universal para cualquier lenguaje; full-stack vs SPA + API vs estático; Node, Python FastAPI, Go y otros stacks.references/new-project.md: incorporación de un repositorio (archivos a agregar, qué pedirle al operador, Dockerfile por stack).references/secrets.md: sops en profundidad, el archivo de claves, rotación, variables en tiempo de compilación.references/database.md: migraciones, cadena de conexión, copias de seguridad, correcciones de datos.references/setup-tools.md: instalación segura de sops/git/gh en macOS, Linux y Windows. Ejecutascripts/install-tools.shoscripts/install-tools.ps1.references/troubleshooting.md: "mi cambio no está en vivo", "el sitio está caído", errores comunes, la plantilla de solicitud al operador.assets/: plantillas de Dockerfile (Node, Python, Go) y plantillas para el flujo de trabajo de despliegue,kustomization.yaml,.sops.yaml,.gitattributesyPROJECT.md.