Operación
Backups
Todo el estado del plano de control está en /var/lib/cloudweb:
| Archivo | Contenido |
|---|---|
cloudweb.db |
apps, despliegues, usuarios, configuración |
master.key |
clave que cifra los secretos — sin ella el resto no se puede descifrar |
ca.key, ca.crt |
CA del clúster (si se pierde, los nodos deben volver a unirse) |
admin.token |
token de administración de arranque |
Para un backup consistente copiá el directorio completo con el servicio detenido (systemctl stop cloudweb-server), o usá sqlite3 cloudweb.db ".backup copia.db" más una copia de master.key, ca.key y ca.crt. Guardá el backup cifrado: contiene claves. Para restaurar, copiá los archivos a un servidor nuevo con el mismo --public-host y reiniciá el servicio.
Las imágenes están en el volumen Docker cloudweb-registry-data del nodo del plano de control; se pueden reconstruir con un redeploy.
Actualizaciones
Volvé a ejecutar el instalador:
curl -fsSL https://cloudweb.zondacloud.com/install.sh | sudo sh
Descarga los binarios nuevos, verifica sus checksums, los reemplaza de forma atómica y reinicia los servicios, conservando datos y configuración. Antes de aplicar migraciones el servidor guarda una copia (cloudweb.db.bak-vN). Para volver atrás: restaurá la copia y el binario anterior. En los workers volvé a ejecutar el instalador con --join … (o simplemente el instalador sin opciones si ya están unidos).
Configuración del servidor
/etc/cloudweb/server.env (reiniciar con systemctl restart cloudweb-server):
| Variable | Descripción |
|---|---|
CLOUDWEB_PUBLIC_URL |
URL pública del dashboard (redirect OAuth y webhooks) |
CLOUDWEB_PUBLIC_HOSTS |
nombres/IPs con que los nodos alcanzan al servidor (certificado del listener de agentes) |
CLOUDWEB_BASE_DOMAIN |
dominio base de las apps |
CLOUDWEB_INGRESS_HOST |
nombre estable de ingress (p. ej. ingress.zondacloud.com), destino recomendado del CNAME de los dominios propios; ver Dominios propios |
CLOUDWEB_TLS_MODE, CLOUDWEB_ACME_EMAIL |
modo TLS y contacto Let's Encrypt |
CLOUDWEB_REGISTRY |
host:puerto del registro de imágenes interno |
Solución de problemas
| Síntoma | Qué mirar |
|---|---|
| El nodo no se une | token vencido o usado; huella --ca-sha256 incorrecta; puerto 8443 bloqueado |
Agente rechazado (control plane rejected this agent's protocol version) |
actualizá el lado indicado: agente con node.sh en el nodo, o el plano de control; ver protocolo |
| Un nodo drenado no vuelve a recibir instancias | Nodos → Reactivar (o POST /api/nodes/{id}/undrain) |
Los despliegues fallan con no node can host the instance (...) |
el mensaje nombra cada nodo y el motivo: draining (reactivalo), registry unreachable (el nodo no alcanza el registro: revisá la malla con sudo cloudweb-agent mesh status en ese nodo) o unreachable (sin latidos) |
| Una app muestra reemplazos fallidos que se repiten y luego dejan de intentarse | al reprogramar una instancia fuera de un nodo drenado o caído, si el reemplazo falla se espera cada vez más (30 s, 1 min, 2 min… hasta 15 min) y tras 5 fallos seguidos se deja de intentar; la instancia actual sigue corriendo. El motivo queda en el historial del último intento. Resolvé la causa (típicamente la malla del nodo destino) y disparás un despliegue nuevo para empezar de cero |
Un nodo en WSL2 queda degradada o nunca conecta la malla |
con la red NAT de WSL2 su endpoint es una IP interna que otras máquinas no alcanzan; ver Nodos en WSL2 |
Nodo unreachable |
journalctl -u cloudweb-agent; UDP 51820 entre nodos; hora del sistema |
| Build falla | los logs del despliegue; si no hay Dockerfile ni lenguaje reconocido, agregá un Dockerfile |
| No llegan los redeploys | el webhook en GitLab (Settings → Webhooks): la URL debe ser alcanzable desde GitLab |
| Sin HTTPS en una app | el DNS debe apuntar a un nodo de ingress y los puertos 80/443 estar abiertos |
502 en una app |
la app debe escuchar en $PORT y en 0.0.0.0 |
Logs: journalctl -u cloudweb-server y journalctl -u cloudweb-agent; las apps con docker logs cw-<app>-<n> o desde el dashboard.
Dominios propios y el nombre de ingress
Un cliente que trae su dominio crea un registro DNS en su propia zona. Si ese registro es un A a la IP de tu nodo y la IP cambia (conexión residencial, migración), cada cliente tiene que editar su DNS y vos no podés arreglarlo. El nombre de ingress evita eso:
- Elegí un nombre que controles, por convención
ingress.<dominio-base>, y apuntalo a tus nodos de ingress (registro A con las IPs públicas, o DDNS si la IP es dinámica). - Configurá
CLOUDWEB_INGRESS_HOST=ingress.zondacloud.comen/etc/cloudweb/server.envy reiniciácloudweb-server. Es opcional: sin él se recomienda el host de cada app. - Los clientes crean un CNAME de su subdominio a ese nombre. Si tu IP cambia, actualizás un registro. Para el dominio raíz siguen necesitando un A (o ALIAS/ANAME).
Los dominios que ya existían con CNAME al host de la app, o con registros A a tus IPs, siguen activos: activar el nombre de ingress no cambia ninguno.
Qué comprobar:
dig +short ingress.zondacloud.comdevuelve las IPs de tus nodos de ingress.- Desde otra red (datos del celular, no tu Wi-Fi: muchos routers no dejan que una máquina alcance su propia IP pública):
curl -kI https://<dominio>responde. Los puertos 80 y 443 de ese nodo tienen que estar abiertos hacia internet. - Con
CLOUDWEB_TLS_MODE=internalel certificado es de la CA local de Caddy y los navegadores lo marcan como no confiable; para dominios de clientes hace faltaauto(Let's Encrypt).
Límites:
- Cloudflare como proxy (nube naranja) en el nombre de ingress o en el dominio del cliente: Cloudflare recibe en 80/443 y reenvía a tu origen. Sirve cuando tu proveedor de internet bloquea esos puertos, pero un dominio detrás del proxy no muestra el CNAME ni tus IPs, así que la verificación lo deja pendiente.
- El dashboard avisa cuando una dirección de ingress es privada o de CGNAT (no se alcanza desde internet) o cuando un nodo se anuncia por un nombre (su IP puede cambiar). Son avisos: no bloquean agregar ni verificar un dominio.
- La plataforma no retira del DNS a un nodo caído ni balancea por sí misma: con varios nodos de ingress el reparto es el del DNS.
- Nombres reservados: no se aceptan como dominio de una app el dominio base y todo lo que está bajo él, el nombre de ingress ni los nombres propios del plano de control (URL pública, instalador de nodos, agentes).
Migrar la plataforma a otro dominio (detrás de un túnel de Cloudflare)
Ejemplo: el dashboard pasa de cp.tukit.uk a console.zondacloud.com y el instalador de cw.tukit.uk a cloudweb.zondacloud.com, mientras agents, ingress y el dominio base de las apps no cambian. Hacelo en este orden y dejá los nombres viejos funcionando durante la transición.
Túnel. En el archivo del túnel (
~/.cloudflared/<nombre>.yml) agregá los nombres nuevos antes de la regla comodín, porque gana la primera que coincide:ingress: - hostname: console.zondacloud.com # dashboard service: http://localhost:8080 - hostname: cp.tukit.uk # alias durante la transición service: http://localhost:8080 - hostname: cloudweb.zondacloud.com # instalador (la app `cw`) service: https://localhost:443 originRequest: noTLSVerify: true - hostname: "*.tukit.uk" service: https://localhost:443 originRequest: noTLSVerify: true - service: http_status:404Reiniciá
cloudflared. El DNS de cada nombre nuevo es un CNAME proxied al túnel (<id>.cfargotunnel.com):cloudflared tunnel route dns <túnel> console.zondacloud.comlo crea, pero necesita elcert.pemde esa zona (cloudflared tunnel loginy elegirzondacloud.com); también se crea a mano en el panel de Cloudflare.El instalador como app de la plataforma. El nombre
cloudweb.zondacloud.comestá fuera del dominio base, así que se agrega como dominio propio de la appcw(Dominios → Agregar dominio). Siendo administrador queda activo sin verificar el DNS (de confianza): detrás de un túnel el DNS público responde con IPs de Cloudflare y la verificación nunca pasaría. Un usuario común no puede registrar ese nombre.Servidor. En
/etc/cloudweb/server.env:CLOUDWEB_PUBLIC_URL=https://console.zondacloud.com CLOUDWEB_NODE_INSTALL_URL=https://cloudweb.zondacloud.com/node.shy
sudo systemctl restart cloudweb-server. Las sesiones abiertas en el dominio viejo no pasan al nuevo: hay que volver a entrar.Apps OAuth. Actualizá, con el dominio nuevo:
- GitLab (Applications): Redirect URI
https://console.zondacloud.com/api/auth/callback. - GitHub App: Callback URL
https://console.zondacloud.com/api/auth/callbacky Webhook URLhttps://console.zondacloud.com/api/webhooks/github.
Después probá iniciar sesión desde el dominio nuevo.
- GitLab (Applications): Redirect URI
Comprobá que responden
https://console.zondacloud.com,https://cloudweb.zondacloud.com/node.shy, todavía,https://cp.tukit.ukyhttps://cw.tukit.uk.
Qué esperar del alias y de lo que ya existe:
- Los webhooks de GitLab ya registrados en cada app apuntan a la URL vieja (
cp.tukit.uk). Siguen entregando mientras ese nombre responda. Una app nueva, o una que se vuelva a conectar, registra el webhook con la URL nueva. - Los comandos de unión que ya se copiaron usan el instalador viejo (
cw.tukit.uk): sigue sirviendo lo mismo. - No hace falta tocar los nodos: hablan con
agents.<dominio>, que no cambia. - Cuando ya no quede nada apuntando a los nombres viejos, quitá
cp.tukit.ukycw.tukit.ukdel túnel.
Cambiar el dominio base de las apps
El dominio base (CLOUDWEB_BASE_DOMAIN) es el sufijo de la dirección por defecto de cada app (<app>.<dominio-base>). Cambiarlo no mueve datos, pero las direcciones
por defecto cambian: una app que estaba en web.tukit.uk pasa a web.zondacloud.com, y la vieja deja de enrutarse. Pasos, en este orden:
Túnel. Agregá el comodín del dominio nuevo después de las reglas específicas (
console,cloudweb, los dominios de clientes), que ganan por orden:- hostname: "*.zondacloud.com" service: https://localhost:443 originRequest: noTLSVerify: trueReiniciá
cloudflared. El comodín no alcanza al dominio raíz (zondacloud.com): ese nombre ywwwtienen su propio destino.DNS. En la zona de Cloudflare, un CNAME comodín
*hacia el túnel (<id>.cfargotunnel.com, con proxy). Los registros explícitos (raíz,www,console,cloudweb) tienen prioridad sobre el comodín. El certificado universal de Cloudflare cubre*.dominiode primer nivel, que es lo que usan las apps.Servidor. En
/etc/cloudweb/server.env:CLOUDWEB_BASE_DOMAIN=zondacloud.com, ysudo systemctl restart cloudweb-server.Comprobá una app:
curl -sI https://<app>.zondacloud.com.
Qué tener en cuenta:
- Todo lo que cuelga del dominio base queda reservado: ningún usuario puede registrarlo como dominio propio, y los nombres de app como
console,cloudweb,agents,ingressowwwno se pueden elegir (para no quedarse con las direcciones de la plataforma). Un dominio propio que ya existía bajo el nuevo dominio base (comocloudweb.zondacloud.comen la appcw) sigue activo. agents.<dominio>y el nombre de ingress no se mueven solos: los nodos ya unidos se conectan aagents.tukit.uk, y cambiarlo exige volver a emitir el certificado del servidor y actualizar los nodos. Mientras tanto siguen donde estaban.
Usuarios que no son administradores: límites y contenedores restringidos
Una cuenta que no es administrador ejecuta código de terceros en tus nodos. Hoy hay estas protecciones, y es importante saber cuáles faltan:
- Tope de apps por cuenta (por omisión 3; en Ajustes → Registro de usuarios → Límites por cuenta,
0= sin tope). Los administradores no tienen tope. Al alcanzarlo, la API responde403con{"code":"quota_exceeded","quota":"apps","limit":3,"used":3}. - Memoria y CPU por contenedor (hoy los valores por defecto del scheduler, aplicados con
--memoryy--cpus). - Perfil restringido para las apps de usuarios comunes (
hardenen la orden de despliegue, que el agente aplica adocker run):--cap-drop ALL(sin capacidades de Linux salvo escuchar en puertos bajos),no-new-privileges,--pids-limit 512,--ulimit nofile=4096:4096y sin swap. Un agente anterior lo ignora. Las apps de administradores (como la propia landing) no se restringen. - Sin buildpacks: los builds con buildpacks montan el socket de Docker del nodo (equivale a ser root en él), así que solo los administradores pueden usarlos. Un usuario común necesita un Dockerfile o un sitio estático; si no, el despliegue falla con un mensaje que explica qué agregar. Los builds de Dockerfile tienen tope de tiempo y de memoria y no ven el socket.
- Nombres reservados y lista bloqueada para las apps de usuarios comunes, y dominios propios que no pueden ser de la plataforma.
Lo que todavía no existe (y por eso el registro abierto a cualquiera sigue detrás de su compuerta, que exige cuotas completas y un nodo endurecido):
- Aislamiento de red: un contenedor de usuario alcanza hoy la LAN del nodo, la malla WireGuard y el registro de imágenes (
10.77.x.x:5000, sin autenticación). No hay reglas de egreso. - Builds sin tope de tamaño: el
docker buildde un usuario tiene límite de tiempo y de memoria, pero no de tamaño del contexto ni de la imagen, y corre con la red por defecto del nodo. - Cargas de usuarios solo en nodos dedicados: el scheduler puede poner la app de un usuario en cualquier nodo, incluida tu propia máquina.
- Cuotas de memoria/CPU por usuario, límites de tasa y suspensión de cuentas.
Mientras falten, no abras el registro a desconocidos en máquinas donde hay datos que te importan: el modo recomendado es invite o las reglas de acceso (cambio add-access-rules).
Seguridad
- El token de administrador (
admin.token) es una credencial de arranque equivalente a root del clúster: protegela y, una vez creado el administrador con GitLab, usá preferentemente la sesión. La pantalla de acceso no muestra el formulario del token cuando ya hay un proveedor listo: se revela escribiendoadminen la página (fuera de un campo de texto) o abriendo/?admin. Es solo para no dejarlo a la vista: lo que protege es el token, no esa palabra. Si la instancia no tiene ningún otro modo de entrar (sin proveedor configurado, o con el setup pendiente), el formulario se muestra solo, porque si no nadie podría configurarla. - Los builds ejecutan el código de los proyectos conectados con límites de memoria y tiempo, pero asumen proyectos de confianza: no conectes repositorios que no controlás.
- Publicá el dashboard detrás de HTTPS (por ejemplo, como una app más del propio clúster).
CI con un runner propio (sin gastar minutos compartidos)
Los pipelines usan imágenes Docker (golang, node, release-cli), así que corren en cualquier máquina con Docker. La imagen
base del runner (la de los trabajos que no declaran image:) es ubuntu:24.04 y se cambia con --image (o --runner-image en node.sh);
el dashboard tiene un campo opcional para eso. Si se
acaban los minutos de los runners compartidos de GitLab, se puede registrar un runner propio:
Desde el dashboard: Ajustes → Runners de CI (GitLab) arma el comando con tu token y permite descargar el script. El propio plano de control lo sirve en
/api/runners/gitlab/install.sh(sin sesión, comonode.sh), ya con la URL de GitLab de la instancia, así que un nodo lo puede bajar directo concurl. El token solo se escribe en el navegador y en el nodo: no pasa por el servidor.
- En GitLab, en el grupo (para que sirva a todos sus proyectos): Build → Runners → New group runner. Dejá Run untagged jobs
marcado y copiá el token (
glrt-…). - En la máquina (necesita un Docker Engine propio, no Docker Desktop):
(ocurl -fsSL https://TU_DASHBOARD/api/runners/gitlab/install.sh | sudo sh -s -- --token glrt-XXXXsudo sh scripts/gitlab-runner.sh --token glrt-XXXXdesde una copia del repositorio) Instalagitlab-runnerdesde el repositorio oficial, lo registra con el executordockery lo deja como servicio. Es idempotente. - En Settings → CI/CD → Runners de cada proyecto, apagá "Enable instance runners", para que los jobs esperen a tu runner en lugar de intentar (y fallar) en los compartidos.
Seguridad: quién puede correr código en tu máquina
Un job ejecuta el código del repositorio dentro de un contenedor, y el usuario que corre el runner suele pertenecer al grupo docker
(equivale a root). Por eso:
- Marcá el runner como Protected al crearlo: solo corre pipelines de ramas y tags protegidos. Protegé la rama por defecto y el patrón
de tags
v*(Settings → Repository → Protected tags), que es lo que usan los releases. cloud-web-nodees público: sin runner protegido, un merge request de un fork ejecutaría su código en tu máquina. En Settings → CI/CD → General pipelines desactivá los pipelines de merge requests de forks.- El script no activa
privilegedni monta el socket de Docker: un job no controla el Docker del equipo. Consecuencia: el jobe2e-docker(docker-in-docker) falla; está marcadoallow_failurey esas pruebas se corren a mano conmake test-e2e. - Si querés aislar más, registrá el runner en una VM dedicada en lugar de tu equipo de trabajo.