# Site estático

Um site estático não tem container de execução: a borda serve os arquivos. Pode ter um passo de build (Vite, Astro, Hugo via `package.json`) ou ser HTML puro.

## Pré-requisitos

- Token com os escopos `projects:write`, `deploy`, `env:write` e `logs:read` (veja [Token de API](/comecar/token/) e [Tokens e escopos](/seguranca/tokens-e-escopos/)). Para listar projetos e serviços, `projects:read`.
- `NEBULAR_API` e `NEBULAR_TOKEN` no ambiente; `curl` e `jq` instalados.
- Toda chamada que cria algo leva `Idempotency-Key`: repetir após timeout é seguro.

## Como a Nebular constrói este tipo de app

Com `builder: "auto"` (ou `"static"`) a Nebular decide assim (um `Dockerfile`, se existir, vence):

- Sem `package.json`, com `index.html` na raiz: publica os arquivos como estão.
- `package.json` **sem** `scripts.start`: instala (`npm ci`/`npm install`) e roda `npm run build` (ou o `build_command`).
- A pasta publicada é a primeira, nesta ordem, que contenha `index.html`: `dist`, `out`, `build`, `public`, a raiz. Nenhum `index.html` encontrado é erro.
- Não há processo para consultar variáveis em runtime: valores só entram **em tempo de build**. Por isso, nunca ponha segredo em variável de site estático: ela vai parar no bundle do browser.
- `port` e `healthcheck_path` não se aplicam ao estático.

Regras comuns a todas as stacks:

- O app precisa escutar em `0.0.0.0` na porta que o serviço declara em `port` e que a plataforma também entrega na variável `PORT`. Leia `PORT` do ambiente; declare `port` no serviço com o mesmo valor.
- O container roda sem root, com sistema de arquivos somente leitura e `/tmp` em tmpfs (64 MB). Não grave em outro lugar; para dados persistentes use um [volume](/conectados/volumes/) ou um [banco](/bancos/postgres/).
- Segredo vai em variável do serviço (`env:write`), nunca no código, nunca em `NEXT_PUBLIC_*` ou equivalente de bundle de browser.

```json
{
  "scripts": { "build": "vite build" }
}
```

## 1. Criar o projeto

O projeto é o grupo: todos os seus serviços compartilham uma rede privada. Guarde o `id` da resposta.

```bash
NAME="site-$(date +%s)-$RANDOM"
curl -sS -X POST "$NEBULAR_API/v1/projects" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $NAME-projeto" \
  -d "{\"name\": \"$NAME\"}"
```
```json
{"id": "01j9zkk07fhzv5zsmn996cdwpb", "name": "site-1760000000"}
```

| Resposta | O que fazer |
|---|---|
| `401` | Token ausente ou inválido: confira o cabeçalho; peça outro token a uma pessoa. |
| `403` | Falta `projects:write`: peça um token com o escopo. |
| `409 slug_taken` / `project_quota_exceeded` | Já existe projeto com esse slug (derivado do nome): escolha outro nome (ou repita com a mesma `Idempotency-Key` para recuperar a resposta anterior); ou o time atingiu o limite de projetos. |
| `429` | Espere `Retry-After` segundos e repita. |

## 2. Criar o serviço

O serviço `web` é público (`visibility: "public"` exige um `slug` único) e usa fonte `upload` e o builder `static`. Este bloco é autossuficiente: cria o projeto e o serviço.

```bash
NAME="site-$(date +%s)-$RANDOM"
P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $NAME-projeto" \
  -d "{\"name\": \"$NAME\"}" | jq -r .id)

curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"visibility\": \"public\",
    \"slug\": \"$NAME\",
    \"source\": {\"type\": \"upload\"},
    \"build\": {\"builder\": \"static\"},
    \"port\": 3000
  }"
```
```json
{"id": "01j9zk32py7tcbz2q0ra5aqx7k", "name": "web", "kind": "app", "visibility": "public", "port": 3000}
```

Campos úteis do serviço: `healthcheck_path` (caminho HTTP que deve responder 2xx), `start_command` e `build_command` (substituem o padrão), `resources` (`{"memory_mb": 512, "cpus": 0.5}`, valores padrão; sempre dentro de `resources`) e `auto_deploy`.

