Buscar
Navega o ejecuta una acción

Lo que hay debajo explica qué espera nimbus de un proyecto: el fichero compose, la rama de despliegue, el workflow que firma el webhook y la forma del pipeline. Cópialo entero en el repositorio que quieras desplegar —como AGENTS.md, como skill de tu agente, o pegado en un chat— y ya sabrá qué preparar cuando le digas «nimbus».

# Desplegar en NimbusOS

Cuando el usuario diga **nimbus**, se refiere a esto: el panel de control de su home server,
en `https://joshua-server.tail44b382.ts.net`. Prepara el proyecto para que nimbus pueda desplegarlo.

## Cómo funciona

Nimbus no es un runner. Es un panel que corre comandos **dentro de la carpeta del proyecto en
el servidor**. La cadena entera es:

```
push a la rama de despliegue
  → GitHub Actions comprueba tipos y build
  → si pasa, firma un webhook HMAC-SHA256 y lo manda al panel
  → el panel ejecuta el pipeline del proyecto en su carpeta
```

Un proyecto es **una subcarpeta directa de la raíz de datos** del servidor (por defecto
`/data`), con su fichero compose dentro. El disco es la fuente de la verdad: nimbus escanea la
carpeta, no inventa registros.

## Lo que tiene que traer el repositorio

**1. Un fichero compose en la raíz.** `docker-compose.yml`, `docker-compose.yaml`, `compose.yml`
o `compose.yaml`. Reglas de la casa:

- `restart: unless-stopped` en todo lo que deba sobrevivir a un reinicio.
- Publica en **loopback**: `"127.0.0.1:8080:80"`, nunca `"8080:80"`. La entrada pública es el
  túnel de Cloudflare o `tailscale serve`, nunca la interfaz de la máquina.
- Nada de `container_name` fijo si puede chocar con otro proyecto.
- Si la pila son varios ficheros —un base y sus overlays— vale: el campo «Ficheros compose» del
  panel acepta una lista en orden, y hay un campo aparte para el `--profile`.

**2. Un `.env.example` completo, y en `.gitignore` el `.env`.** Esto no es una formalidad: **el
panel construye el formulario de variables leyendo el `.env.example`**. Lo que no esté declarado
ahí no aparece, y quien despliegue tendrá que entrar por SSH a escribirlo a mano — que es
exactamente lo que este panel existe para evitar.

Un ejemplo sirve cuando:

- **Están todas las claves**, incluidas las que en desarrollo tienen un valor por defecto.
- **Cada clave lleva encima un comentario** de una o dos líneas diciendo qué es y de dónde se
  saca. Ese comentario es la única ayuda que verá quien rellene el formulario.
- **Las secciones se marcan** con `# --- Nombre ---`; el panel las convierte en grupos.
- **Los valores son plantillas, no secretos**: `<your-supabase-url>`, no una clave de verdad. El
  panel los muestra como *placeholder* y nunca los escribe en el `.env` final.
- Si el monorepo necesita **más de un `.env`**, hay un `.env.example` al lado de cada uno. El
  panel los descubre todos y los ofrece por separado.

```
# --- Supabase ---------------------------------------------------------------
# El proyecto y su clave pública, de dash.supabase.com → Project settings → API.
SUPABASE_URL=<your-supabase-url>
SUPABASE_ANON_KEY=<your-anon-key>
# La clave de servicio. Salta RLS, así que solo en servidor.
SUPABASE_SECRET_KEY=<your-service-role-key>
```

Las credenciales van en `.env`, que **no está en el repositorio**. Nimbus lo escribe al crear el
proyecto y lo edita después desde el panel, conservando los comentarios del ejemplo.

**3. Una rama de despliegue separada de la de trabajo.** Por convención `prod`. `main` es donde
se trabaja y nunca despliega: un panel que controla el servidor no se actualiza por accidente
al hacer un merge.

**4. Un workflow que avise al panel.** En `.github/workflows/deploy.yml`:

