Saltar al contenido
cloud-webv0.1.0
Documentación

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

Certificados de los dominios propios. Aunque el servidor esté en CLOUDWEB_TLS_MODE=internal (CA local de Caddy, lo normal detrás de un túnel), un dominio propio verificado (su DNS apunta directo al nodo de ingress) pide un certificado público de Let's Encrypt y, si no se puede emitir, cae a la CA interna: nunca queda sin certificado. Para que se emita, el nodo de ingress tiene que ser alcanzable desde internet en los puertos 80 y 443 (reenvío en el router) y el nombre de ingress tiene que ser solo DNS (nube gris) apuntando a tu IP pública: si resuelve a IPs de Cloudflare el tráfico entra por su proxy y nunca llega al nodo. Los dominios activados por un administrador sin verificar el DNS (los de un túnel o un proxy) y los de la plataforma siguen con el modo del servidor. Hace falta el agente ≥ 0.4.1 en el nodo 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:

  1. 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).
  2. Configurá CLOUDWEB_INGRESS_HOST=ingress.zondacloud.com en /etc/cloudweb/server.env y reiniciá cloudweb-server. Es opcional: sin él se recomienda el host de cada app.
  3. 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.com devuelve 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=internal el certificado es de la CA local de Caddy y los navegadores lo marcan como no confiable; para dominios de clientes hace falta auto (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.

  1. 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:404
    

    Reiniciá 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.com lo crea, pero necesita el cert.pem de esa zona (cloudflared tunnel login y elegir zondacloud.com); también se crea a mano en el panel de Cloudflare.

  2. El instalador como app de la plataforma. El nombre cloudweb.zondacloud.com está fuera del dominio base, así que se agrega como dominio propio de la app cw (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.

  3. Servidor. En /etc/cloudweb/server.env:

    CLOUDWEB_PUBLIC_URL=https://console.zondacloud.com
    CLOUDWEB_NODE_INSTALL_URL=https://cloudweb.zondacloud.com/node.sh
    

    y sudo systemctl restart cloudweb-server. Las sesiones abiertas en el dominio viejo no pasan al nuevo: hay que volver a entrar.

  4. 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/callback y Webhook URL https://console.zondacloud.com/api/webhooks/github.

    Después probá iniciar sesión desde el dominio nuevo.

  5. Comprobá que responden https://console.zondacloud.com, https://cloudweb.zondacloud.com/node.sh y, todavía, https://cp.tukit.uk y https://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.uk y cw.tukit.uk del túnel.

Ramas: direcciones y migración

Cada rama vinculada de una app se sirve en <app>--<rama>.<dominio base>: una sola etiqueta, porque el comodín DNS y el certificado universal de Cloudflare cubren un nivel (rama.app.dominio no tendría certificado). Por eso el nombre de una app nueva no puede contener -- (las existentes siguen funcionando; al vincular una rama se comprueba que su dirección no choque con ninguna app ni rama). La migración 0012_branches.sql crea la rama principal de cada app existente y deja el historial y los dominios apuntando a ella: no cambia nada de lo que ya está servido. Un dominio propio apunta a una sola rama. El túnel o el proxy deben aceptar los nombres *.<dominio base> como ya hacen para las apps.

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:

  1. 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: true
    

    Reiniciá cloudflared. El comodín no alcanza al dominio raíz (zondacloud.com): ese nombre y www tienen su propio destino.

  2. 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 *.dominio de primer nivel, que es lo que usan las apps.

  3. Servidor. En /etc/cloudweb/server.env: CLOUDWEB_BASE_DOMAIN=zondacloud.com, y sudo systemctl restart cloudweb-server.

  4. 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, ingress o www no se pueden elegir (para no quedarse con las direcciones de la plataforma). Un dominio propio que ya existía bajo el nuevo dominio base (como cloudweb.zondacloud.com en la app cw) sigue activo.
  • agents.<dominio> y el nombre de ingress no se mueven solos: los nodos ya unidos se conectan a agents.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 responde 403 con {"code":"quota_exceeded","quota":"apps","limit":3,"used":3}.
  • Tope de ramas por app (por omisión 3, contando la principal; mismo lugar, 0 = sin tope; sin tope para administradores): cada rama vinculada es un despliegue activo que consume nodo. Al alcanzarlo, 403 con "quota":"branches".
  • Despliegues por hora (por omisión 20 por cuenta, el mismo lugar; 0 = sin tope; los administradores no tienen): cuenta los que se hacen por push o a mano (no rollbacks ni reprogramados). Al alcanzarlo, la API responde 429 con Retry-After y {"code":"rate_limited"}; el webhook responde 429 y el proveedor reintenta más tarde.
  • Intentos de acceso por origen: 30 por hora desde una misma dirección (detrás del túnel de Cloudflare se usa CF-Connecting-IP, y sólo cuando la conexión llega por loopback). Frena el alta masiva de cuentas; pasado el tope el acceso muestra «demasiados intentos».
  • Memoria y CPU por contenedor (hoy los valores por defecto del scheduler, aplicados con --memory y --cpus).
  • Perfil restringido para las apps de usuarios comunes (harden en la orden de despliegue, que el agente aplica a docker run): --cap-drop ALL (sin capacidades de Linux salvo escuchar en puertos bajos), no-new-privileges, --pids-limit 512, --ulimit nofile=4096:4096 y 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.

Aislamiento de cargas de usuarios (nodo dedicado). Un nodo se declara apto para alojar código de otras personas con node.sh --tenant-node (o cloudweb-agent run --tenant-isolation). Eso modifica el firewall del nodo: crea la red Docker cloudweb-tenants (sin tráfico entre contenedores, interfaz cwtenant0, subred 172.30.77.0/24 configurable con --tenant-subnet) y escribe reglas en DOCKER-USER e INPUT que a los contenedores y a los builds de usuarios les cierran: la malla WireGuard, el registro de imágenes y toda la LAN (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, CGNAT), 127.0.0.0/8, los metadatos de la nube (169.254.0.0/16), el correo saliente (25 y 465) y el propio nodo (SSH, el agente…). Les deja internet, las respuestas a lo que entra por el puerto publicado y el DNS del nodo. Los contenedores usan DNS públicos (1.1.1.1 y 9.9.9.9). Los builds de usuarios corren en esa misma red con el builder clásico de Docker (BuildKit no admite redes con nombre), con tope de tiempo, de memoria, de tamaño del código (512 MB) y de la imagen (2 GB), y sin las credenciales del nodo. Un Dockerfile de usuario que se base en una imagen del registro de la plataforma se rechaza. Las reglas se reaplican si algo las borra, y si el aislamiento no se puede comprobar el despliegue de un usuario falla en lugar de correr sin él. Si el nodo se reinicia, las reglas se recrean al arrancar el agente (unos segundos con los contenedores ya levantados por Docker); las apps de usuarios que ya corrían en la red por defecto antes de activar esto no se mueven: se vuelven a desplegar.

Registro de imágenes autenticado. En Ajustes → Registro de usuarios → Registro de imágenes, «Exigir autenticación al registro». El plano de control genera una credencial y la entrega, por mTLS, a todos los agentes; el nodo que sirve el registro lo recrea con htpasswd y los demás inician sesión. Sólo se puede activar cuando todos los nodos tienen un agente que lo entiende (el panel nombra los que faltan actualizar); mientras esté activo, un nodo con agente viejo no recibe despliegues.

Lo que todavía no existe (y por eso el registro abierto a cualquiera sigue detrás de su compuerta):

  • Prueba en un nodo real: las reglas están probadas con tráfico real entre namespaces de red y con Docker real (make test-isolation-e2e en el repo del agente), pero falta la pasada en el nodo dedicado. Para eso hay una app «sonda» en tests/abuse-probe/ (ver su README): se despliega con una cuenta que no sea administradora y su página dice qué logró y qué no (LAN, malla, registro, metadatos, correo, capacidades, tope de procesos y de memoria).
  • Cuotas de memoria/CPU por usuario (hoy cada instancia usa los valores por omisión del scheduler).
  • --tmpfs /tmp y remapeo de usuarios (userns) en los contenedores.

La compuerta del registro abierto mide esto: exige un tope de apps mayor que 0, la autenticación del registro activa y al menos un nodo listo cuyo agente informe la capacidad tenant-isolation (que informa sólo mientras comprueba en el propio nodo la red aislada con sus reglas y que el registro autenticado está en uso). Con el registro abierto, el código de usuarios comunes sólo se programa en esos nodos y un nodo sin la capacidad queda excluido con el motivo a la vista.

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).

Límites propios de una cuenta

Los límites de Ajustes → Registro de usuarios → Límites por cuenta valen para todas las cuentas que no son administradoras. Para darle más (o menos) a una cuenta: Usuarios → Límites de esa cuenta. Se pueden fijar por separado las apps, las ramas por app y los despliegues por hora; lo que no cambies sigue usando el límite general, 0 es «sin tope» para esa cuenta y Volver a los generales quita los propios. La columna Límites muestra el efectivo y marca «propios» a los que fijaste vos. Cambiar los límites generales después no pisa los propios. Queda en la auditoría (user.quotas). API: PUT /api/admin/users/{id}/quotas con {"max_apps": 10} (un campo ausente no cambia, null quita el propio).

Términos de uso y contacto de abuso

En Ajustes → Términos de uso y contacto de abuso se cargan la dirección (https) de la página de términos y un correo o dirección de contacto para reportar abusos. Se muestran en el pie de toda página del panel (también sin sesión) y la pantalla de acceso los menciona cuando el registro es abierto («al entrar aceptás los términos»). El texto de los términos lo redactás vos: la plataforma sólo lo enlaza. Antes de abrir el registro a cualquiera conviene tenerlos.

Monitoreo (gráficos de uso)

Monitoreo en la consola muestra a los administradores cómo está la plataforma (los usuarios comunes ven la vista de su cuenta, más abajo): tarjetas de resumen (nodos sanos, CPU, memoria y disco del clúster, apps en ejecución, despliegues de 24 h con sus fallos y cuentas), un panel por nodo con CPU, memoria y disco en el tiempo, las apps que más consumen (por memoria y por CPU, con su dueño y un enlace a la app) y los despliegues de los últimos 7 días con la causa de fallo más común. El rango es de 1 hora, 6 horas, 24 horas o 7 días y la página se actualiza sola cada 30 segundos mientras está a la vista. Cada gráfico tiene debajo una tabla «Ver los datos» y un resumen en texto, y se recorre con las flechas del teclado.

Cómo funciona: cada agente (versión 0.5 o posterior) toma una muestra cada 30 segundos (CPU del nodo desde /proc/stat, memoria usada = MemTotal - MemAvailable, disco usado del directorio de datos y, con una sola llamada a docker stats, CPU y memoria de cada contenedor de app) y la manda en el latido. El servidor guarda las muestras crudas 24 horas y los promedios de 5 minutos 7 días (después se borran); una app sólo se mide mientras corre. Con 3 nodos y 50 instancias son unos pocos MB en la base. La CPU de una app se muestra en núcleos usados (1,0 = un núcleo entero), no en porcentaje, para poder compararlas entre nodos de distinto tamaño.

Cómo leerlo:

  • «Sin datos» no es cero. Un nodo dice por qué: agente sin actualizar (ejecutá node.sh en el nodo), todavía sin muestras (nodo nuevo o agente recién reiniciado) o el nodo no responde. Un hueco en un gráfico es un período sin muestras: la línea se corta, no baja a cero.
  • Detectar un abuso: mirá Apps que más consumen con el dueño al lado. Una app de una cuenta común que sostiene CPU alta o se acerca todo el tiempo a su límite de memoria es la candidata; abrí su página para ver los logs y, si corresponde, suspendé la cuenta desde Usuarios (ver abajo).
  • Dimensionar: la memoria o el disco de un nodo cerca del tope sostenido es la señal para sumar un nodo; los despliegues que fallan por insufficient capacity son la otra.
  • Las causas de fallo se agrupan sin números ni identificadores: sirven para ver tendencias, no son una clasificación exacta.

Sin alertas ni exportación a Prometheus: son otros cambios. La retención (24 h y 7 días) es fija. Un administrador puede abrir la vista de cualquier app desde Apps que más consumen («monitoreo de la app»).

Lo que ve cada usuario

Quien no es administrador entra a Monitoreo y ve solo lo suyo (lo ajeno responde como si no existiera):

  • Su cuenta contra sus límites: apps, despliegues de la última hora (con la hora en que se libera un cupo si llegó al tope) y los de 7 días con sus fallos. Los límites son los de la cuenta (los propios si un administrador se los fijó, si no los generales).
  • Una fila por app con estado, tiempo en línea de 24 h, CPU y memoria de ahora, pedidos y errores 5xx de la última hora, y enlaces a su monitoreo y sus logs.
  • /apps/<id>/monitoring: estado, tiempo en línea, reinicios, CPU y memoria de cada instancia (la memoria contra su límite) y el tráfico (pedidos, errores 4xx y 5xx, latencia media y porcentaje de pedidos de más de 1 s). El usuario no ve nombres de nodo.
  • «Tiempo en línea observado» es el porcentaje del rango en que la plataforma tuvo un despliegue de la rama principal en running, calculado del historial de despliegues. No es un SLA ni se promete: si el agente de un nodo se cae, el despliegue sigue running hasta que el servidor lo reprograme.

Cómo se mide el tráfico

Caddy (el ingress) escribe su registro de accesos por un socket local al agente del nodo; el agente toma solo el host, el código de respuesta y la duración de cada pedido y los cuenta por host de las rutas conocidas (un Host inventado no cuenta nada). Cada 30 s manda los contadores del intervalo en el latido (metrics.traffic) y el servidor los suma por app: un dominio propio suma a su app, y varios nodos de ingress se suman. Nunca se guardan ni se mandan direcciones IP, rutas, parámetros ni cabeceras. Se guardan 24 h crudos y 7 días en baldes de 5 minutos, igual que el uso. Hace falta el agente nuevo (capacidad traffic-metrics) en el nodo de ingress; sin él la pantalla dice «todavía no se mide el tráfico». No usa las métricas por host de Caddy a propósito: etiquetan cualquier Host que llegue y harían crecer su memoria sin tope.

Logs de las apps

/apps/<id>/logs es la pantalla de logs: sigue la salida en vivo (con pausa y reanudación sin perder líneas), filtra por tipo (build o ejecución), busca texto (subcadena sin distinguir mayúsculas, hasta 200 caracteres) y descarga un .log. Los valores de las variables de entorno se enmascaran como *** antes de guardarse, así que ni la vista, ni la búsqueda ni la descarga pueden mostrarlos. Retención: se conservan las últimas 5.000 líneas de cada despliegue (las más viejas se descartan) y los logs de los despliegues terminados se borran a los 14 días; un despliegue en ejecución solo pierde líneas por el tope. La pantalla lo dice. La poda corre cada 2 minutos, por lotes. Para conservar un log más tiempo, descargalo.

Suspender cuentas y apps

En Usuarios (sólo administradores) se ve cada cuenta con su proveedor, rol, cantidad de apps y estado. Suspender una cuenta (con un motivo que ve la persona) cierra todas sus sesiones, detiene sus apps y las deja de servir (también sus dominios), y la cuenta no puede volver a entrar (el login responde «cuenta suspendida»). Una push a sus repositorios no despliega nada. Se revierte con Reactivar: la cuenta vuelve a entrar y se vuelven a desplegar las apps que cayeron con ella, no las que se suspendieron aparte. Una app también se suspende sola desde su página (Administración) y se reactiva ahí. A un administrador no se lo suspende (se le quita el rol primero), ni uno mismo.

Cada suspensión, reactivación y cambio de política de registro, de límites o de la autenticación del registro queda en la Auditoría (misma pantalla, o GET /api/admin/audit): quién, qué, sobre qué y cuándo. API: GET /api/admin/users, POST /api/admin/users/{id}/suspend|unsuspend, POST /api/admin/apps/{id}/suspend|unsuspend.

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 escribiendo admin en 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, como node.sh), ya con la URL de GitLab de la instancia, así que un nodo lo puede bajar directo con curl. El token solo se escribe en el navegador y en el nodo: no pasa por el servidor.

  1. 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-…).
  2. En la máquina (necesita un Docker Engine propio, no Docker Desktop):
    curl -fsSL https://TU_DASHBOARD/api/runners/gitlab/install.sh | sudo sh -s -- --token glrt-XXXX
    
    (o sudo sh scripts/gitlab-runner.sh --token glrt-XXXX desde una copia del repositorio) Instala gitlab-runner desde el repositorio oficial, lo registra con el executor docker y lo deja como servicio. Es idempotente.
  3. 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-node es 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 privileged ni monta el socket de Docker: un job no controla el Docker del equipo. Consecuencia: el job e2e-docker (docker-in-docker) falla; está marcado allow_failure y esas pruebas se corren a mano con make test-e2e.
  • Si querés aislar más, registrá el runner en una VM dedicada en lugar de tu equipo de trabajo.