| Resposta | O que fazer |
|---|---|
| `400 invalid_request` | Corpo inválido: o `name` do serviço segue `^[a-z][a-z0-9-]{0,30}$`; `build.builder` deve ser um de `auto`, `node`, `go`, `rust`, `python`, `static`, `dockerfile`. |
| `404` | Projeto inexistente **ou** de outro time (a API não diferencia). Confira o `id`. |
| `409 name_taken` / `slug_taken` | Já existe serviço com esse nome no projeto (todo projeto nasce com um `web`), ou o `slug` está em uso. |

## 3. Variáveis

Defina as variáveis antes do deploy: elas são resolvidas na hora do deploy. O valor pode referenciar outros serviços (`${{db.DATABASE_URL}}`) ou o grupo (`${{shared.VAR}}`); veja [Variáveis e referências](/conectados/variaveis-e-referencias/).

```bash
NAME="site-$(date +%s)-$RANDOM"
P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $NAME-projeto" \
  -d "{\"name\": \"$NAME\"}" | jq -r .id)
S=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"visibility\": \"public\",
    \"slug\": \"$NAME\",
    \"source\": {\"type\": \"upload\"},
    \"build\": {\"builder\": \"static\"},
    \"port\": 3000
  }" | jq -r .id)

curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/VITE_API_URL" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \
  -d '{"value": "https://api.exemplo.com"}'
```
Resposta: `200`. Variáveis `NEXT_PUBLIC_*`, `VITE_*` e similares vão para o bundle do browser: nunca ponha segredo nelas.

| Resposta | O que fazer |
|---|---|
| `403` | Falta `env:write`. |
| `400 invalid_request` | Nome de variável inválido ou valor acima de 64 KiB. |
| `unresolved_reference` / `reference_cycle` | Aparecem no **deploy**, não aqui (o `PUT` aceita a referência): o `POST` do deploy responde `422` e nada é criado; `details.references` lista os nomes com problema (uma `${{...}}` aponta para variável inexistente ou forma ciclo). Corrija a variável citada. |

## 4. Enviar o código, fazer o deploy e esperar

Compacte a pasta do projeto (sem `node_modules`, `.git` nem arquivos `.env`) e envie como `multipart/form-data` para o serviço. A fonte `upload` não usa git: cada envio é um deploy. Se o serviço tem fonte `git` (`{"type": "git", "installation_id": "...", "repo": "org/repo", "branch": "main"}`) ou `image`, o `POST` de deploy usa a fonte configurada no serviço.

```bash
# preparo (passos 1 e 2): projeto e serviço, para o bloco rodar sozinho
NAME="site-$(date +%s)-$RANDOM"
P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \
  -d "{\"name\": \"$NAME\"}" | jq -r .id)
S=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"visibility\": \"public\",
    \"slug\": \"$NAME\",
    \"source\": {\"type\": \"upload\"},
    \"build\": {\"builder\": \"static\"},
    \"port\": 3000
  }" | jq -r .id)
H="Authorization: Bearer $NEBULAR_TOKEN"

# código: a pasta do projeto, sem node_modules, .git nem .env
ARQ=$(mktemp)
tar --exclude=node_modules --exclude=.git --exclude='.env*' -czf "$ARQ" .
RESP=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services/$S/deployments" \
  -H "$H" \
  -H "Idempotency-Key: deploy-$(sha256sum "$ARQ" | cut -c1-16)-$RANDOM" \
  -F "file=@$ARQ")
D=$(echo "$RESP" | jq -r .id)     # resposta: 202 {"id": "dep_...", "status": "queued"}

# polling com backoff: 2 s, dobrando até 30 s, no máximo 40 tentativas (~10 min)
delay=2
for i in $(seq 1 40); do
  st=$(curl -sS "$NEBULAR_API/v1/projects/$P/services/$S/deployments/$D" -H "$H" | jq -r .status)
  echo "tentativa $i: $st"
  case "$st" in
    ready) break ;;
    failed|blocked|canceled|superseded) echo "deploy terminou em $st" >&2; exit 1 ;;
  esac
  sleep "$delay"; delay=$(( delay * 2 > 30 ? 30 : delay * 2 ))
done
[ "$st" = ready ]

# a URL pública do serviço vem em public_url
curl -sS "$NEBULAR_API/v1/projects/$P/services/$S" -H "$H" | tee /dev/stderr | jq -e '.public_url | startswith("https://")' >/dev/null
```