```yaml
name: deploy

on:
  push:
    branches: [prod]
  workflow_dispatch:

concurrency:
  group: deploy-prod
  cancel-in-progress: false

jobs:
  check:
    uses: ./.github/workflows/ci.yml

  notify:
    needs: check
    runs-on: ubuntu-latest
    steps:
      - name: Avisar al panel
        env:
          URL: ${{ secrets.NIMBUS_WEBHOOK_URL }}
          SECRET: ${{ secrets.NIMBUS_WEBHOOK_SECRET }}
          SHA: ${{ github.sha }}
        run: |
          set -euo pipefail
          BODY=$(jq -nc --arg sha "$SHA" --arg ref "${{ github.ref }}" \
            '{after: $sha, ref: $ref, head_commit: {id: $sha}}')

          # HMAC-SHA256 del cuerpo crudo: exactamente lo que valida el receptor.
          SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/.*= //')

          CODE=$(curl -sS -o response.json -w '%{http_code}' -X POST "$URL" \
            -H 'content-type: application/json' \
            -H "x-hub-signature-256: sha256=$SIG" \
            --data "$BODY")

          echo "respuesta $CODE"; cat response.json; echo
          # 202 lo lanzó, 200 llegó bien pero auto_deploy está apagado.
          case "$CODE" in 200|202) ;; *) exit 1 ;; esac
```

`NIMBUS_WEBHOOK_URL` y `NIMBUS_WEBHOOK_SECRET` son secretos del repositorio. El panel te da
los dos valores en la vista del proyecto; se pegan una sola vez.

El paso `check` es deliberado: **si el build falla, el webhook no se envía**. El servidor no se
entera siquiera de que hubo un push.

## Cómo se llega al proyecto desde fuera

Aquí es donde los proyectos se desvían y donde hay que no desviarse. Son **cuatro formas y no
hay una quinta**; elige según lo que el proyecto necesite y no inventes una propia.

**Regla que no se negocia: el proyecto publica en loopback.** `"127.0.0.1:8080:80"`, nunca
`"8080:80"`. Quien termina TLS y quien decide el hostname vive fuera del proyecto. Un proyecto
que se queda el 80 y el 443 impide que exista cualquier otro en la misma máquina, y ése es el
fallo que obliga a reinventar el despliegue en cada repositorio.

| | Cuándo | Qué hace el proyecto |
| --- | --- | --- |
| **1. Solo tailnet** | Interno, sin dominio | Publicar en loopback. El servidor hace `tailscale serve`. |
| **2. Funnel** | Público sin dominio propio | Igual. El servidor hace `tailscale funnel`. |
| **3. Dominio propio** | Público con dominio | Igual. El **cloudflared compartido** del servidor enruta `hostname → 127.0.0.1:puerto`. |
| **4. Estático o Next** | Web sin backend propio | Igual: un container que sirve el build. Nada de subir ficheros a mano. |

Lo que **no** hace el proyecto: pedir el 80, pedir el 443, resolver ACME, tocar DNS, traer su
propio `cloudflared` ni su propio Caddy de borde. Un proyecto puede traer un proxy **interno**
—Caddy, nginx— cuando de verdad necesita repartir rutas entre varios containers suyos (un
websocket en `/collab`, una API en `/api`); en ese caso ese proxy es el único que publica, y
publica en loopback.

Un solo puerto por proyecto y una sola variable con la URL pública. Si el framework necesita
saber su propia URL, se deriva de esa variable, nunca se repite en cinco sitios.

### Por qué el túnel es uno y es del servidor

Solo una cosa en una máquina puede quedarse el `:443`. Un proyecto que se lo queda es un
proyecto al lado del cual no puede vivir ningún otro, y ése es exactamente el fallo que obliga a
reinventar el despliegue en cada repositorio.

El servidor corre **un** `cloudflared` en la red del host, que por eso alcanza el loopback de
cualquier proyecto. El enrutado vive en la configuración del túnel en Cloudflare, no en un
fichero de la máquina, así que dar de alta un proyecto no toca el servidor: es un *public
hostname* más apuntando a `127.0.0.1:<puerto>`, y el CNAME lo crea Cloudflare al guardarlo. No
se abre ningún puerto en el router, no hay certificado que renovar, y que la IP sea dinámica da
igual porque la conexión la abre la máquina hacia fuera.