Com o deploy `ready`, chame `<public_url>/healthz` (esperado `200`; exige DNS e o container no ar, por isso esse último passo não roda no teste).

Estados do deploy: `queued`, `building`, `deploying`, `ready` (sucesso), e os finais de falha `failed`, `blocked` (um serviço do qual este depende falhou), `canceled`, `superseded` (um deploy mais novo o substituiu). Não há webhooks: faça polling. Detalhes em [Deploys e status](/operacao/deploys-e-status/).

| Estado ou resposta | O que fazer |
|---|---|
| `failed` | Leia o log de build (passo 5). Corrija o código ou a configuração e faça um novo deploy. Não repita igual. |
| `422 unresolved_reference` ao disparar o deploy | Variável referenciada não existe: crie-a ou corrija o nome (`details.references` diz qual). |
| `422 reference_cycle` ao disparar o deploy | Duas variáveis se referenciam: quebre o ciclo. |
| `blocked` | Corrija primeiro o serviço do qual este depende. |
| `404` | O deploy ou serviço não existe ou não é seu. |
| `401` / `403` | Token inválido / sem escopo `deploy`. Pare e peça outro token. |
| `429` | Espere `Retry-After` e repita o `GET` (o polling já faz backoff). |
| `503` | Transitório: repita com backoff. |
| `quota_exceeded` | Limite de serviços, memória ou volume do plano: veja [Limites e cotas](/operacao/limites-e-cotas/). |
| `failed`: "nenhum index.html encontrado" | Gere o `index.html` em `dist`, `out`, `build`, `public` ou na raiz (confira o `build_command`). |

## 5. Logs e rollback

Log do build (deploys `failed`) e log de execução, ambos com paginação por `cursor`; precisam do escopo `logs:read`.

```bash
# preparo (passos 1 e 2): projeto e serviço, para o bloco rodar sozinho
NAME="site-$(date +%s)-$RANDOM"
P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \
  -d "{\"name\": \"$NAME\"}" | jq -r .id)
S=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"visibility\": \"public\",
    \"slug\": \"$NAME\",
    \"source\": {\"type\": \"upload\"},
    \"build\": {\"builder\": \"static\"},
    \"port\": 3000
  }" | jq -r .id)
H="Authorization: Bearer $NEBULAR_TOKEN"
D=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services/$S/deployments" \
  -H "$H" -H "Content-Type: application/json" -d '{}' | jq -r .id)

# o destino do rollback precisa ter terminado com sucesso (ready)
delay=2
for i in $(seq 1 40); do
  st=$(curl -sS "$NEBULAR_API/v1/projects/$P/services/$S/deployments/$D" -H "$H" | jq -r .status)
  echo "tentativa $i: $st"
  case "$st" in
    ready) break ;;
    failed|blocked|canceled|superseded) echo "deploy terminou em $st" >&2; exit 1 ;;
  esac
  sleep "$delay"; delay=$(( delay * 2 > 30 ? 30 : delay * 2 ))
done
[ "$st" = ready ]

# log do build do deploy D
curl -sS "$NEBULAR_API/v1/projects/$P/services/$S/deployments/$D/logs" -H "$H"

# log de execução do serviço
curl -sS "$NEBULAR_API/v1/projects/$P/services/$S/logs" -H "$H"

# rollback: volta ao deployment anterior do serviço, com as variáveis resolvidas daquela versão
curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services/$S/deployments/$D/rollback" \
  -H "$H" -H "Content-Type: application/json" -H "Idempotency-Key: rollback-$D" -d '{}'
```

O rollback cria um novo deploy; faça o polling dele como no passo 4. Veja [Rollback](/operacao/rollback/) e [Logs](/operacao/logs/).

## Veja também

- [Domínio próprio](/dominios/dominio-proprio/) e [TLS](/dominios/tls/).
- [Node.js e Next.js](/guia/nodejs-next/) para renderização no servidor.