El panel enseña los pasos exactos, con el hostname y el puerto ya puestos, en la ficha «Cómo se
llega» del proyecto.

## El pipeline

Es una lista de comandos, uno por línea, guardada en el panel. Se ejecutan **en orden y dentro
de la carpeta del proyecto**, cada uno con `bash -lc`, con `CI=1` y `NIMBUS_DEPLOY=1` en el
entorno. El de partida para un proyecto con git:

```
git fetch origin prod
git reset --hard origin/prod
docker compose up -d --build
```

`fetch` + `reset`, no `git pull`: la rama de despliegue se reescribe y un merge ahí solo puede
salir mal.

Reglas que conviene saber:

- **Un paso que falla detiene el pipeline.** Queda en `failed` con el código de salida y los
  siguientes no se ejecutan.
- **Un comando por proyecto a la vez.** El pipeline y los botones de compose comparten la misma
  cerradura, así que nada se cuela a mitad de un `up --build`.
- La salida sale en vivo por WebSocket y se guarda entera, con estado y duración.

## Si el proyecto es el propio panel

`docker compose up -d --build` como último paso **se mata a sí mismo**: compose para el
contenedor viejo antes de crear el nuevo, y el cliente que ejecuta el comando vive dentro del
viejo. El último paso tiene que lanzar el trabajo desde un contenedor efímero aparte. Para
cualquier otro proyecto, compose directo está bien.

## Lo que nimbus no hace, y no va a hacer

- Runners aislados, builds en paralelo, matrix testing. Un usuario, una máquina.
- Exponer SSH por contraseña.
- Desplegar a más de un servidor.

## Si el repositorio es privado

Las claves de despliegue de GitHub son **por repositorio**: la que tiene el panel para sí mismo
no abre ninguna otra. Antes de dar de alta el proyecto, en el servidor:

```sh
./scripts/setup-git-access.sh duenyo/repo
docker compose up -d
```

La URL se pega tal cual desde el navegador; el panel la pasa a SSH él solo cuando tiene la clave.

## Cómo se trabaja y cómo se publica

Dos ramas y nada más:

- **`main`** es donde se trabaja. **Nunca despliega.**
- **`prod`** es lo que está corriendo en el servidor.

El ciclo completo, y conviene seguirlo tal cual:

```sh
# 1. Trabajo en main
git switch main
git add -A
git commit -m "feat: lo que sea"
git push origin main            # dispara ci.yml; no despliega nada

# 2. Publicar es una pull request de main a prod
gh pr create --base prod --head main \
  --title "Lo que entra" \
  --body "Qué cambia, y cómo se verificó."

# 3. Al mergearla, deploy.yml comprueba el árbol y avisa al panel
```

**La pull request no es ceremonia: es el único sitio donde queda escrito qué se desplegó y por
qué.** El servidor no guarda esa historia; guarda la salida del pipeline. Escribe en el cuerpo
qué cambia y cómo lo comprobaste.

Después del merge, `prod` va por delante de `main` en un commit de merge. Antes de seguir
trabajando:

```sh
git switch main && git pull
```

Si el despliegue sale mal, revertir es empujar a `prod` el commit bueno; el pipeline hace
`reset --hard` a lo que haya en la rama, así que vuelve solo.

## Checklist para dejar un proyecto listo

1. Fichero compose en la raíz, publicando en `127.0.0.1` y en un solo puerto.
2. Elegida una de las cuatro formas de exposición, sin inventar una quinta.
3. `.env.example` **completo y comentado**, uno por cada `.env` que haga falta.
4. `.env` en `.gitignore`.
5. Rama `prod` creada a partir de `main`.
6. `.github/workflows/ci.yml` que compruebe tipos, lint y build.
7. `.github/workflows/deploy.yml` con el paso de arriba.
8. En el panel: **Nuevo proyecto** → pegar la URL del repositorio → rellenar el formulario de
   variables, que sale del `.env.example`.
9. Copiar `NIMBUS_WEBHOOK_URL` y `NIMBUS_WEBHOOK_SECRET` en los secretos del repositorio.
10. En el panel, declarar la exposición y seguir los pasos que salen en «Cómo se llega».
11. Push a `main`, pull request a `prod`, merge.