# Nebular — documentação completa (texto puro) Fonte: https://docs.nebularws.com. Ordem: tutorial (começar, guias, referência). --- URL: https://docs.nebularws.com/index.md # Nebular Docs A Nebular é uma plataforma de hospedagem. Esta documentação é escrita para **pessoas e para agents de IA**: cada página existe também em Markdown cru (acrescente `.md` à URL) e todo o conteúdo está em texto puro em [`/llms-full.txt`](/llms-full.txt). ## Usar com um agent 1. Crie um [token de API](/comecar/token/) (`nbk_…`). 2. Cole no agent o conteúdo de `https://docs.nebularws.com/llms-full.txt` (ou aponte para `https://docs.nebularws.com/llms.txt`) e o token como segredo do ambiente, nunca no texto do prompt. 3. Peça a tarefa. Veja [Como um agent usa isto](/comecar/como-um-agent-usa-isto/) e [Para agents](/agents/). ## O que a API cobre A API pública `/v1` cobre **projetos, serviços, deploys, variáveis com referência, rede privada, volumes, bancos, domínios, templates (WordPress), galerias e arquivos**. Os guias descrevem o contrato v13; páginas ainda marcadas “em construção” (referência gerada de rotas, isolamento) não têm API publicada: não invente rotas. ## Mapa | Área | Conteúdo | Estado | |---|---|---| | [Começar](/comecar/token/) | token, conceitos, convenções da API | real | | [CDN e imagens](/cdn/visao-geral/) | galerias, arquivos, transformação, cache | real | | [Erros](/erros/tabela/) | código, HTTP, causa e ação do agent | real | | [Segurança](/seguranca/tokens-e-escopos/) | tokens, escopos, abuso | parcial | | [Operação](/operacao/limites-e-cotas/) | limites e cotas, [como escrever um app pronto para escalar](/operacao/app-pronto-para-escalar/) | parcial | | [Referência da API](/referencia/autenticacao/) | gerada da OpenAPI | parcial | | [Guias por stack](/guia/go/), [bancos](/bancos/postgres/), [domínios](/dominios/dominio-proprio/), [receitas](/receitas/do-zero-ao-ar-com-um-agent/) | deploy ponta a ponta | real (contrato v13) | --- URL: https://docs.nebularws.com/comecar/token.md # Token de API Para usar a API `/v1` você precisa de um **token de API** (`nbk_…`). Ele pertence ao time, não a uma pessoa. ## Criar o token 1. No painel: **Time → Configurações → Tokens de API → Criar token**. Exige papel `admin` do time e confirmação recente de identidade (step-up). 2. Escolha o nome, os **escopos** e, opcionalmente, as galerias permitidas (por padrão, todas as do time): | Escopo | Permite | |---|---| | `assets:read` | listar galerias e arquivos, consultar detalhes e status de varredura | | `assets:write` | enviar, versionar e excluir arquivos | | `galleries:manage` | criar galerias (um token restrito a galerias específicas **não** cria galeria: 403) | Escopos de deploy, serviços, variáveis, domínios, volumes, bancos, logs e templates: | Escopo | Permite | |---|---| | `projects:read` | listar e ler projetos, serviços, deploys, domínios e bancos | | `projects:write` | criar, editar e apagar projetos e serviços | | `deploy` | disparar deploy e rollback, enviar fonte | | `env:read` | ler nomes e valores **não sensíveis** de variáveis | | `env:write` | criar, alterar e apagar variáveis | | `env:reveal` | ver valores sensíveis (o token precisa ter sido criado com step-up **e** com este escopo explícito) | | `domains:write` | anexar e remover domínios | | `volumes:write` | criar e apagar volume | | `databases:write` | criar, apagar, rotacionar credencial, backup e restore de bancos | | `logs:read` | ler logs de build e de runtime | | `templates:deploy` | instanciar templates (ex.: WordPress) | Um token também pode ser restrito a projetos específicos (`project_ids`). Detalhes em [Tokens e escopos](/seguranca/tokens-e-escopos/). 3. **O token (`nbk_…`) é mostrado uma única vez.** Guardamos só o hash. Perdeu? Revogue e crie outro. 4. Validade: 90 dias por padrão, máximo 1 ano. O token pertence ao **time** (não a uma pessoa) e nunca dá acesso a membros ou cobrança. Pode ser revogado a qualquer momento. 5. Envie **somente** no cabeçalho `Authorization: Bearer nbk_…`. **Nunca em query string** (a requisição é recusada com 401). **Cookies são ignorados** nestas rotas. Variáveis usadas nos exemplos: ```bash export NEBULAR_API=https://api.nebularws.com # base URL export NEBULAR_TOKEN=nbk_... # o seu token export GALLERY=marcas-k7p2 # a "key" de uma galeria (veja [Galerias e arquivos](/cdn/galerias/)) ``` Todas as rotas ficam em `/v1` e o corpo de erro tem sempre o formato `{"error": "", "message": "..."}`: trate pelo `error` (estável), mostre o `message` (pt-BR) para pessoas. ## Verificação Confirme o token e veja seus escopos e limites com `GET /v1/me` (espera `200`): ```bash curl -sS "$NEBULAR_API/v1/me" \ -H "Authorization: Bearer $NEBULAR_TOKEN" ``` ```json {"team": {"id": "01j9zk8xwdtry8pxzy0qqvb8k3", "name": "Acme", "plan": "hobby"}, "token": {"id": "01j9zksf51s25jd7735q8fefyf", "name": "agente-deploy", "scopes": ["projects:read", "projects:write", "deploy"], "project_ids": null, "expires_at": "2026-11-10T12:00:00Z"}} ``` A resposta traz o time (`team`: `id`, `name`, `plan`) e o token (`token`: `id`, `name`, `scopes`, `project_ids` — `null` quando vale para todos os projetos — e `expires_at`). Os limites do time estão em `GET /v1/limits`. Ou, só para galerias, listando as galerias do time (espera `200`): ```bash curl -sS "$NEBULAR_API/v1/galleries" \ -H "Authorization: Bearer $NEBULAR_TOKEN" ``` ## Se algo falhar | Resposta | O que fazer | |---|---| | `401 unauthorized` | Token ausente, expirado, revogado ou enviado fora do cabeçalho. Não repita: confira o cabeçalho ou peça um token novo a um admin. | | `403 forbidden` | O token não tem o escopo da rota. Peça um token com o escopo certo. | Veja a [tabela completa de erros](/erros/tabela/) e [Tokens e escopos](/seguranca/tokens-e-escopos/). --- URL: https://docs.nebularws.com/comecar/conceitos.md # Conceitos ## Visão geral ```mermaid flowchart LR T[Time] --> K[Token nbk_] T --> PR[Projeto = grupo] PR --> SV[Serviço app] PR --> DB[Serviço database] PR --> SH[Variáveis shared] SV --> VAR[Variáveis] SV --> VOL[Volume] SV --> DOM[Domínio] SV --> DEP[Deploy] T --> G[Galeria] G --> A[Arquivo] ``` ## Time e token - **Time**: dono dos recursos e dos tokens. Um token pertence ao time, nunca a uma pessoa. - **Token de API** (`nbk_…`): credencial com **escopos** e, opcionalmente, restrita a alguns projetos (`project_ids`) ou galerias. Veja [Token de API](/comecar/token/) e [Tokens e escopos](/seguranca/tokens-e-escopos/). ## Projeto (grupo) Um **projeto** é um grupo de serviços. Todos os serviços do projeto ficam numa **rede privada** própria (nunca alcançam outro projeto) e têm em comum as **variáveis `shared`** do projeto, que cada serviço usa referenciando-as com `${{shared.NOME}}` (não entram sozinhas). Veja [Rede privada](/conectados/rede-privada/). ## Serviço Um **serviço** pertence a um projeto. Campos principais: | Campo | Significado | |---|---| | `name` | `^[a-z][a-z0-9-]{0,30}$`, único no projeto; é o hostname na rede privada. Imutável. | | `kind` | `app` (seu código ou imagem) ou `database` (criado pela rota de bancos). Imutável. | | `visibility` | `public` (recebe `slug` e URL pública) ou `private` (só na rede do projeto, sem rota pública). **Se omitida, o padrão é `private`**: informe `public` e um `slug` para ter URL. | | `source` | `{"type":"upload"}`, `{"type":"git","installation_id","repo","branch","root_dir","watch_paths"}` ou `{"type":"image","ref"}`, com `ref` = `repo[:tag]@sha256:` para fixar a versão ([Imagem pública](/guia/imagem-publica/#fixar-por-digest-e-como-obter-o-digest)). | | `build` | `{"builder":"auto\|node\|go\|rust\|python\|static\|dockerfile","dockerfile_path":"Dockerfile"}`. | | `port` | porta interna (padrão 3000). | | `healthcheck_path`, `start_command`, `build_command` | opcionais. | | `resources` (`memory_mb`, `cpus`) | recursos (padrão 512 MB e 0.5 CPU; máximo por plano). Veja [Escala e recursos](/operacao/escala-e-recursos/). | | `auto_deploy` | deploy automático por git. | Todo serviço roda como não-root, com sistema de arquivos somente leitura (só `/tmp` e o volume são graváveis). Veja [Isolamento](/seguranca/isolamento/). ### O serviço `web` padrão Todo projeto novo nasce com um serviço `web` (`kind=app`, fonte `upload`, `build.builder=auto`, porta 3000). Ele nasce **`public`**, já com `slug` e `public_url` gerados (por exemplo `web-br5pg0` e `https://web-br5pg0.`), mesmo que o padrão de `visibility` ao criar outros serviços seja `private`. Use `PATCH .../services/web` para adaptá-lo (fonte, build, porta) em vez de criar outro; um `POST` com `name: "web"` dá `409 name_taken`. Para torná-lo privado: `PATCH .../services/web` com `{"visibility": "private"}`. O `slug` e a `public_url` desaparecem e ele passa a ser alcançável só na rede do projeto. Voltando a `public` sem informar `slug`, a plataforma deriva um do nome. ```bash P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d "{\"name\": \"web-padrao-$(date +%s)-$RANDOM\"}" | jq -r .id) # o web padrão nasce público, com slug e public_url curl -sS "$NEBULAR_API/v1/projects/$P/services" -H "Authorization: Bearer $NEBULAR_TOKEN" \ | jq -e '.items[] | select(.name == "web") | .visibility == "public" and (.slug | length) > 0 and (.public_url | length) > 0' >/dev/null # tornar privado curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d '{"visibility": "private"}' ``` ## Variáveis e referências Variável: escopo `service` (só aquele serviço) ou `shared` (guardada no projeto; só chega ao serviço que a referencia com `${{shared.NOME}}`). O valor pode referenciar outro serviço com `${{servico.VAR}}` ou o grupo com `${{shared.VAR}}`; a resolução acontece no deploy. Veja [Variáveis e referências](/conectados/variaveis-e-referencias/). ## Volume Disco persistente de um serviço (1 a 50 GB, 1 por serviço), montado em um caminho absoluto; sobrevive a redeploy. Veja [Volumes](/conectados/volumes/). ## Banco Serviço `kind=database` com motor `postgres`, `mariadb` ou `sqlite`, num nó de banco gerenciado. Entrega variáveis prontas (`DATABASE_URL` etc.) para referenciar. Veja [PostgreSQL](/bancos/postgres/), [MariaDB](/bancos/mariadb/), [SQLite](/bancos/sqlite/). ## Domínio Todo serviço público tem um subdomínio gerado; você pode anexar domínio próprio. Veja [Subdomínio gerado](/dominios/subdominio-gerado/) e [Domínio próprio](/dominios/dominio-proprio/). ## Deploy Um **deploy** constrói e sobe uma versão de um serviço (ou do grupo inteiro, em ordem topológica das referências). Estados: `queued`, `building`, `deploying`, `ready`, `failed`, `blocked`, `canceled`, `superseded`. O agent acompanha por polling com backoff. Veja [Deploys e status](/operacao/deploys-e-status/). ## Galeria e arquivo (CDN) - **Galeria**: pasta de arquivos do time servida pela CDN, com `key` pública e imutável. - **Arquivo** (asset): um nome dentro de uma galeria; enviar o mesmo nome cria uma **versão**. - **URL imutável** e **URL alias**: veja [Visão geral da CDN](/cdn/visao-geral/). - **Varredura**: todo arquivo é varrido antes de publicar: [Classes de arquivo e varredura](/cdn/classes-de-arquivo-e-varredura/). --- URL: https://docs.nebularws.com/comecar/primeiro-deploy.md # Primeiro deploy Este é o fluxo base de todos os [guias por stack](/guia/nodejs-next/). Escopos do token: `projects:write`, `deploy`, `projects:read` (veja [Token de API](/comecar/token/)). Todo `POST` que cria leva `Idempotency-Key`; repetir a mesma chave com o mesmo corpo devolve a mesma resposta. Cada bloco abaixo é autossuficiente (variáveis `P` = id do projeto, `S` = id do serviço, `D` = id do deploy). ## 1. Criar o projeto Um projeto é um grupo de serviços que compartilham rede e podem compartilhar variáveis por referência ([Conceitos](/comecar/conceitos/)). ```bash curl -sS -X POST "$NEBULAR_API/v1/projects" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pd-proj-$(date +%s)-$RANDOM" \ -d "{\"name\": \"meu-app-$(date +%s)-$RANDOM\"}" ``` ```json {"id": "01j9zkk07fhzv5zsmn996cdwpb", "name": "meu-app-1760000000"} ``` Guarde `id` (`P`). Em erro: | Resposta | O que fazer | |---|---| | `401` | Token ausente ou inválido. Pare e confira o cabeçalho. | | `403 forbidden` | Falta o escopo `projects:write`. Peça um token novo. | | `409` (ex.: `slug_taken`, `project_quota_exceeded`) | Troque o nome ou apague um projeto que não usa mais. | ## 2. Criar o serviço Duas fontes (`source`) cobrem o primeiro deploy: **imagem pública** ou **upload** de código. Para `visibility: "public"` informe o `slug` (sai em `https://.`); omitido, a plataforma deriva um do nome. O `web` que todo projeto traz nasce **público**, já com `slug` e `public_url` (para privado: `PATCH` com `{"visibility": "private"}`; veja [Conceitos](/comecar/conceitos/#o-serviço-web-padrão)). ### Opção A: imagem pública ```bash SUF=$(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: pd-proj2-$SUF" -d "{\"name\": \"meu-app-$SUF\"}" | 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\": \"meu-app-$SUF\", \"source\": {\"type\": \"image\", \"ref\": \"ghcr.io/org/app:1.2\"}, \"port\": 8080, \"healthcheck_path\": \"/healthz\" }" ``` ```json {"id": "01j9zk3jremq8fkd3sjt7h300r", "name": "web", "kind": "app", "visibility": "public", "port": 8080} ``` ### Opção B: upload de código Use `"source": {"type": "upload"}` e um `build` (`builder` = `auto`, `node`, `go`, `rust`, `python`, `static` ou `dockerfile`). Sem `build`, vale `auto`. ```bash SUF=$(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: pd-proj3-$SUF" -d "{\"name\": \"meu-site-$SUF\"}" | 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\": \"meu-site-$SUF\", \"source\": {\"type\": \"upload\"}, \"build\": {\"builder\": \"auto\"} }" ``` ```json {"id": "01j9zkngmvzxkw57zdyzz80b5e", "name": "web", "kind": "app", "visibility": "public", "port": 3000} ``` Regras do `name`: `^[a-z][a-z0-9-]{0,30}$`, único no projeto (é o hostname na [rede privada](/conectados/rede-privada/)). `port` padrão: 3000. | Resposta | O que fazer | |---|---| | `400 invalid_request` | `name`, `source.type`, `build.builder` ou `healthcheck_path` inválidos: corrija o campo citado em `message`. | | `404 not_found` | O projeto não existe ou não é do seu time (as duas situações são indistinguíveis). | | `409 name_taken` / `slug_taken` | `name` ou `slug` já usado (todo projeto já nasce com um serviço `web`: use `PATCH` nele ou escolha outro nome). Troque. | ## 3. Disparar o deploy Resposta imediata: o deploy fica `queued` e o build roda em seguida. Guarde o `id` (`D`). ```bash # preparo (passos 1 e 2): projeto e serviço de imagem SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"pd4-$SUF\"}" | jq -r .id) S=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d '{"source": {"type": "image", "ref": "ghcr.io/org/app:1.0"}}' | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services/$S/deployments" \ -H "$H" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pd-dep-$SUF" \ -d '{}' ``` ```json {"id": "01j9zkv1t9ndd76xmr8ahxee7p", "status": "queued"} ``` Com `source.type = "upload"`, envie o código como `multipart/form-data` nesta mesma rota (no lugar do corpo `{}`): `-F "file=@fonte.tar.gz"`. Com `git`, o deploy usa o repositório e o branch do `source`. | Resposta | O que fazer | |---|---| | `403 forbidden` | Falta o escopo `deploy`. | | `409 deploy_in_progress` | Já existe um deploy em andamento para o serviço: consulte-o em vez de disparar outro. | | `unresolved_reference` / `reference_cycle` | Variável referenciada não existe ou há ciclo: veja [Variáveis e referências](/conectados/variaveis-e-referencias/). | | `429` | Espere `Retry-After` ([Rate limit e retry](/erros/rate-limit-e-retry/)). | ## 4. Esperar o deploy (polling com backoff) Não há webhooks neste ciclo. Consulte `GET .../deployments/{d}` começando em 2 s e dobrando até 30 s, por no máximo cerca de 10 minutos. Terminal de sucesso: `ready`. Terminais de falha: `failed`, `blocked`, `canceled`, `superseded`. ```bash # preparo (passos 1 a 3): projeto, serviço de imagem e deploy SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"pd5-$SUF\"}" | jq -r .id) S=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d '{"source": {"type": "image", "ref": "ghcr.io/org/app:1.0"}}' | jq -r .id) D=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services/$S/deployments" -H "$H" \ -H "Content-Type: application/json" -d '{}' | jq -r .id) 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 ] ``` Em `failed`, leia o [log de build](/operacao/logs/). Em `blocked`, um serviço do qual este depende falhou ([Deploys e status](/operacao/deploys-e-status/)). Em `superseded`, um deploy mais novo tomou o lugar: acompanhe o mais novo. ## 5. Verificar a URL pública Com `ready`, o serviço público responde em `https://.`, onde `` é o que você enviou na criação. ```bash curl -sS -o /dev/null -w '%{http_code}\n' "https://meu-app-1760000000.nebularws.com/healthz" ``` Se não for `200`, leia os [logs de runtime](/operacao/logs/); para voltar à versão anterior, [rollback](/operacao/rollback/). Para domínio próprio, [Domínio próprio](/dominios/dominio-proprio/). ## Listar o que você criou ```bash curl -sS "$NEBULAR_API/v1/projects" \ -H "Authorization: Bearer $NEBULAR_TOKEN" ``` ```json {"items": [{"id": "01j9zkk07fhzv5zsmn996cdwpb", "name": "meu-app-1760000000"}], "next_cursor": null, "has_more": false} ``` --- URL: https://docs.nebularws.com/comecar/como-um-agent-usa-isto.md # Como um agent usa isto Um agent usa a API `/v1` em um laço simples: 1. **Autenticar**: `Authorization: Bearer $NEBULAR_TOKEN`. O token vem de variável de ambiente, nunca do texto do prompt nem de arquivo versionado. Confirme escopos e limites com `GET /v1/me` ([Token de API](/comecar/token/)). 2. **Agir** com `Idempotency-Key` em todo `POST` que cria, para que repetir seja seguro. 3. **Esperar** quando a resposta for assíncrona: upload `202 pending` ([Uploads pending](/cdn/galerias/#uploads-pending-202-como-esperar)) ou deploy `queued`. Consulte o recurso com backoff (2 s, dobrando até 30 s); não reenvie. 4. **Ler o erro** pelo campo `error` e decidir pela [tabela de erros](/erros/tabela/): - `4xx` (exceto `429`): corrija a requisição; não repita igual. - `429`, `503`: repita depois de `Retry-After` (ou com backoff). - `401`/`403`: pare e peça um token novo ou com o escopo certo a uma pessoa. - `404`: o recurso não existe **ou não é do seu time**; a API não distingue. Confira o id; não tente descobrir. 5. **Parar e pedir uma pessoa** quando o erro exigir decisão humana (`malware_detected`, `quota_exceeded`, deploy `failed` sem causa clara nos logs). ## Laço de deploy 1. `POST /v1/projects` e `POST .../services` (veja [Primeiro deploy](/comecar/primeiro-deploy/)). 2. Variáveis e, se preciso, banco e volume ([Variáveis e referências](/conectados/variaveis-e-referencias/), [PostgreSQL](/bancos/postgres/)). 3. `POST .../deployments` (ou `POST /v1/projects/{p}/deploy` para o grupo). 4. Polling de `GET .../deployments/{d}` até `ready` (para o grupo, `GET /v1/projects/{p}/group-deployments/{g}` até `status` sair de `running`; `ready` é sucesso). Estado `failed`, `blocked`, `canceled` ou `superseded`: pare e leia o log de build ([Logs](/operacao/logs/)). 5. Verifique a URL pública. Se falhar, [rollback](/operacao/rollback/) para o deploy anterior e reporte. Fluxo de decisão completo: [Fluxo de diagnóstico](/erros/fluxo-de-diagnostico/). Receita pronta: [Do zero ao ar com um agent](/receitas/do-zero-ao-ar-com-um-agent/). --- URL: https://docs.nebularws.com/comecar/convencoes-da-api.md # Convenções da API - **Base URL**: `https://api.nebularws.com`; todas as rotas ficam em `/v1`. - **Autenticação**: `Authorization: Bearer nbk_…`, nunca em query string. Cookies são ignorados. Veja [Token de API](/comecar/token/). - **Erros**: sempre `{"error": "", "message": "..."}`. Trate pelo `error` (estável); mostre o `message` (pt-BR) a pessoas. Veja a [tabela de erros](/erros/tabela/). - **404 anti-IDOR**: recurso inexistente e recurso de outro time/projeto respondem igual (`404 not_found`). `401` é genérico de propósito. `403` só aparece quando o recurso é seu e falta escopo. Não tente distinguir. - **Paginação por cursor**: listas devolvem `items`, `next_cursor` e `has_more`. Siga `next_cursor` enquanto `has_more` for `true`; o cursor é opaco. Receita em [Galerias e arquivos](/cdn/galerias/#d-listar-com-cursor-next_cursor--has_more). - **Idempotência**: todo `POST` que cria ou dispara aceita `Idempotency-Key` (8 a 128 caracteres `[A-Za-z0-9._:-]`). Mesma chave e mesmo pedido em 24 h devolve a mesma resposta (cabeçalho `Idempotent-Replay: true`); mesma chave com outro pedido dá `422 idempotency_conflict` (`409` no upload de galeria); chave malformada (fora de 8 a 128 caracteres `[A-Za-z0-9._:-]`), `400 invalid_request`. Só respostas `2xx` são gravadas. Veja [Galerias e arquivos](/cdn/galerias/#idempotência-idempotency-key). - **Corpo JSON**: todo `POST`/`PUT`/`PATCH` **com corpo** leva `Content-Type: application/json`. Regra exata para `POST /v1/projects/{p}/deploy`, `POST .../services/{s}/deployments` e `POST .../deployments/{d}/rollback`: o corpo é **opcional**. Sem corpo (ou `Content-Length: 0`), com ou sem `Content-Type`, vale `{}`; com corpo, ele tem de ser JSON com `Content-Type: application/json` (outro tipo, como `text/plain` ou formulário, dá `415 unsupported_media_type`); o deploy de serviço aceita também `multipart/form-data` (campo `file`). Os exemplos mandam `Content-Type: application/json` e `{}` por ser a forma que funciona em qualquer cliente HTTP. Todo projeto nasce com um serviço `web` padrão, **público** e com `slug` ([Conceitos](/comecar/conceitos/#o-serviço-web-padrão)). - **Rate limit**: por token e por IP; em `429` espere `Retry-After` segundos. Veja [Rate limit e retry](/erros/rate-limit-e-retry/). - **Ids**: opacos. Projetos, serviços, deploys, bancos e demais recursos da v13 usam ULID em minúsculas de 26 caracteres (por exemplo `01m4kxaxjts58n5esjajbr5pg0`); galerias e arquivos têm prefixo do tipo (`gal_…`, `ast_…`, `ver_…`). Não os interprete nem dependa do formato: use o que a API devolveu. - **Datas**: RFC 3339 em UTC (`2026-10-09T14:03:11Z`). - **Assíncrono**: deploys, backups e restores respondem logo e terminam depois; consulte o recurso com backoff. Não há webhooks neste ciclo. - **Estados de deploy**: `queued`, `building`, `deploying`, `ready`, `failed`, `blocked`, `canceled`, `superseded`. Terminais: todos menos `queued`, `building` e `deploying`. Veja [Deploys e status](/operacao/deploys-e-status/). - **Limites do token e do time**: `GET /v1/me` e `GET /v1/limits`. --- URL: https://docs.nebularws.com/guia/nodejs-next.md # Node.js e Next.js Serviços Node (Express, Fastify, Nest, Next.js) rodam `npm start` como usuário não-root (`node`, uid 1000) em Node 24 com `NODE_ENV=production`. ## 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 `"node"`) a Nebular lê o `package.json` (um `Dockerfile`, se existir, vence): - **Com `scripts.start`**: servidor Node. O build roda `npm ci` (se houver `package-lock.json`, senão `npm install`), `npm run build --if-present`, remove devDependencies (`npm prune --omit=dev`) e copia a árvore para `/app`. O start é `npm start`. - **Sem `scripts.start`**: site estático (veja [Site estático](/guia/site-estatico/)). - `build_command` substitui o `npm run build`; `start_command` substitui o `npm start`. - Porta: 3000 por padrão. O app **tem que** escutar `0.0.0.0:$PORT`. Servidores `vite preview`/`astro preview` ignoram `PORT`: a plataforma acrescenta `--host 0.0.0.0 --port 3000` ao `npm start` nesses casos. - Next.js: use `"start": "next start"` (escuta `PORT`; defina `HOSTNAME=0.0.0.0` como variável do serviço se o app só responder dentro do container). O sistema de arquivos é somente leitura, mas a plataforma concede um tmpfs em `.next/cache` para o cache de ISR/fetch. Não grave outros arquivos em runtime. - **Piso de segurança:** tudo que começa com `NEXT_PUBLIC_` é embutido no bundle do browser em tempo de build. Nunca ponha segredo nessas variáveis; segredo só em variáveis lidas no servidor. 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": "next build", "start": "next start" } } ``` ```ts // app/healthz/route.ts: health check do Next (App Router) export function GET() { return new Response("ok"); } ``` ## 1. Criar o projeto O projeto é o grupo: todos os seus serviços compartilham uma rede privada. Guarde o `id` da resposta. ```bash NAME="app-next-$(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": "app-next-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 `node`. Este bloco é autossuficiente: cria o projeto e o serviço. ```bash NAME="app-next-$(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\": \"node\"}, \"port\": 3000, \"healthcheck_path\": \"/healthz\" }" ``` ```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="app-next-$(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\": \"node\"}, \"port\": 3000, \"healthcheck_path\": \"/healthz\" }" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/HOSTNAME" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d '{"value": "0.0.0.0"}' ``` 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="app-next-$(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\": \"node\"}, \"port\": 3000, \"healthcheck_path\": \"/healthz\" }" | 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 `/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/). | | deploy `ready` mas o health check falha / 502 | O app não escuta `0.0.0.0:$PORT`. Confira `PORT`, `HOSTNAME` e o `port` do serviço. | | `failed` no `npm ci` | `package-lock.json` fora de sincronia com `package.json`: regere o lockfile. | | `failed`: sem memória no `next build` | Suba `resources.memory_mb` do serviço. | ## 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="app-next-$(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\": \"node\"}, \"port\": 3000, \"healthcheck_path\": \"/healthz\" }" | 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 - [Front Next com back Go privado e Postgres](/conectados/front-back-db/). - [Galerias e imagens na CDN](/cdn/galerias/) para servir mídia com `next/image`. - [Domínio próprio](/dominios/dominio-proprio/). --- URL: https://docs.nebularws.com/guia/go.md # Go Um serviço Go vira um binário estático e roda numa imagem mínima (distroless) como usuário não-root. Sem Dockerfile. ## 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 `"go"`), a Nebular procura `go.mod` na raiz do serviço (`root_dir`). Se houver um `Dockerfile`, **ele vence** os demais sinais: para forçar o buildpack Go, declare `build.builder: "go"`. - Versão do Go: linha `toolchain` do `go.mod`, senão a linha `go`. - Pacote a compilar: `.` se a raiz tem `package main`; senão o único `./cmd/`. Com vários programas em `cmd/`, veja [Vários programas em `cmd/`](#vários-programas-em-cmd). - Build: `go build -trimpath -ldflags="-s -w"` com `CGO_ENABLED=0` (binário estático). Sem checkout `.git`: `-buildvcs=false`. - Start padrão: `/app/app` (o binário). Porta padrão do buildpack: 8080; declare `port` no serviço. - Dependências C (cgo) exigem a variante glibc; se precisar, use [Dockerfile](/guia/dockerfile/). 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. ```go // main.go: lê PORT, escuta em 0.0.0.0 e expõe /healthz package main import ( "log" "net/http" "os" ) func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } mux := http.NewServeMux() mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("ok")) }) mux.HandleFunc("GET /", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("olá da Nebular")) }) log.Fatal(http.ListenAndServe("0.0.0.0:"+port, mux)) } ``` ## Vários programas em `cmd/` A Nebular escolhe o programa a publicar nesta ordem, e para na primeira regra que decide: 1. Há `package main` na raiz do módulo: publica a raiz (`.`). 2. Há um único `cmd/` com `package main`: publica ele. 3. Há vários: escolhe o que tem o mesmo nome do final do módulo (linha `module` do `go.mod`). 4. Nenhum bate com o módulo: escolhe pela convenção de serviço web, na prioridade `server`, `app`, `api`, `web`, `gateway`, `http`, `service`, `main`, `cmd` (maiúsculas não importam). Vale o de maior prioridade presente. 5. Ainda sem decisão: o deploy falha com uma mensagem pedindo o **comando de build** do serviço. Quando a escolha vem da convenção, o motivo aparece no log do deploy. Se não é o programa que você quer, force-o: o `build_command` do serviço **sempre** vence a detecção. O binário precisa sair em `/app/app`, que é o que o runtime executa. ```bash curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d '{"build_command": "go build -o /app/app ./cmd/server"}' ``` ## 1. Criar o projeto O projeto é o grupo: todos os seus serviços compartilham uma rede privada. Guarde o `id` da resposta. ```bash NAME="api-go-$(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": "api-go-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 `go`. Este bloco é autossuficiente: cria o projeto e o serviço. ```bash NAME="api-go-$(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\": \"go\"}, \"port\": 8080, \"healthcheck_path\": \"/healthz\" }" ``` ```json {"id": "01j9zk32py7tcbz2q0ra5aqx7k", "name": "web", "kind": "app", "visibility": "public", "port": 8080} ``` 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="api-go-$(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\": \"go\"}, \"port\": 8080, \"healthcheck_path\": \"/healthz\" }" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/APP_ENV" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d '{"value": "producao"}' ``` 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="api-go-$(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\": \"go\"}, \"port\": 8080, \"healthcheck_path\": \"/healthz\" }" | 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 `/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 package main" | Ponha `package main` na raiz ou em `cmd/`, ou defina `build_command`. | | `failed`: vários programas em `cmd/` sem escolha possível | Defina `build_command` do serviço, por exemplo `go build -o /app/app ./cmd/server` (veja [Vários programas em `cmd/`](#vários-programas-em-cmd)). | ## 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="api-go-$(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\": \"go\"}, \"port\": 8080, \"healthcheck_path\": \"/healthz\" }" | 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 - [Serviço privado e rede do grupo](/conectados/rede-privada/) para um back Go que só o front alcança. - [PostgreSQL](/bancos/postgres/) para ligar `DATABASE_URL` ao serviço. - [Front Next com back Go e Postgres](/conectados/front-back-db/). --- URL: https://docs.nebularws.com/guia/rust.md # Rust Um serviço Rust é compilado com `cargo build --release --locked` e roda numa imagem mínima (distroless com libc) como não-root. ## 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 `"rust"`) a Nebular procura `Cargo.toml` (um `Dockerfile`, se existir, vence). - `Cargo.lock` é obrigatório no repositório (`--locked`). - Binário: o `[[bin]]` com o nome do pacote; se houver um só `[[bin]]`, ele; senão o nome do pacote. Vários `[[bin]]` sem nenhum igual ao pacote, ou *workspace* virtual (sem `[package]`), exigem `build_command` e `start_command`. - Versão do toolchain: `rust-version` do `Cargo.toml`; mais nova que a tabela da plataforma é erro. - Runtime: distroless com libc, sem OpenSSL. Use `rustls` ou, se precisar de OpenSSL/bibliotecas do sistema, [Dockerfile](/guia/dockerfile/). - Start padrão: `/app/app`. Porta padrão do buildpack: 8080; declare `port` no serviço. 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. ```rust // src/main.rs (axum 0.8): lê PORT, escuta em 0.0.0.0, expõe /healthz use axum::{routing::get, Router}; #[tokio::main] async fn main() { let port = std::env::var("PORT").unwrap_or_else(|_| "8080".into()); let app = Router::new() .route("/healthz", get(|| async { "ok" })) .route("/", get(|| async { "olá da Nebular" })); let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap(); axum::serve(listener, app).await.unwrap(); } ``` ## 1. Criar o projeto O projeto é o grupo: todos os seus serviços compartilham uma rede privada. Guarde o `id` da resposta. ```bash NAME="api-rust-$(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": "api-rust-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 `rust`. Este bloco é autossuficiente: cria o projeto e o serviço. ```bash NAME="api-rust-$(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\": \"rust\"}, \"port\": 8080, \"healthcheck_path\": \"/healthz\" }" ``` ```json {"id": "01j9zk32py7tcbz2q0ra5aqx7k", "name": "web", "kind": "app", "visibility": "public", "port": 8080} ``` 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="api-rust-$(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\": \"rust\"}, \"port\": 8080, \"healthcheck_path\": \"/healthz\" }" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/RUST_LOG" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d '{"value": "info"}' ``` 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 `target/` nem `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="api-rust-$(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\": \"rust\"}, \"port\": 8080, \"healthcheck_path\": \"/healthz\" }" | 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 `/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`: Cargo.lock / `--locked` | Versione o `Cargo.lock` (rode `cargo generate-lockfile` e inclua no envio). | | `failed`: falta de memória (processo morto) | Compilação Rust é pesada: suba `resources.memory_mb` do serviço (`PATCH` no serviço) ou use Dockerfile com cache. | ## 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="api-rust-$(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\": \"rust\"}, \"port\": 8080, \"healthcheck_path\": \"/healthz\" }" | 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 - [Rede privada](/conectados/rede-privada/) para expor o serviço só ao grupo. - [Dockerfile](/guia/dockerfile/) quando precisar de bibliotecas do sistema. --- URL: https://docs.nebularws.com/guia/python.md # Python (FastAPI) Um serviço Python instala as dependências com `pip` e roda como não-root numa imagem `python` slim. Este guia usa FastAPI; Flask e Django também são detectados. ## 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 `"python"`) a Nebular procura `pyproject.toml`, `requirements.txt` ou `Pipfile` (um `Dockerfile`, se existir, vence). - FastAPI/Starlette/uvicorn: start `python -m uvicorn :app --host 0.0.0.0 --port 8000`, onde o módulo é achado em `main.py`, `app.py`, `server.py`, `app/main.py` ou `src/main.py` (o arquivo precisa conter `FastAPI(`). Não achou: defina `start_command`. - Flask: `gunicorn :app`; Django: `gunicorn .wsgi:application` (exige `manage.py`). Sem framework web: `python main.py` ou `python app.py`. - Dependências: `requirements.txt` (`pip install -r`), `Pipfile` ou `pyproject.toml` (`pip install .`), instaladas em `/app/deps` (`PYTHONPATH` já ajustado). Pacotes nativos que precisem de compilador **não** funcionam na imagem slim: use [Dockerfile](/guia/dockerfile/). - Porta padrão do buildpack: 8000; declare `port` no serviço (use o mesmo valor em `PORT`). 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. ```python # main.py from fastapi import FastAPI app = FastAPI() @app.get("/healthz") def healthz(): return {"ok": True} @app.get("/") def raiz(): return {"msg": "olá da Nebular"} ``` ```text # requirements.txt fastapi uvicorn ``` Fixe versões em `requirements.txt` para builds reproduzíveis. O start padrão escuta na porta 8000; para outra porta, defina `start_command` com `--port` e `port` igual. ## 1. Criar o projeto O projeto é o grupo: todos os seus serviços compartilham uma rede privada. Guarde o `id` da resposta. ```bash NAME="api-py-$(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": "api-py-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 `python`. Este bloco é autossuficiente: cria o projeto e o serviço. ```bash NAME="api-py-$(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\": \"python\"}, \"port\": 8000, \"healthcheck_path\": \"/healthz\" }" ``` ```json {"id": "01j9zk32py7tcbz2q0ra5aqx7k", "name": "web", "kind": "app", "visibility": "public", "port": 8000} ``` 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="api-py-$(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\": \"python\"}, \"port\": 8000, \"healthcheck_path\": \"/healthz\" }" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/LOG_LEVEL" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d '{"value": "info"}' ``` 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 `.venv`, `.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="api-py-$(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\": \"python\"}, \"port\": 8000, \"healthcheck_path\": \"/healthz\" }" | 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 `/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`: "app (FastAPI/Flask) não encontrado" / "comando de start desconhecido" | Defina `start_command` (por exemplo `python -m uvicorn pacote.app:app --host 0.0.0.0 --port 8000`). | | `failed` ao instalar uma dependência | A imagem não tem compilador: use uma roda binária (wheel) ou [Dockerfile](/guia/dockerfile/). | ## 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="api-py-$(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\": \"python\"}, \"port\": 8000, \"healthcheck_path\": \"/healthz\" }" | 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 - [PostgreSQL](/bancos/postgres/) e [migrações](/bancos/migracoes/). - [Variáveis e referências](/conectados/variaveis-e-referencias/). --- URL: https://docs.nebularws.com/guia/site-estatico.md # 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 `/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. --- URL: https://docs.nebularws.com/guia/dockerfile.md # Dockerfile Quando as stacks automáticas não servem (bibliotecas do sistema, cgo, compilador Python), escreva um `Dockerfile`. A Nebular o constrói com BuildKit rootless isolado e roda a imagem resultante endurecida. ## 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 - Um `Dockerfile` na raiz (`root_dir`) **vence** a detecção automática. Para apontar outro arquivo: `build: {"builder": "dockerfile", "dockerfile_path": "docker/Dockerfile.prod"}` (relativo ao `root_dir`; não pode sair dele). - O contexto do build é o `root_dir` do serviço. O build roda sem acesso à rede interna e com tempo limite (10 min por padrão). - Só `linux/arm64` (os servidores são ARM): não use `FROM --platform=` com outra plataforma. Imagens-base multi-arch funcionam. - Variáveis de build: só entram como `--build-arg` as variáveis do próprio serviço cujo nome o `Dockerfile` declara com `ARG`. - Limites da imagem resultante: tamanho total e número de camadas (127). Imagem acima do limite é recusada. - **Usuário não-root:** declare `USER` com um uid sem privilégio. Se o `USER` for vazio ou root, a plataforma força um uid não-root; se o app não funcionar assim, o deploy termina em `failed`. - Runtime: sistema de arquivos somente leitura, `/tmp` em tmpfs, sem capabilities. Escreva só em `/tmp` ou num [volume](/conectados/volumes/). - `EXPOSE` é informativo: o que vale é o `port` do serviço, e o app precisa escutar `0.0.0.0:$PORT`. 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. ```dockerfile # Dockerfile multi-stage para um serviço Go (exemplo) FROM golang:1.25 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/api FROM gcr.io/distroless/static-debian12 COPY --from=build /out/app /app USER 65532:65532 EXPOSE 8080 ENTRYPOINT ["/app"] ``` ## 1. Criar o projeto O projeto é o grupo: todos os seus serviços compartilham uma rede privada. Guarde o `id` da resposta. ```bash NAME="app-df-$(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": "app-df-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 `dockerfile`. Este bloco é autossuficiente: cria o projeto e o serviço. ```bash NAME="app-df-$(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\": \"dockerfile\", \"dockerfile_path\": \"Dockerfile\"}, \"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="app-df-$(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\": \"dockerfile\", \"dockerfile_path\": \"Dockerfile\"}, \"port\": 3000 }" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/APP_ENV" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d '{"value": "producao"}' ``` 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="app-df-$(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\": \"dockerfile\", \"dockerfile_path\": \"Dockerfile\"}, \"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 `/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`: plataforma não suportada | O `Dockerfile` pede outra plataforma que não `linux/arm64`: remova o `--platform`. | | `failed`: build-arg não permitido | O `--build-arg` não é uma variável do serviço: crie a variável antes. | | `failed`: build morto (provável falta de memória) ou tempo limite | Reduza o build (cache de camadas, menos paralelismo). | | `failed` com permissão negada ao iniciar | O app só funciona como root: ajuste permissões (`chown`) e use `USER` não-root. Root não é permitido. | ## 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="app-df-$(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\": \"dockerfile\", \"dockerfile_path\": \"Dockerfile\"}, \"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 - [Imagem pública](/guia/imagem-publica/) para usar uma imagem pronta sem build. - [Volumes](/conectados/volumes/) para dados persistentes. --- URL: https://docs.nebularws.com/guia/imagem-publica.md # Imagem pública Use uma imagem pronta quando o software já é distribuído assim (Nginx, Ghost, Metabase). Não há build: a Nebular resolve a imagem, confere e roda. ## Pré-requisitos - Token com `projects:write`, `deploy`, `env:write` e `logs:read` ([Token de API](/comecar/token/)). - A imagem precisa estar num registry da lista permitida (padrão: `docker.io`, `ghcr.io`, `quay.io`), ser **pública** (sem login) e ter manifesto `linux/arm64` (os servidores são ARM). ## O que a plataforma faz com a imagem 1. Resolve a tag para um **digest** `sha256:` (se `source.ref` já traz `@sha256:...`, esse digest fixa a versão) e baixa a imagem verificando o hash. A plataforma escolhe a variante `linux/arm64` do manifesto; imagem só `amd64` é recusada. Tamanho total e número de camadas têm limite; acima disso a imagem é recusada. 2. Roda **sem root**: se o `USER` da imagem for vazio ou root, a plataforma força um uid sem privilégio (`65534`). Se o app não funcionar assim, o container encerra e o deploy termina em `failed` com `error.code = image_requires_root` (a mensagem pede `build.user`). Não há como rodar como root: declare `build.user` (`uid[:gid]`) com um usuário que a imagem suporte, ou escolha uma variante *unprivileged*. 3. Sistema de arquivos **somente leitura**, `/tmp` em tmpfs de 64 MB, sem capabilities, `no-new-privileges`. Dados persistentes só num [volume](/conectados/volumes/). 4. Como não é root, o app **não pode escutar portas abaixo de 1024**. Use uma imagem que escute em 8080 (ou configure a porta por variável) e declare `port` igual. Fixe sempre por digest em produção: veja [Fixar por digest e como obter o digest](#fixar-por-digest-e-como-obter-o-digest). ## Fixar por digest e como obter o digest A forma aceita pela API é **`source.ref = "repo[:tag]@sha256:<64 hex>"`**: o digest vai **dentro** do `ref`, depois de um `@` (o `:tag` é opcional e só serve de legenda para humanos; quem manda é o digest). Exemplo: `docker.io/nginxinc/nginx-unprivileged:1.27-alpine@sha256:0123...cdef`. O mesmo formato vale no corpo do deploy, `{"image": "repo:tag@sha256:..."}`, que atualiza a imagem do serviço e implanta (corpo sem `@sha256:` é recusado com `400 invalid_request`). Não há campo `digest` separado no contrato. Com só a tag no `ref`, a plataforma resolve a tag no momento do deploy e registra o digest escolhido em `image_digest` do deployment. Como obter o digest de uma tag (rode na sua máquina ou no CI, não é uma rota da Nebular): ```bash # mostra o digest do índice (linha "Digest:") e as plataformas disponíveis docker buildx imagetools inspect docker.io/nginxinc/nginx-unprivileged:1.27-alpine # só o digest (índice multi-arch) crane digest docker.io/nginxinc/nginx-unprivileged:1.27-alpine # digest da variante arm64 isolada crane digest --platform linux/arm64 docker.io/nginxinc/nginx-unprivileged:1.27-alpine ``` **Arquitetura:** os servidores de produção são `linux/arm64`. Confira na saída do `imagetools inspect` que existe uma entrada `Platform: linux/arm64`. O digest do **índice** (multi-arch) serve: a plataforma escolhe nele a variante arm64. O digest de uma variante isolada só serve se ela for `linux/arm64`; o digest de uma variante `amd64` falha o deploy com "a imagem não tem variante linux/arm64". Imagem que você mesmo constrói: publique multi-arch ou só arm64 (`docker buildx build --platform linux/arm64 --push ...`). ```bash # DIGEST de exemplo (64 hex): no uso real, o digest obtido acima DIGEST=sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"digest-$SUF\"}" | jq -r .id) S=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d "{\"port\": 8080, \"source\": {\"type\": \"image\", \"ref\": \"docker.io/nginxinc/nginx-unprivileged:1.27-alpine@$DIGEST\"}}" | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services/$S/deployments" -H "$H" \ -H "Content-Type: application/json" -H "Idempotency-Key: digest-$SUF" \ -d "{\"image\": \"docker.io/nginxinc/nginx-unprivileged:1.27-alpine@$DIGEST\"}" ``` ```json {"id": "01j9zkv1t9ndd76xmr8ahxee7p", "service_id": "01j9zk32py7tcbz2q0ra5aqx7k", "status": "queued", "trigger": "api", "source_ref": "docker.io/nginxinc/nginx-unprivileged:1.27-alpine@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "image_digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"} ``` ## 1. Criar o serviço com `source.type = image` ```bash NAME="vitrine-$(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\": \"image\", \"ref\": \"docker.io/nginxinc/nginx-unprivileged:1.27-alpine\"}, \"port\": 8080, \"healthcheck_path\": \"/\" }" ``` ```json {"id": "01j9zk32py7tcbz2q0ra5aqx7k", "name": "web", "kind": "app", "visibility": "public", "port": 8080} ``` | Resposta | O que fazer | |---|---| | `400 invalid_request` | `source.type` deve ser `upload`, `git` ou `image`; confira `ref` (formato `registro/org/imagem:tag`, ou `registro/org/imagem:tag@sha256:`). | | `404` | Projeto inexistente ou de outro time (indistinguíveis). | | `409 name_taken` / `slug_taken` | Nome de serviço ou `slug` já em uso. | | `403` | Falta `projects:write`. | ## 2. Deploy e polling Para fonte `image`, o `POST` de deploy usa a fonte configurada no serviço (sem corpo). ```bash # preparo (passo 1): projeto e serviço de imagem SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"vitrine-$SUF\"}" | jq -r .id) S=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d '{"source": {"type": "image", "ref": "ghcr.io/org/app:1.0"}}' | jq -r .id) # fonte image: o POST de deploy usa a imagem configurada no serviço (corpo {}) D=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services/$S/deployments" -H "$H" \ -H "Content-Type: application/json" -H "Idempotency-Key: deploy-nginx-$SUF" -d '{}' | jq -r .id) 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 ] ``` | Estado ou erro | O que fazer | |---|---| | `failed` com `error.code = image_requires_root` | A imagem só funciona como root. Declare `build.user` (`uid[:gid]`, nunca root) no serviço, ou troque por uma variante sem privilégio (`-unprivileged`, `-rootless`, distroless) ou use [Dockerfile](/guia/dockerfile/) para ajustar permissões. | | `failed`: registry fora da lista permitida | Use `docker.io`, `ghcr.io` ou `quay.io` (ou peça ao operador para incluir o registry). | | `failed`: "a imagem não tem variante linux/arm64" | Escolha outra tag ou uma imagem multi-arch com `linux/arm64`; um digest `amd64` não serve. | | `failed`: imagem maior que o limite / camadas demais | Use uma variante menor (alpine, slim). | | `failed`: digest não confere / imagem não encontrada pelo digest | O digest no `ref` não existe no registry ou não bate com o conteúdo; obtenha-o de novo (veja acima). | | `failed` com o app caindo logo após subir | Leia o log de execução (`GET .../services/$S/logs`): porta errada, escrita em disco somente leitura (mova para `/tmp` ou volume). | | `503` | Transitório: repita com backoff. | Veja também [Deploys e status](/operacao/deploys-e-status/), [Logs](/operacao/logs/) e [Rollback](/operacao/rollback/). ## 3. Variáveis e atualização Configure a imagem por variáveis ([Variáveis e referências](/conectados/variaveis-e-referencias/)). Para atualizar a versão, mude `source.ref` (nova tag e novo `@sha256:...`) com `PATCH` no serviço e faça um novo deploy; o rollback volta ao deployment anterior. ```bash NAME="vitrine-$(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\": \"private\", \"source\": {\"type\": \"image\", \"ref\": \"docker.io/nginxinc/nginx-unprivileged:1.27-alpine\"}, \"port\": 8080}" | jq -r .id) curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/$S" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d '{"source": {"type": "image", "ref": "docker.io/nginxinc/nginx-unprivileged:1.27.5-alpine"}}' ``` --- URL: https://docs.nebularws.com/guia/wordpress.md # WordPress O template `wordpress` instancia, de uma vez, um banco MariaDB gerenciado e o serviço WordPress ligado a ele. Você não escreve Dockerfile nem variáveis de banco. ## Pré-requisitos - Token com `projects:write`, `templates:deploy`, `deploy` e `logs:read` ([Tokens e escopos](/seguranca/tokens-e-escopos/)). - Mínimo de **512 MB e 1 vCPU** para o serviço (recomendado 2 vCPU para tráfego real). Confira o que o seu plano permite em `GET /v1/limits`. ## O que o template configura Serviços criados (nomes fixos): | Serviço | Tipo | Detalhes | |---|---|---| | `db` | `database`, motor `mariadb` | cota de 1024 MB | | `web` | `app`, público | imagem `wordpress:6.9.4-php8.3-apache` fixada por digest, porta 8080, volume `wp-content` de 5 GB em `/var/www/html/wp-content` | O `web` que todo projeto novo já traz **não precisa ser apagado**: se ainda estiver virgem (sem deploy, variáveis, domínio ou volume), o template o **adota** (o `id` do serviço é o mesmo) e o configura. Se o `web` já foi usado, a instanciação responde `409 name_taken` e nada é criado. Como o WordPress roda na plataforma: - Roda como `33:33` (`www-data`), sistema de arquivos **somente leitura**; tmpfs em `/var/www/html` (128 MB), `/tmp`, `/var/run/apache2` e `/var/lock/apache2`. Só `wp-content` (o volume) persiste: plugins, temas e uploads ficam lá. - As variáveis de banco vêm por referência (`WORDPRESS_DB_HOST=${{db.MYSQL_HOST}}:${{db.MYSQL_PORT}}`, `..._USER`, `..._PASSWORD`, `..._NAME`); a conexão usa TLS com verificação do certificado do servidor. - As chaves e *salts* (`WORDPRESS_AUTH_KEY` etc.) são geradas por instância com `{{generate:secret64}}` (CSPRNG) e gravadas como variáveis **sensíveis**. - O `wp-cron` do WordPress é desligado (`DISABLE_WP_CRON`) e a plataforma faz um `GET /wp-cron.php?doing_wp_cron` interno a cada 5 minutos pela rede do grupo (sem shell, sem comando arbitrário). - O proxy responde `404` para `/xmlrpc.php`, `/readme.html` e `/license.txt` antes de chegar ao app. - Atualizações automáticas do WordPress ficam desligadas (`AUTOMATIC_UPDATER_DISABLED`): **atualizar é trocar a imagem do template** (veja abaixo). ## 1. Ver o template e seus parâmetros ```bash curl -sS "$NEBULAR_API/v1/templates" \ -H "Authorization: Bearer $NEBULAR_TOKEN" ``` ```json {"items": [{"id": "wordpress", "name": "WordPress", "version": 1}], "next_cursor": null, "has_more": false} ``` Cada item traz `params` (nome, tipo e padrão). O template declara o parâmetro `table_prefix` (texto, padrão `"wp_"`): o prefixo das tabelas do WordPress, até 16 caracteres, no formato `^[a-z][a-z0-9_]{0,14}_$`. | Resposta | O que fazer | |---|---| | `401` / `403` | Token inválido ou sem escopo: pare e peça outro. | | `429` | Espere `Retry-After`. | ## 2. Instanciar Crie o projeto e instancie o template nele. A instanciação é **atômica** (se qualquer parte falhar, tudo que foi criado é desfeito) e **idempotente** pela `Idempotency-Key`: repetir a mesma chamada depois de timeout devolve o mesmo resultado e não duplica serviços. ```bash NAME="blog-$(date +%s)-$RANDOM" H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"$NAME\"}" | jq -r .id) # o `web` que o projeto já traz, se virgem, é adotado pelo template (não apague) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/templates/wordpress" \ -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: $NAME-wordpress" \ -d '{"params": {"table_prefix": "wp_"}}' ``` ```json {"template_id": "wordpress", "version": 1, "project_id": "01j9zkk07fhzv5zsmn996cdwpb", "services": [{"id": "01j9zkff8y1dmcwa49fanx9qak", "name": "db", "kind": "database"}, {"id": "01j9zkwf4q15w38y0taf680jvv", "name": "web", "kind": "app"}], "deploy_started": true} ``` | Resposta | O que fazer | |---|---| | `404` | Projeto ou template inexistente (ou projeto de outro time). | | `400 invalid_request` | `params` inválido (tipo ou parâmetro desconhecido). | | `409 name_taken` | O projeto já tem um serviço `db`, ou o `web` já foi usado (tem deploy, variáveis, domínio ou volume). Use um projeto novo, ou limpe o `web`. Um `web` virgem **não** dá erro: é adotado. | | `422 quota_exceeded` | Limite do plano (serviços, memória, volume ou cota de banco). Veja [Limites e cotas](/operacao/limites-e-cotas/). | | `503 unavailable` | O nó de banco está indisponível: nada foi criado (atômico); repita a **mesma** chamada com backoff de 5, 10, 20, 40 s. | ## 3. Acompanhar os deploys Liste os serviços do projeto, pegue o deployment mais novo do `web` e faça polling dele por id. O `web` só fica `ready` depois que o banco `db` está provisionado (ele depende do `db` pelas referências); se o `db` falhar, o `web` termina `blocked`. ```bash NAME="blog-$(date +%s)-$RANDOM" H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"$NAME\"}" | jq -r .id) # o `web` que o projeto já traz, se virgem, é adotado pelo template (não apague) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/templates/wordpress" \ -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: $NAME-wordpress" \ -d '{"params": {"table_prefix": "wp_"}}' >/dev/null S=$(curl -sS "$NEBULAR_API/v1/projects/$P/services" -H "$H" | jq -r '.items[] | select(.name=="web") | .id') # o deploy do template é o mais novo da lista; acompanhe-o pelo id D=$(curl -sS "$NEBULAR_API/v1/projects/$P/services/$S/deployments" -H "$H" | jq -r '.items[0].id') 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 vem em public_url curl -sS "$NEBULAR_API/v1/projects/$P/services/$S" -H "$H" | jq -e '.public_url | startswith("https://")' >/dev/null ``` Para ver o `409` quando o `web` já foi usado: ```bash NAME="blog-$(date +%s)-$RANDOM" H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"$NAME\"}" | jq -r .id) # usa o web (variável) e então tenta o template curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/web/variables/FOO" -H "$H" -H "Content-Type: application/json" \ -d '{"value": "bar"}' >/dev/null curl -sS -X POST "$NEBULAR_API/v1/projects/$P/templates/wordpress" \ -H "$H" -H "Content-Type: application/json" -d '{"params": {}}' ``` Abra `$URL/wp-admin/install.php` no navegador (ou complete a instalação do WordPress por uma pessoa): criar o usuário administrador é um passo humano. | Estado ou erro | O que fazer | |---|---| | `blocked` no `web` | O `db` falhou: leia os logs de build/provisionamento do `db` e corrija. | | `422 unresolved_reference` ao disparar o deploy | Uma variável `${{db.…}}` não existe: o serviço `db` foi apagado ou renomeado. | | 502 / health check falhando | Memória insuficiente (suba `resources.memory_mb` para 512 ou mais, `resources.cpus` para 1) ou banco ainda em `provisioning`. Leia os logs de execução do `web`. | | `503 unavailable` | Banco indisponível: repita com backoff; não recrie. | ## Domínio, backups e atualização - URL gerada: [Subdomínio gerado](/dominios/subdominio-gerado/); domínio próprio: [Domínio próprio](/dominios/dominio-proprio/). - O banco tem backup diário (7 diários e 4 semanais) e sob demanda: [Backup e restore](/bancos/backup-restore/). `wp-content` (volume) é separado: veja [Volumes](/conectados/volumes/). - **Atualizar o WordPress** = trocar `source.ref` (nova tag e novo `@sha256:...`) do serviço `web` por uma versão mais nova da imagem oficial e fazer um novo deploy; o rollback volta à versão anterior. Faça backup do banco antes. ```bash NAME="blog-$(date +%s)-$RANDOM" H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"$NAME\"}" | jq -r .id) # o `web` que o projeto já traz, se virgem, é adotado pelo template (não apague) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/templates/wordpress" \ -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: $NAME-wordpress" \ -d '{"params": {"table_prefix": "wp_"}}' >/dev/null S=$(curl -sS "$NEBULAR_API/v1/projects/$P/services" -H "$H" | jq -r '.items[] | select(.name=="web") | .id') curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/$S" \ -H "$H" -H "Content-Type: application/json" \ -d '{"source": {"type": "image", "ref": "wordpress:6.9.5-php8.3-apache"}}' curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services/$S/deployments" \ -H "$H" -H "Content-Type: application/json" -H "Idempotency-Key: wp-update-$NAME" -d '{}' ``` --- URL: https://docs.nebularws.com/guia/monorepo.md # Monorepo Um repositório com `apps/web` e `apps/api` vira **dois serviços do mesmo projeto**, cada um com seu `root_dir`. O projeto é o grupo: os serviços se enxergam pela [rede privada](/conectados/rede-privada/). ## Pré-requisitos - Token com `projects:write`, `deploy` e `env:write`. - Integração GitHub instalada no time (o `installation_id` identifica a instalação). A rota para listar instalações não faz parte do contrato v13: pegue o `installation_id` no painel. ## Fonte git por serviço A fonte do serviço é `{"type": "git", "installation_id": "...", "repo": "org/repo", "branch": "main", "root_dir": "/apps/api", "watch_paths": ["apps/api/**"]}`. - `root_dir`: subdiretório do repositório que é a raiz do serviço (detecção da stack, Dockerfile e contexto de build partem dele). Não pode sair do repositório. - `watch_paths`: padrões que decidem se um push afeta o serviço. Sem `watch_paths`, todo push na branch o afeta. - Cada serviço escolhe seu `build.builder` (`auto` detecta pela raiz do serviço). ```bash # INSTALL: installation_id da integração GitHub do time NAME="mono-$(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) # API privada (só o front alcança), em Go curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d "{\"name\": \"api\", \"kind\": \"app\", \"visibility\": \"private\", \"port\": 8080, \"auto_deploy\": true, \"build\": {\"builder\": \"go\"}, \"source\": {\"type\": \"git\", \"installation_id\": \"$INSTALL\", \"repo\": \"org/repo\", \"branch\": \"main\", \"root_dir\": \"/apps/api\", \"watch_paths\": [\"apps/api/**\"]}}" # front público, em Next.js 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\", \"port\": 3000, \"auto_deploy\": true, \"build\": {\"builder\": \"node\"}, \"source\": {\"type\": \"git\", \"installation_id\": \"$INSTALL\", \"repo\": \"org/repo\", \"branch\": \"main\", \"root_dir\": \"/apps/web\", \"watch_paths\": [\"apps/web/**\", \"packages/ui/**\"]}}" ``` | Resposta | O que fazer | |---|---| | `400 invalid_request` | `root_dir` inválido ou `source.type` desconhecido. | | `404` | Projeto inexistente ou de outro time (indistinguíveis). | | `409 name_taken` / `slug_taken` | Nome ou `slug` já em uso. | ## Auto-deploy por git Com `auto_deploy: true`, o push na branch aciona o deploy. O webhook do GitHub encontra **todos** os serviços ligados ao repositório e à branch; com `watch_paths`, só os afetados pelos arquivos do push. Se mais de um serviço do grupo for afetado, o resultado é **um deploy de grupo**. ## Deploy de grupo `POST /v1/projects/{p}/deploy` publica o grupo em **ordem topológica das referências**: o serviço referenciado sobe antes de quem o referencia (por exemplo `api` antes de `web`, se `web` usa `${{api.PRIVATE_URL}}`). Se o build de um serviço falhar, os dependentes ficam `blocked`; os que já estavam ativos continuam no ar. ```bash G=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/deploy" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -H "Idempotency-Key: grupo-mono-1" -d '{}' | jq -r .id) # polling do GRUPO: um GET mostra o status geral e o de cada serviço curl -sS "$NEBULAR_API/v1/projects/$P/group-deployments/$G" -H "Authorization: Bearer $NEBULAR_TOKEN" \ | jq -r '.status, (.items[] | "\(.service_id) \(.status)")' ``` O corpo é `{}` com `Content-Type: application/json`; o `POST` também é aceito **sem corpo** (nesse caso o `Content-Type` é dispensável). O polling é feito no **grupo** (`GET /v1/projects/{p}/group-deployments/{g}`), não serviço a serviço. Repita a consulta com backoff (2 s, dobrando até 30 s, até ~10 min) até o `status` do grupo deixar de ser `running`: `ready` é sucesso; `failed`, `canceled` ou `superseded` são finais, e o item `failed` traz o `deployment_id` cujo log de build você lê (`GET .../deployments/{d}/logs`). Os itens têm `status` próprio (`queued`, `building`, `deploying`, `ready`, `failed`, `blocked`, `skipped`). Exemplo executável (projeto de imagem; primeiro com `{}`, depois sem corpo): ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"mono3-$SUF\"}" | jq -r .id) curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d '{"source": {"type": "image", "ref": "ghcr.io/org/app:1.0"}}' >/dev/null # 1) com Content-Type e {}: a forma que funciona em qualquer cliente G=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/deploy" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: mono3-a-$SUF" -d '{}' | jq -r .id) for i in $(seq 1 40); do st=$(curl -sS "$NEBULAR_API/v1/projects/$P/group-deployments/$G" -H "$H" | jq -r .status) [ "$st" = running ] || break sleep 2 done [ "$st" = ready ] # 2) sem corpo e sem Content-Type: também aceito (vale {}) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/deploy" -H "$H" -H "Idempotency-Key: mono3-b-$SUF" ``` | Estado ou erro | O que fazer | |---|---| | `blocked` | Um serviço do qual este depende falhou: conserte-o primeiro e redeploye o grupo. | | `422 unresolved_reference` ao disparar o deploy | Uma `${{serviço.VAR}}` aponta para variável ou serviço inexistente (a lista vem no erro). | | `422 reference_cycle` ao disparar o deploy | Os serviços se referenciam em ciclo: quebre o ciclo. | | `429` | Espere `Retry-After`. | Rollback é **por serviço**: `POST .../services/{s}/deployments/{d}/rollback` ([Rollback](/operacao/rollback/)). Para variáveis e referências entre serviços, veja [Variáveis e referências](/conectados/variaveis-e-referencias/). --- URL: https://docs.nebularws.com/bancos/postgres.md # PostgreSQL Um banco gerenciado é um **serviço** `kind=database` do projeto. Ele vive no nó de banco (outro host), **não** na rede privada do grupo: os apps o acessam pela URL com TLS. Escopo do token: `databases:write` (criar, apagar, rotacionar) e `projects:read` (ler). Veja [Tokens e escopos](/seguranca/tokens-e-escopos/). ## 1. Criar `name` é o nome do serviço (`^[a-z][a-z0-9-]{0,30}$`, único no projeto; é o prefixo das referências `${{db.…}}`). `engine` é `postgres`, `mariadb` ou `sqlite`; `quota_mb` é a cota em MB. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"demo-pg-$SUF\"}" | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: pg-db-$SUF" \ -d '{"name": "db", "engine": "postgres", "quota_mb": 1024}' ``` ```json {"id": "01j9zktv26djn3k6dd2yraqsbt", "name": "db", "kind": "database", "engine": "postgres", "status": "provisioning", "quota_mb": 1024} ``` O banco nasce em `provisioning` e vira `ready`. Consulte `GET /v1/projects/$P/databases/{s}` com backoff (2 s, dobrando até 30 s, até uns 10 min) até o banco estar pronto antes de fazer deploy dos dependentes: ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"demo-pg-$SUF\"}" | jq -r .id) DB=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: pg-db-$SUF" \ -d '{"name": "db", "engine": "postgres", "quota_mb": 1024}' | jq -r .id) delay=2 for i in $(seq 1 40); do st=$(curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB" -H "$H" | jq -r .status) echo "tentativa $i: $st" [ "$st" = ready ] && break sleep "$delay"; delay=$(( delay * 2 > 30 ? 30 : delay * 2 )) done curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB" -H "$H" ``` ```json {"id": "01j9zktv26djn3k6dd2yraqsbt", "name": "db", "kind": "database", "engine": "postgres", "status": "ready", "quota_mb": 1024, "read_only": false} ``` ## 2. Variáveis entregues Todas **sensíveis** (mascaradas em log e UI) e referenciáveis por qualquer serviço do grupo: | Variável | Conteúdo | |---|---| | `DATABASE_URL` | `postgres://:@:5432/?sslmode=verify-full` | | `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE` | as mesmas peças, separadas | O certificado do `public_host` é de CA pública (Let's Encrypt): `sslmode=verify-full` funciona sem instalar CA própria. **Não** troque para `sslmode=disable`/`require`. No app, use por referência (nunca copie o valor): `${{db.DATABASE_URL}}`. Veja [Conexão e referências](/bancos/conexao-e-referencias/). Ler o valor em claro exige o escopo `env:reveal` num token criado com step-up (`GET …/variables?resolved=true`). Sem ele, o valor vem mascarado. As credenciais também saem de `POST /v1/projects/{p}/databases/{s}/credentials` (exige `env:reveal`): `{"credentials": [{"name": "DATABASE_URL", "value": "..."}, ...]}`. ## 3. Conectar de fora (diagnóstico) Com as credenciais reveladas, use as variáveis padrão do libpq: ```bash PGSSLMODE=verify-full psql "$DATABASE_URL" -c 'select 1' ``` ## 4. Cota `quota_mb` é medida a cada 5 minutos. Acima do limite o banco vira `read_only` (escritas falham; há evento e aviso ao time) e **volta ao normal ao baixar** do limite (apague dados). O motor sinaliza `quota_exceeded` (422) quando a cota é incompatível com o uso. Para mudar a cota: `PATCH /v1/projects/{p}/databases/{s}` com `{"quota_mb": 2048}` (`databases:write`); a resposta é o banco (`quota_mb`, `size_mb`, `read_only`). `GET /v1/projects/{p}/databases` lista os bancos do projeto. ## 5. Rotacionar credencial `POST /v1/projects/{p}/databases/{s}/rotate` (`databases:write`) gera senha nova e termina as sessões antigas. Os serviços dependentes são marcados "precisam de redeploy" (re-resolução): faça deploy deles. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"demo-pg-$SUF\"}" | jq -r .id) DB=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: pg-db-$SUF" \ -d '{"name": "db", "engine": "postgres", "quota_mb": 1024}' | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases/$DB/rotate" \ -H "$H" ``` ```json {"id": "01j9zktv26djn3k6dd2yraqsbt", "name": "db", "kind": "database", "engine": "postgres"} ``` ## 6. Apagar `DELETE /v1/projects/{p}/databases/{s}` (`databases:write`; `204`). Pela API de token a chamada não exige step-up (o painel exige); por padrão faz **backup final** antes (`{"final_backup": false}` no corpo desliga). Apagar é irreversível para os dados atuais; restaure do backup final se precisar ([Backup e restore](/bancos/backup-restore/)). ## Se algo falhar | Resposta | O que fazer | |---|---| | `401` / `403` | Token ausente/sem escopo `databases:write`. Peça um token com o escopo. | | `404 not_found` | Projeto ou banco inexistente **ou de outro time** (indistinguíveis). Confira os ids. | | `409 name_taken` | Já existe um serviço com esse `name` no projeto (todo banco é um serviço). Use outro `name`. | | `422 quota_exceeded` | Cota insuficiente para o uso atual. Apague dados ou peça cota maior. | | `503 unavailable` | Nó de banco indisponível. Repita com backoff (a mesma `Idempotency-Key`). Persistindo, pare e avise uma pessoa. | | `429 rate_limited` | Espere `Retry-After`. | Tabela completa: [Erros](/erros/tabela/). --- URL: https://docs.nebularws.com/bancos/mariadb.md # MariaDB Um banco gerenciado é um **serviço** `kind=database` do projeto. Ele vive no nó de banco (outro host), **não** na rede privada do grupo: os apps o acessam pela URL com TLS. Escopo do token: `databases:write` (criar, apagar, rotacionar) e `projects:read` (ler). Veja [Tokens e escopos](/seguranca/tokens-e-escopos/). ## 1. Criar `name` é o nome do serviço (`^[a-z][a-z0-9-]{0,30}$`, único no projeto; é o prefixo das referências `${{db.…}}`). `engine` é `postgres`, `mariadb` ou `sqlite`; `quota_mb` é a cota em MB. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"demo-mdb-$SUF\"}" | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: mdb-db-$SUF" \ -d '{"name": "db", "engine": "mariadb", "quota_mb": 1024}' ``` ```json {"id": "01j9zktv26djn3k6dd2yraqsbt", "name": "db", "kind": "database", "engine": "mariadb", "status": "provisioning", "quota_mb": 1024} ``` O banco nasce em `provisioning` e vira `ready`. Consulte `GET /v1/projects/$P/databases/{s}` com backoff (2 s, dobrando até 30 s, até uns 10 min) até o banco estar pronto antes de fazer deploy dos dependentes: ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"demo-mdb-$SUF\"}" | jq -r .id) DB=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: mdb-db-$SUF" \ -d '{"name": "db", "engine": "mariadb", "quota_mb": 1024}' | jq -r .id) delay=2 for i in $(seq 1 40); do st=$(curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB" -H "$H" | jq -r .status) echo "tentativa $i: $st" [ "$st" = ready ] && break sleep "$delay"; delay=$(( delay * 2 > 30 ? 30 : delay * 2 )) done curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB" -H "$H" ``` ```json {"id": "01j9zktv26djn3k6dd2yraqsbt", "name": "db", "kind": "database", "engine": "mariadb", "status": "ready", "quota_mb": 1024, "read_only": false} ``` ## 2. Variáveis entregues Todas **sensíveis** (mascaradas em log e UI) e referenciáveis por qualquer serviço do grupo: | Variável | Conteúdo | |---|---| | `DATABASE_URL` | `mysql://:@:3306/?ssl=true` | | `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE` | as mesmas peças, separadas | O certificado do `public_host` é de CA pública (Let's Encrypt): `ssl=true` funciona sem instalar CA própria. Mantenha o TLS ligado; não o desative no cliente. No app, use por referência (nunca copie o valor): `${{db.DATABASE_URL}}`. Veja [Conexão e referências](/bancos/conexao-e-referencias/). Ler o valor em claro exige o escopo `env:reveal` num token criado com step-up (`GET …/variables?resolved=true`). Sem ele, o valor vem mascarado. As credenciais também saem de `POST /v1/projects/{p}/databases/{s}/credentials` (exige `env:reveal`): `{"credentials": [{"name": "DATABASE_URL", "value": "..."}, ...]}`. ## 3. Conectar de fora (diagnóstico) Com as credenciais reveladas (cliente `mysql`/`mariadb`, TLS ligado): ```bash mysql --ssl -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" -e 'select 1' ``` ## 4. Cota `quota_mb` é medida a cada 5 minutos. Acima do limite o banco vira `read_only` (escritas falham; há evento e aviso ao time) e **volta ao normal ao baixar** do limite (apague dados). O motor sinaliza `quota_exceeded` (422) quando a cota é incompatível com o uso. Para mudar a cota: `PATCH /v1/projects/{p}/databases/{s}` com `{"quota_mb": 2048}` (`databases:write`); a resposta é o banco (`quota_mb`, `size_mb`, `read_only`). `GET /v1/projects/{p}/databases` lista os bancos do projeto. ## 5. Rotacionar credencial `POST /v1/projects/{p}/databases/{s}/rotate` (`databases:write`) gera senha nova e termina as sessões antigas. Os serviços dependentes são marcados "precisam de redeploy" (re-resolução): faça deploy deles. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"demo-mdb-$SUF\"}" | jq -r .id) DB=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: mdb-db-$SUF" \ -d '{"name": "db", "engine": "mariadb", "quota_mb": 1024}' | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases/$DB/rotate" \ -H "$H" ``` ```json {"id": "01j9zktv26djn3k6dd2yraqsbt", "name": "db", "kind": "database", "engine": "mariadb"} ``` ## 6. Apagar `DELETE /v1/projects/{p}/databases/{s}` (`databases:write`; `204`). Pela API de token a chamada não exige step-up (o painel exige); por padrão faz **backup final** antes (`{"final_backup": false}` no corpo desliga). Apagar é irreversível para os dados atuais; restaure do backup final se precisar ([Backup e restore](/bancos/backup-restore/)). ## Se algo falhar | Resposta | O que fazer | |---|---| | `401` / `403` | Token ausente/sem escopo `databases:write`. Peça um token com o escopo. | | `404 not_found` | Projeto ou banco inexistente **ou de outro time** (indistinguíveis). Confira os ids. | | `409 name_taken` | Já existe um serviço com esse `name` no projeto (todo banco é um serviço). Use outro `name`. | | `422 quota_exceeded` | Cota insuficiente para o uso atual. Apague dados ou peça cota maior. | | `503 unavailable` | Nó de banco indisponível. Repita com backoff (a mesma `Idempotency-Key`). Persistindo, pare e avise uma pessoa. | | `429 rate_limited` | Espere `Retry-After`. | Tabela completa: [Erros](/erros/tabela/). --- URL: https://docs.nebularws.com/bancos/sqlite.md # SQLite (libSQL) Um banco gerenciado é um **serviço** `kind=database` do projeto. Ele vive no nó de banco (outro host), **não** na rede privada do grupo: os apps o acessam pela URL com TLS. Escopo do token: `databases:write` (criar, apagar, rotacionar) e `projects:read` (ler). Veja [Tokens e escopos](/seguranca/tokens-e-escopos/). ## 1. Criar `name` é o nome do serviço (`^[a-z][a-z0-9-]{0,30}$`, único no projeto; é o prefixo das referências `${{db.…}}`). `engine` é `postgres`, `mariadb` ou `sqlite`; `quota_mb` é a cota em MB. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"demo-sq-$SUF\"}" | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: sq-db-$SUF" \ -d '{"name": "db", "engine": "sqlite", "quota_mb": 1024}' ``` ```json {"id": "01j9zktv26djn3k6dd2yraqsbt", "name": "db", "kind": "database", "engine": "sqlite", "status": "provisioning", "quota_mb": 1024} ``` O banco nasce em `provisioning` e vira `ready`. Consulte `GET /v1/projects/$P/databases/{s}` com backoff (2 s, dobrando até 30 s, até uns 10 min) até o banco estar pronto antes de fazer deploy dos dependentes: ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"demo-sq-$SUF\"}" | jq -r .id) DB=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: sq-db-$SUF" \ -d '{"name": "db", "engine": "sqlite", "quota_mb": 1024}' | jq -r .id) delay=2 for i in $(seq 1 40); do st=$(curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB" -H "$H" | jq -r .status) echo "tentativa $i: $st" [ "$st" = ready ] && break sleep "$delay"; delay=$(( delay * 2 > 30 ? 30 : delay * 2 )) done curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB" -H "$H" ``` ```json {"id": "01j9zktv26djn3k6dd2yraqsbt", "name": "db", "kind": "database", "engine": "sqlite", "status": "ready", "quota_mb": 1024, "read_only": false} ``` ## 2. Variáveis entregues Todas **sensíveis** (mascaradas em log e UI) e referenciáveis por qualquer serviço do grupo: | Variável | Conteúdo | |---|---| | `LIBSQL_URL` | `libsql://…` do banco | | `LIBSQL_AUTH_TOKEN` | token de acesso (JWT) do banco | | `DATABASE_URL` | igual a `LIBSQL_URL` | A conexão é por TLS; o cliente libSQL usa `LIBSQL_URL` + `LIBSQL_AUTH_TOKEN`. No app, use por referência (nunca copie o valor): `${{db.DATABASE_URL}}`. Veja [Conexão e referências](/bancos/conexao-e-referencias/). Ler o valor em claro exige o escopo `env:reveal` num token criado com step-up (`GET …/variables?resolved=true`). Sem ele, o valor vem mascarado. As credenciais também saem de `POST /v1/projects/{p}/databases/{s}/credentials` (exige `env:reveal`): `{"credentials": [{"name": "DATABASE_URL", "value": "..."}, ...]}`. ## 3. Conectar de fora (diagnóstico) Com as credenciais reveladas, use o SDK libSQL da sua linguagem com `LIBSQL_URL` e `LIBSQL_AUTH_TOKEN` (ilustrativo): ```ts // SDK libSQL: createClient({ url: process.env.LIBSQL_URL, authToken: process.env.LIBSQL_AUTH_TOKEN }) ``` ## 4. Cota `quota_mb` é medida a cada 5 minutos. Acima do limite o banco vira `read_only` (escritas falham; há evento e aviso ao time) e **volta ao normal ao baixar** do limite (apague dados). O motor sinaliza `quota_exceeded` (422) quando a cota é incompatível com o uso. Para mudar a cota: `PATCH /v1/projects/{p}/databases/{s}` com `{"quota_mb": 2048}` (`databases:write`); a resposta é o banco (`quota_mb`, `size_mb`, `read_only`). `GET /v1/projects/{p}/databases` lista os bancos do projeto. ## 5. Rotacionar credencial `POST /v1/projects/{p}/databases/{s}/rotate` (`databases:write`) troca a chave de assinatura do banco: o `LIBSQL_AUTH_TOKEN` anterior passa a dar 401 na hora e um token novo é selado. A URL não muda. Os serviços dependentes são marcados "precisam de redeploy" (re-resolução): faça deploy deles. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"demo-sq-$SUF\"}" | jq -r .id) DB=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: sq-db-$SUF" \ -d '{"name": "db", "engine": "sqlite", "quota_mb": 1024}' | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases/$DB/rotate" \ -H "$H" ``` ```json {"id": "01j9zktv26djn3k6dd2yraqsbt", "name": "db", "kind": "database", "engine": "sqlite"} ``` ## 6. Apagar `DELETE /v1/projects/{p}/databases/{s}` (`databases:write`; `204`). Pela API de token a chamada não exige step-up (o painel exige); por padrão faz **backup final** antes (`{"final_backup": false}` no corpo desliga). Apagar é irreversível para os dados atuais; restaure do backup final se precisar ([Backup e restore](/bancos/backup-restore/)). ## Se algo falhar | Resposta | O que fazer | |---|---| | `401` / `403` | Token ausente/sem escopo `databases:write`. Peça um token com o escopo. | | `404 not_found` | Projeto ou banco inexistente **ou de outro time** (indistinguíveis). Confira os ids. | | `409 name_taken` | Já existe um serviço com esse `name` no projeto (todo banco é um serviço). Use outro `name`. | | `422 quota_exceeded` | Cota insuficiente para o uso atual. Apague dados ou peça cota maior. | | `503 unavailable` | Nó de banco indisponível. Repita com backoff (a mesma `Idempotency-Key`). Persistindo, pare e avise uma pessoa. | | `429 rate_limited` | Espere `Retry-After`. | Tabela completa: [Erros](/erros/tabela/). --- URL: https://docs.nebularws.com/bancos/conexao-e-referencias.md # Conexão e referências O banco **não** entra na rede privada do grupo (`nbg-…`): ele fica no nó de banco e é acessado pela URL das variáveis, com TLS, no `public_host`. Por isso o app deve usar as variáveis entregues, nunca um hostname montado à mão. ## Variáveis referenciáveis Prefixo = `name` do serviço-banco (aqui `db`): | Motor | Referências | |---|---| | PostgreSQL | `${{db.DATABASE_URL}}`, `${{db.PGHOST}}`, `${{db.PGPORT}}`, `${{db.PGUSER}}`, `${{db.PGPASSWORD}}`, `${{db.PGDATABASE}}` | | MariaDB | `${{db.DATABASE_URL}}`, `${{db.MYSQL_HOST}}`, `${{db.MYSQL_PORT}}`, `${{db.MYSQL_USER}}`, `${{db.MYSQL_PASSWORD}}`, `${{db.MYSQL_DATABASE}}` | | SQLite | `${{db.LIBSQL_URL}}`, `${{db.LIBSQL_AUTH_TOKEN}}`, `${{db.DATABASE_URL}}` | Todas são sensíveis: o valor resolvido de quem as referencia herda a sensibilidade e fica mascarado em log e UI (veja [Referência a valor sensível](/conectados/variaveis-e-referencias/#referência-a-valor-sensível)). ## Ligar o app ao banco Defina no serviço do app uma variável cujo valor é a referência. A resolução acontece no **deploy**. ```bash P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -H "Idempotency-Key: conexao-proj-$(date +%s)-$RANDOM" \ -d "{\"name\": \"demo-conexao-$(date +%s)-$RANDOM\"}" | jq -r '.id') S=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -H "Idempotency-Key: conexao-svc-$P" \ -d '{"name": "api", "kind": "app", "visibility": "private", "source": {"type": "image", "ref": "ghcr.io/org/app:1.2"}}' | jq -r '.id') curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/DATABASE_URL" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -d '{"value": "${{db.DATABASE_URL}}"}' ``` ```json {"name": "DATABASE_URL"} ``` O `POST` do deploy responde `422 unresolved_reference` se `db` não existir no projeto ou a variável não existir (`details.references` lista quais), e `422 reference_cycle` se as referências formarem ciclo. Corrija a referência e faça deploy de novo; não adianta repetir igual. ## Conexão com TLS - PostgreSQL: mantenha `sslmode=verify-full` (já vem na `DATABASE_URL`); a CA é pública, não instale nada. - MariaDB: `ssl=true` (já vem na `DATABASE_URL`). - SQLite: `LIBSQL_URL` (TLS) + `LIBSQL_AUTH_TOKEN`. ## Como o serviço alcança o banco O serviço conecta ao `public_host` do nó de banco (por exemplo `db1.:5432`), saindo da rede do projeto pela internet pública, **sempre com TLS** (`sslmode=verify-full` no PostgreSQL: valide o certificado e o nome do host, com a CA pública, sem instalar nada). O nó de banco só aceita conexões vindas dos **IPs de saída da plataforma**: um cliente em outra origem (seu notebook, outro provedor) é recusado na rede, mesmo com a credencial certa. Cada banco tem a **sua própria credencial** (usuário e senha só dele, sem privilégios administrativos), entregue pelas referências acima. O acesso direto de fora da plataforma não é suportado: faça a consulta de dentro de um serviço do projeto. Detalhes de isolamento: [Isolamento](/seguranca/isolamento/#bancos-gerenciados). ## Quando o valor muda Rotação de credencial, restore em SQLite ou restore para banco novo mudam o valor referenciado: os serviços dependentes ficam marcados "precisam de redeploy". Faça deploy deles ([Deploys e status](/operacao/deploys-e-status/)). ## Se algo falhar | Resposta | O que fazer | |---|---| | `422 unresolved_reference` no deploy | Variável ou serviço referenciado não existe: confira o nome do serviço-banco e a variável da tabela acima. | | `422 reference_cycle` no deploy | Remova a referência circular entre variáveis. | | app não conecta (TLS/credencial) | Houve rotação? Faça deploy do app para re-resolver. | | `404 not_found` | Projeto/serviço inexistente ou de outro time. | Veja [Variáveis e referências](/conectados/variaveis-e-referencias/) e [Erros](/erros/tabela/). Se um app usa um driver de banco (PostgreSQL, MySQL, MongoDB) mas nenhum banco está ligado, a plataforma detecta e **sugere** criar um banco gerenciado e injetar a referência `${{db.DATABASE_URL}}` automaticamente — sem criar nada sem a sua autorização. Veja [Sugestões de persistência](/conectados/sugestoes-de-persistencia/). --- URL: https://docs.nebularws.com/bancos/backup-restore.md # Backup e restore Escopo: `databases:write`. Substitua `$P` (projeto) e `$DB` (serviço-banco) pelos ids. ## Política - Backup **diário** automático: retenção de 7 diários + 4 semanais. - Backup **sob demanda** quando quiser (antes de uma migration arriscada, por exemplo). - Apagar o banco faz um backup final por padrão. ## Criar um backup ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"bk-$SUF\"}" | jq -r .id) DB=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -d '{"name": "db", "engine": "postgres", "quota_mb": 1024}' | jq -r .id) # o banco nasce em provisioning: espere ficar ready antes de pedir backup for i in $(seq 1 40); do [ "$(curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB" -H "$H" | jq -r .status)" = ready ] && break sleep 2 done curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases/$DB/backups" \ -H "$H" \ -H "Idempotency-Key: backup-$DB-$(date +%Y%m%d%H%M)-$RANDOM" ``` ```json {"backup_id": "b_01J9ZK5Q", "job_id": "j_01J9ZK5R"} ``` A resposta é `202`: o backup é assíncrono. Guarde `backup_id`. ## Listar backups Paginação por cursor (`next_cursor` / `has_more`). Cada item tem `id`, `at`, `size`, `status`. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"bk-$SUF\"}" | jq -r .id) DB=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -d '{"name": "db", "engine": "postgres", "quota_mb": 1024}' | jq -r .id) # o banco nasce em provisioning: espere ficar ready antes de pedir backup for i in $(seq 1 40); do [ "$(curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB" -H "$H" | jq -r .status)" = ready ] && break sleep 2 done curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases/$DB/backups" -H "$H" >/dev/null curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB/backups" \ -H "$H" ``` ```json {"items": [{"id": "b_01J9ZK5Q", "at": "2026-10-10T03:00:00Z", "size": 1048576, "status": "running"}], "next_cursor": null, "has_more": false} ``` O item nasce `running` e vira `done`. Espere o backup concluir antes de usá-lo (restaurar um backup que ainda roda responde `409 backup_not_ready`): consulte o job com `GET /v1/projects/{p}/databases/{s}/jobs/{job_id}` (`status`: `queued`, `running`, `done`, `failed`; no `failed` vem `error` com `code` e `message`), com backoff de 2 s dobrando até 30 s, teto de uns 10 min. ## Restore `target`: - `"new"` (padrão): restaura para um banco novo; informe `new_db_name`. Seguro, não toca nos dados atuais. - `"same"`: sobrescreve o banco atual. Destrutivo para os dados atuais: no painel exige step-up (confirmação recente de identidade); pela API de token vale o escopo `databases:write`, então só use com autorização explícita de uma pessoa. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"bk-$SUF\"}" | jq -r .id) DB=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -d '{"name": "db", "engine": "postgres", "quota_mb": 1024}' | jq -r .id) # o banco nasce em provisioning: espere ficar ready antes de pedir backup for i in $(seq 1 40); do [ "$(curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB" -H "$H" | jq -r .status)" = ready ] && break sleep 2 done R=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases/$DB/backups" -H "$H") B=$(echo "$R" | jq -r .backup_id); J=$(echo "$R" | jq -r .job_id) # espera o job do backup concluir (backoff 2 s, dobrando até 30 s) delay=2 for i in $(seq 1 40); do st=$(curl -sS "$NEBULAR_API/v1/projects/$P/databases/$DB/jobs/$J" -H "$H" | jq -r .status) echo "tentativa $i: $st" [ "$st" = done ] && break case "$st" in failed) exit 1 ;; esac sleep "$delay"; delay=$(( delay * 2 > 30 ? 30 : delay * 2 )) done [ "$st" = done ] curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases/$DB/restore" \ -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: restore-$DB-$B" \ -d "{\"backup_id\": \"$B\", \"target\": \"new\", \"new_db_name\": \"n_restaurado\"}" ``` ```json {"job_id": "j_01J9ZK5S"} ``` Resposta `202`. O restore é assíncrono: consulte o resultado com backoff até concluir. ## O que muda em cada motor | Motor | Efeito do restore | |---|---| | PostgreSQL / MariaDB, `new` | cria um banco extra na mesma instância; a plataforma monta a `DATABASE_URL` do banco novo (mesmo papel e senha, outro nome de banco). Aponte o app para ele quando validar. | | PostgreSQL / MariaDB, `same` | sobrescreve o banco atual; as variáveis não mudam. | | SQLite, `new` ou `same` | **a URL e o token MUDAM em todo restore** (o namespace antigo é removido no `same`). A plataforma sela o token novo, atualiza `LIBSQL_URL`/`LIBSQL_AUTH_TOKEN` e marca os dependentes para redeploy: faça deploy deles. | ## Se algo falhar | Resposta | O que fazer | |---|---| | `401` / `403` | Falta `databases:write`. Pare e peça a uma pessoa. | | `409 backup_not_ready` | O backup ainda não terminou (job `running`): espere o job e repita. | | `404 not_found` | Banco ou `backup_id` inexistente ou de outro time. Liste os backups de novo. | | `409` | Há outra operação em andamento no banco, ou `new_db_name` já existe. Espere ou troque o nome. | | `422 quota_exceeded` | O restore não cabe na cota. Aumente a cota ou apague dados. | | `503 unavailable` | Repita com backoff e a mesma `Idempotency-Key`; persistindo, pare e avise. | | `429 rate_limited` | Espere `Retry-After`. | Veja [Erros](/erros/tabela/) e [Rate limit e retry](/erros/rate-limit-e-retry/). --- URL: https://docs.nebularws.com/bancos/migracoes.md # Migrações A plataforma não executa migrations por você: o **seu app** (ou uma ferramenta de migrations da sua linguagem) as aplica usando a `DATABASE_URL` entregue ([Conexão e referências](/bancos/conexao-e-referencias/)). ## Regras - **Toda migration tem `down`** e foi testada (suba e desça em ambiente local antes do deploy). - Mudanças destrutivas em dois passos (expand-contract): adicione o novo, migre o código, só depois remova o antigo. Nunca `DROP` ou renomeação destrutiva num passo só. - Faça um [backup sob demanda](/bancos/backup-restore/) antes de uma migration arriscada. - Migrations usam SQL parametrizado e a mesma conexão TLS (`sslmode=verify-full`, `ssl=true` ou `LIBSQL_URL`). - Cota: um banco acima de `quota_mb` fica `read_only`; migration que cresce dados pode falhar por isso. ## Onde rodar Rode as migrations na **inicialização do serviço**, antes de aceitar tráfego, de forma idempotente (a ferramenta guarda a versão aplicada). Com mais de uma instância subindo, use o lock da própria ferramenta de migrations. Se a migration falha, o app deve sair com erro: o deploy não fica `ready` e o rollback ([Rollback](/operacao/rollback/)) volta ao deploy anterior. Atenção: o rollback do deploy **não** desfaz o schema. Se o código antigo for incompatível com a migration já aplicada, rode a `down` ou restaure um backup. ## Se algo falhar | Sintoma | O que fazer | |---|---| | deploy não chega a `ready` por erro de migration | Leia o log de build/runtime ([Logs](/operacao/logs/)), corrija a migration e faça novo deploy. | | `read_only` / erro de escrita | Cota estourada: libere espaço; volta ao normal ao baixar do limite. | | `503 unavailable` ao conectar | Repita com backoff; o app não deve derrubar o deploy por indisponibilidade transitória do banco além do tempo de retry. | --- URL: https://docs.nebularws.com/conectados/front-back-db.md # Front, back e banco Objetivo: um projeto com três serviços. ```mermaid flowchart LR U[Internet] --> W[web: Next.js, public] W -- API_URL=${{api.PRIVATE_URL}} --> A[api: Go, private] A -- DATABASE_URL=${{db.DATABASE_URL}} --> D[(db: Postgres)] ``` Escopos do token: `projects:write`, `databases:write`, `env:write`, `deploy`, `projects:read`. As variáveis do shell (`H`, `P`, `DB`, `API`, `WEB`) vivem no mesmo bloco; ao repetir só um passo, recapture os ids com `GET` (lista de projetos e serviços). Os ids são opacos (ULID de 26 caracteres); os exemplos mostram o formato real. O caminho completo para código **próprio** é: banco, back, front (passos 1 a 3), **enviar o código de cada serviço** (passo 4), **deploy do grupo** (passo 5) e **polling** (passo 6). Se você já tem imagens publicadas, use a variante com imagens, no fim do passo 5. ## 1. Criar o projeto e o banco `POST .../databases` cria um serviço `kind=database`. Os motores são `postgres`, `mariadb`, `sqlite`; `quota_mb` é a cota do banco. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"loja-$SUF\"}" | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: fbd-db-$SUF" \ -d '{"name": "db", "quota_mb": 1024, "engine": "postgres"}' ``` ```json {"id": "01j9zkff8y1dmcwa49fanx9qak", "name": "db", "kind": "database", "engine": "postgres", "status": "provisioning"} ``` Em erro: `503 unavailable` (repita com backoff), `422 quota_exceeded` (reduza `quota_mb` ou veja [Limites e cotas](/operacao/limites-e-cotas/)), `403` (falta `databases:write`). ## 2. Criar o back (Go, privado) e ligar ao banco O back não tem rota pública (`private`); o `DATABASE_URL` vem do banco por referência. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: fbd2-proj-$SUF" -d "{\"name\": \"loja2-$SUF\"}" | jq -r .id) API=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: fbd2-api-$SUF" \ -d '{"name": "api", "kind": "app", "visibility": "private", "port": 8080, "source": {"type": "upload"}, "build": {"builder": "go"}, "healthcheck_path": "/healthz"}' | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$API/variables/DATABASE_URL" -H "$H" \ -H "Content-Type: application/json" -d '{"value": "${{db.DATABASE_URL}}"}' ``` A referência só é resolvida no deploy. Se `db` não existir no projeto, o `POST` do deploy responde `422 unresolved_reference` (nada é criado) com `details.references`. ## 3. Criar o front (Next.js, público) apontando para o back Todo projeto já nasce com um serviço `web`, **público** e com `slug` e `public_url` gerados (por exemplo `web-br5pg0`): o `PATCH` abaixo o adapta (fonte, build, porta) em vez de criar outro. Para um front **privado** (só alcançável na rede do projeto), mande `{"visibility": "private"}`; o `slug` e a URL pública deixam de existir. O `PATCH` com `"visibility": "public"` e sem `slug` devolve o serviço público com um `slug` derivado do nome. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: fbd3-proj-$SUF" -d "{\"name\": \"loja3-$SUF\"}" | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" -H "$H" -H "Content-Type: application/json" \ -d '{"name": "api", "kind": "app", "visibility": "private", "port": 8080, "source": {"type": "upload"}, "build": {"builder": "go"}}' >/dev/null WEB=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d "{\"visibility\": \"public\", \"slug\": \"loja3-$SUF\", \"port\": 3000, \"source\": {\"type\": \"upload\"}, \"build\": {\"builder\": \"node\"}}" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$WEB/variables/API_URL" -H "$H" \ -H "Content-Type: application/json" -d '{"value": "${{api.PRIVATE_URL}}"}' ``` No código do Next.js, chame o back **no servidor** (route handler ou server component) com `process.env.API_URL`: a URL é privada e não funciona no browser. Segredos nunca em variável `NEXT_PUBLIC_*`. ## 4. Enviar o código (upload) e implantar cada serviço Serviço com `source.type = "upload"` só tem o que implantar depois que **recebe um arquivo**. Não existe rota de "enviar sem implantar": o envio é o próprio deploy do serviço (`POST .../services/{s}/deployments` com `multipart/form-data`, campo `file`, `.tar.gz` ou `.zip`). Por isso a ordem é: 1. espere o banco ficar `ready` (`GET .../databases/{db}`); 2. envie o código do **back** e espere `ready`; 3. envie o código do **front** e espere `ready`. O exemplo cria tudo do zero com dois pacotes mínimos (troque pelos seus). O arquivo recebido fica guardado como a fonte atual do serviço. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" espera() { # espera() URL: polling com backoff até ready; falha em qualquer terminal ruim local delay=2 st for i in $(seq 1 40); do st=$(curl -sS "$1" -H "$H" | jq -r .status) case "$st" in ready) return 0 ;; failed|blocked|canceled|superseded) echo "terminou em $st" >&2; return 1 ;; esac sleep "$delay"; delay=$(( delay * 2 > 30 ? 30 : delay * 2 )) done return 1 } API_URL_BASE="$NEBULAR_API/v1/projects" P=$(curl -sS -X POST "$API_URL_BASE" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"loja7-$SUF\"}" | jq -r .id) DB=$(curl -sS -X POST "$API_URL_BASE/$P/databases" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: fbd7-db-$SUF" -d '{"name": "db", "engine": "postgres", "quota_mb": 1024}' | jq -r .id) API=$(curl -sS -X POST "$API_URL_BASE/$P/services" -H "$H" -H "Content-Type: application/json" \ -d '{"name": "api", "visibility": "private", "port": 8080, "source": {"type": "upload"}, "build": {"builder": "go"}, "healthcheck_path": "/healthz"}' | jq -r .id) WEB=$(curl -sS -X PATCH "$API_URL_BASE/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d '{"port": 3000, "source": {"type": "upload"}, "build": {"builder": "node"}}' | jq -r .id) curl -sS -X PUT "$API_URL_BASE/$P/services/$API/variables/DATABASE_URL" -H "$H" \ -H "Content-Type: application/json" -d '{"value": "${{db.DATABASE_URL}}"}' >/dev/null curl -sS -X PUT "$API_URL_BASE/$P/services/$WEB/variables/API_URL" -H "$H" \ -H "Content-Type: application/json" -d '{"value": "${{api.PRIVATE_URL}}"}' >/dev/null # pacotes mínimos (no uso real: o código do seu back e do seu front) T=$(mktemp -d) printf 'module api\n\ngo 1.22\n' > "$T/go.mod" printf 'package main\n\nfunc main() {}\n' > "$T/main.go" tar -czf "$T/api.tgz" -C "$T" go.mod main.go printf '{"name":"web","scripts":{"start":"node server.js"}}\n' > "$T/package.json" printf 'require("http").createServer((q,r)=>r.end("ok")).listen(3000)\n' > "$T/server.js" tar -czf "$T/web.tgz" -C "$T" package.json server.js espera "$API_URL_BASE/$P/databases/$DB" D=$(curl -sS -X POST "$API_URL_BASE/$P/services/$API/deployments" -H "$H" \ -H "Idempotency-Key: fbd7-api-$SUF" -F "file=@$T/api.tgz" | jq -r .id) espera "$API_URL_BASE/$P/services/$API/deployments/$D" D=$(curl -sS -X POST "$API_URL_BASE/$P/services/$WEB/deployments" -H "$H" \ -H "Idempotency-Key: fbd7-web-$SUF" -F "file=@$T/web.tgz" | jq -r .id) espera "$API_URL_BASE/$P/services/$WEB/deployments/$D" curl -sS "$API_URL_BASE/$P/services/$WEB/deployments/$D" -H "$H" ``` Ao enviar o arquivo, o `source_ref` do deploy mostra `upload:`. Em erro: `400 invalid_request` (arquivo vazio, grande demais ou sem o campo `file`), `409 deploy_in_progress` (já há um deploy do serviço rodando: acompanhe-o), `422 unresolved_reference` (a variável aponta para algo que não existe). ## 5. Deploy do grupo `POST /v1/projects/{p}/deploy` reimplanta todos os serviços em ordem topológica das referências: `db`, depois `api`, depois `web`. Use-o **depois** do passo 4, por exemplo após mudar uma variável compartilhada ou referenciada. As regras para serviços `upload`: - **Já recebeu um arquivo antes**: o grupo **reimplanta o último arquivo enviado** (o item vem com `deployment_id`). - **Nunca recebeu arquivo (sem fonte)**: o item vem `skipped`, sem `deployment_id` e com `reason` (`sem fonte: nenhum arquivo foi enviado para este serviço`). O resto do grupo segue; se **tudo** é `skipped`, o grupo termina `ready` sem implantar nada. Citar o serviço em `services` não o pula: sem fonte, o deploy dele falha. - Se o build de um serviço falhar, os dependentes ficam `blocked`; os já ativos continuam no ar. O corpo é `{}` (ou `{"services": ["api", "web"]}` para um subconjunto) com `Content-Type: application/json`; também é aceito sem corpo e sem `Content-Type` (veja [Convenções](/comecar/convencoes-da-api/)). ```bash # projeto novo: o web padrão é upload e ainda não recebeu arquivo SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"loja8-$SUF\"}" | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/deploy" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: fbd8-grp-$SUF" -d '{}' ``` ```json {"id": "01j9zk0dst6zttk3bamcgshaxf", "project_id": "01j9zkk07fhzv5zsmn996cdwpb", "status": "ready", "items": [{"service_id": "01j9zkff8y1dmcwa49fanx9qak", "status": "skipped", "layer": 0, "reason": "sem fonte: nenhum arquivo foi enviado para este serviço"}]} ``` Em erro: `unresolved_reference` ou `reference_cycle` (corrija as variáveis, veja [Variáveis e referências](/conectados/variaveis-e-referencias/)), `403` (falta `deploy`), `429` (espere `Retry-After`). ### Variante com imagens publicadas Se o back e o front já são imagens, o grupo dispara tudo de uma vez, sem passo de upload. Fixe a versão por digest (`repo:tag@sha256:...`, veja [Imagem pública](/guia/imagem-publica/#fixar-por-digest-e-como-obter-o-digest)); o exemplo usa tags só para ser curto. ```bash # preparo (passos 1 a 3): banco, back e front do projeto SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"loja4-$SUF\"}" | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/databases" -H "$H" -H "Content-Type: application/json" \ -d '{"name": "db", "engine": "postgres", "quota_mb": 1024}' >/dev/null API=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" -H "$H" -H "Content-Type: application/json" \ -d '{"name": "api", "visibility": "private", "port": 8080, "source": {"type": "image", "ref": "ghcr.io/org/api:1.0"}, "healthcheck_path": "/healthz"}' | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$API/variables/DATABASE_URL" -H "$H" \ -H "Content-Type: application/json" -d '{"value": "${{db.DATABASE_URL}}"}' >/dev/null WEB=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d "{\"visibility\": \"public\", \"slug\": \"loja4-$SUF\", \"port\": 3000, \"source\": {\"type\": \"image\", \"ref\": \"ghcr.io/org/web:1.0\"}}" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$WEB/variables/API_URL" -H "$H" \ -H "Content-Type: application/json" -d '{"value": "${{api.PRIVATE_URL}}"}' >/dev/null curl -sS -X POST "$NEBULAR_API/v1/projects/$P/deploy" \ -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: fbd-deploy-$SUF" -d '{}' ``` ```json {"id": "01j9zk0dst6zttk3bamcgshaxf", "project_id": "01j9zkk07fhzv5zsmn996cdwpb", "status": "running", "items": [{"service_id": "01j9zkff8y1dmcwa49fanx9qak", "deployment_id": "01j9zkxg2h4dbcfe129fwypfsb", "status": "queued", "layer": 0}]} ``` ## 6. Esperar com backoff (um GET para o grupo todo) Acompanhe o **grupo** com `GET /v1/projects/{p}/group-deployments/{g}` (`G` = `id` devolvido pelo `POST .../deploy`): um único `GET` mostra o `status` do grupo (`running` até terminar; depois `ready`, `failed`, `canceled` ou `superseded`) e o `status` de cada item (`queued`, `building`, `deploying`, `ready`, `failed`, `blocked`, `skipped`). Não é preciso listar os deploys de cada serviço. Para o motivo de uma falha, pegue o `deployment_id` do item `failed` e leia o [log de build](/operacao/logs/). ```bash # preparo: projeto com o front (imagem) e um deploy de grupo em andamento SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"fbd5-$SUF\"}" | jq -r .id) curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d '{"source": {"type": "image", "ref": "ghcr.io/org/app:1.0"}}' >/dev/null G=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/deploy" -H "$H" \ -H "Content-Type: application/json" -H "Idempotency-Key: fbd5-grp-$SUF" -d '{}' | jq -r .id) delay=2 for i in $(seq 1 40); do st=$(curl -sS "$NEBULAR_API/v1/projects/$P/group-deployments/$G" -H "$H" | jq -r .status) echo "tentativa $i: $st" case "$st" in ready) break ;; failed|canceled|superseded) echo "grupo terminou em $st" >&2; exit 1 ;; esac sleep "$delay"; delay=$(( delay * 2 > 30 ? 30 : delay * 2 )) done [ "$st" = ready ] curl -sS "$NEBULAR_API/v1/projects/$P/group-deployments/$G" -H "$H" ``` Estado `failed` no grupo: algum item falhou; leia o log de build dele. Item `blocked`: um serviço do qual ele depende falhou; corrija o dependente primeiro. Para acompanhar **um** serviço isolado, use `GET .../services/{s}/deployments/{d}` ([Deploys e status](/operacao/deploys-e-status/)). ## 7. Verificar ```bash curl -sS -o /dev/null -w '%{http_code}\n' "https://loja3-1760000000.nebularws.com/" ``` Se der erro `5xx`, leia o [log de runtime](/operacao/logs/) do `web` e do `api`. Para voltar a versão, [rollback](/operacao/rollback/). ## Ver também [Rede privada](/conectados/rede-privada/), [PostgreSQL](/bancos/postgres/), [Next.js](/guia/nodejs-next/), [Go](/guia/go/). --- URL: https://docs.nebularws.com/conectados/variaveis-e-referencias.md # Variáveis e referências Escopos: `env:read`, `env:write`, `env:reveal` ([Tokens e escopos](/seguranca/tokens-e-escopos/)). ## Escopos de variável - **`service`**: vale só para um serviço. Rota: `/v1/projects/{p}/services/{s}/variables[/{name}]`. - **`shared`**: guardada no projeto. Rota: `/v1/projects/{p}/shared-variables[/{name}]`. **Ela não entra sozinha no ambiente de nenhum serviço**: só chega a um serviço que a referencie com `${{shared.NOME}}` (veja abaixo). Valor literal ou com **referências**: `${{.}}` (variável de outro serviço do projeto) e `${{shared.VAR}}` (variável compartilhada). `shared` é nome reservado: nenhum serviço pode se chamar assim. ## Definir, ler e apagar ```bash SUF=$(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: var-proj-$SUF" -d "{\"name\": \"vars-$SUF\"}" | jq -r .id) S=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -H "Idempotency-Key: var-svc-$SUF" \ -d "{\"name\": \"api\", \"kind\": \"app\", \"visibility\": \"private\", \"source\": {\"type\": \"image\", \"ref\": \"ghcr.io/org/api:1.0\"}}" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/LOG_LEVEL" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"value": "info"}' ``` ```json {"name": "LOG_LEVEL", "value": "info", "sensitive": false, "needs_redeploy": []} ``` O corpo do `PUT` é `{"value": "...", "sensitive": true|false}` (`sensitive` é opcional; uma variável já sensível nunca volta a ser não sensível). A resposta traz a variável e `needs_redeploy`, a lista de serviços que precisam de novo deploy para ver o valor (vazia quando nenhum serviço usa a variável). Variável **sensível**: o `PUT` nunca devolve o valor (`"value": null`) e as leituras só o mostram a um token com `env:reveal`; sem ele, `value` vem `null` e `sensitive` vem `true`. Para definir várias de uma vez (tudo ou nada, de 1 a 200 itens), use `PUT .../variables` com `{"variables": [{"name": "A", "value": "1"}, {"name": "B", "value": "2", "sensitive": true}]}`; a resposta é `{"items": [...], "needs_redeploy": [...]}`. `GET .../variables?resolved=true` devolve as variáveis já com as referências expandidas e as automáticas (exige `env:reveal`). Variável compartilhada do projeto: ```bash SUF=$(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: var2-proj-$SUF" -d "{\"name\": \"vars2-$SUF\"}" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/shared-variables/REGION" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"value": "br"}' ``` Para que o serviço enxergue a variável compartilhada `REGION`, crie nele uma variável própria que a referencie (o nome pode ser o mesmo ou outro). Sem essa referência, `REGION` não aparece no ambiente do serviço: ```json PUT /v1/projects/{p}/services/{s}/variables/REGION {"value": "${{shared.REGION}}"} ``` Referência a outro serviço (o front lê a URL privada do back): ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: var3-proj-$SUF" -d "{\"name\": \"vars3-$SUF\"}" | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" -H "$H" -H "Content-Type: application/json" \ -d '{"name": "api", "kind": "app", "visibility": "private", "port": 8080, "source": {"type": "image", "ref": "ghcr.io/org/api:1.0"}}' >/dev/null S=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d "{\"visibility\": \"public\", \"slug\": \"vars3-$SUF\", \"source\": {\"type\": \"image\", \"ref\": \"ghcr.io/org/web:1.0\"}}" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/API_URL" -H "$H" \ -H "Content-Type: application/json" -d '{"value": "${{api.PRIVATE_URL}}"}' ``` Listar (valores sensíveis vêm mascarados sem `env:reveal`) e apagar: ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: var4-proj-$SUF" -d "{\"name\": \"var4-$SUF\"}" | jq -r .id) S=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: var4-svc-$SUF" \ -d '{"name": "app", "kind": "app", "visibility": "private", "source": {"type": "image", "ref": "ghcr.io/org/app:1.0"}}' | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/LOG_LEVEL" -H "$H" \ -H "Content-Type: application/json" -d '{"value": "info"}' curl -sS "$NEBULAR_API/v1/projects/$P/services/$S/variables" -H "$H" curl -sS -X DELETE "$NEBULAR_API/v1/projects/$P/services/$S/variables/LOG_LEVEL" -H "$H" ``` ## Variáveis automáticas (resolvíveis por referência) Todo serviço ganha, sem você definir: | Variável | Valor | |---|---| | `NEBULAR_SERVICE_NAME` | o `name` do serviço | | `PORT` | a porta interna | | `PRIVATE_HOST` | `` (hostname na [rede privada](/conectados/rede-privada/)) | | `PRIVATE_URL` | `http://:` | | `PUBLIC_URL` | URL pública, se o serviço é `public`; senão vazio | | `PUBLIC_DOMAIN` | domínio público do serviço | Serviços de banco também expõem `DATABASE_URL` e as variáveis do motor ([Conexão e referências](/bancos/conexao-e-referencias/)). Exemplo: `${{api.PRIVATE_URL}}`, `${{db.DATABASE_URL}}`. Se um app precisa de um banco ou de disco persistente que ainda não existe, a plataforma detecta e sugere criar o recurso (e, no caso de banco, injetar a referência `${{db.DATABASE_URL}}`), sempre esperando sua autorização: veja [Sugestões de persistência](/conectados/sugestoes-de-persistencia/). ## Como a resolução funciona - Acontece **no deploy**, por serviço. Quem é referenciado sobe antes (ordem topológica no deploy de grupo). - Referência a variável ou serviço inexistente: o `POST` do deploy responde `422 unresolved_reference` e nada é criado; `details.references` lista os nomes com problema. O `PUT` da variável aceita a referência: o erro só aparece no deploy. - Referências em ciclo: `422 reference_cycle` (também com `details.references`). - Outros erros de resolução presentes na API: `invalid_reference` (um `${{...}}` que não é da forma `escopo.VAR`), `reference_too_deep` (aninhamento acima de 16 níveis) e `value_too_large` (valor expandido acima de 64 KiB ou ambiente acima de 256 KiB). - Valor resolvido **herda a sensibilidade** do que foi referenciado (veja a próxima seção). - **Re-resolução**: mudar um valor referenciado marca os dependentes como "precisam de redeploy". Faça um novo deploy dos dependentes (ou ative `auto_deploy`). ## Referência a valor sensível Uma variável cujo valor é uma referência (`DATABASE_URL = ${{db.DATABASE_URL}}`) guarda o **texto da referência**, não o segredo. Por isso: - O `PUT` pode responder `"sensitive": false` e a leitura sem `resolved` mostra o texto `${{db.DATABASE_URL}}`: nada de segredo trafega ali. Se quiser a variável tratada como sensível já na definição, envie `"sensitive": true` no `PUT` (a resposta volta com `value: null`). - O **valor resolvido** herda a sensibilidade de quem foi referenciado: `GET ...?resolved=true` mostra `DATABASE_URL` com `"sensitive": true`, e o valor fica mascarado em log e painel. - Essa leitura resolvida (e `env:reveal` em geral) entrega o segredo **em claro** ao token com o escopo `env:reveal`, que precisa ser pedido explicitamente ao criar o token ([Tokens e escopos](/seguranca/tokens-e-escopos/)). Sem o escopo, `resolved=true` dá `403`. Trate a resposta como segredo: não a registre em log nem a cole em chat. - `depends_on` do serviço (usado para a ordem do deploy de grupo) é calculado das referências gravadas: o serviço `api` passa a listar `["db"]` na leitura do serviço assim que a variável com `${{db...}}` existe. A resposta de **criação** do serviço, anterior à variável, vem com `depends_on` vazio. ## Ler valores resolvidos `GET .../variables?resolved=true` devolve os valores já expandidos. Exige o escopo `env:reveal` (token criado com step-up) e devolve valores sensíveis em claro. ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"var5-$SUF\"}" | jq -r .id) S=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d '{"source": {"type": "image", "ref": "ghcr.io/org/app:1.0"}}' | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/variables/LOG_LEVEL" -H "$H" \ -H "Content-Type: application/json" -d '{"value": "info"}' >/dev/null curl -sS "$NEBULAR_API/v1/projects/$P/services/$S/variables?resolved=true" \ -H "$H" ``` ```json {"items": [{"name": "LOG_LEVEL", "value": "info", "sensitive": false}], "next_cursor": null, "has_more": false} ``` ## Se algo falhar | Resposta | O que fazer | |---|---| | `403 forbidden` | Falta `env:write` (escrever), `env:read` (ler) ou `env:reveal` (`resolved=true`). Peça token novo. | | `404 not_found` | Projeto, serviço ou variável inexistente ou fora do seu acesso. | | `422 unresolved_reference` no deploy | Crie a variável/serviço citado ou corrija o nome na referência; redeploy. | | `422 reference_cycle` no deploy | Quebre o ciclo (A referencia B que referencia A). | | `400 invalid_request` | Nome ou valor inválido: corrija. | --- URL: https://docs.nebularws.com/conectados/rede-privada.md # Rede privada ## Regras - Serviços do **mesmo projeto** ficam numa rede própria e se resolvem pelo `name` do serviço (DNS interno). O serviço `api` é alcançável por `api`, na porta do serviço. - Serviços de **projetos diferentes nunca se alcançam**. - `visibility: "private"` = **sem rota pública**: o proxy não publica e o serviço não recebe `slug`. Só os outros serviços do projeto o alcançam. - Use as variáveis automáticas em vez de montar a URL: `${{api.PRIVATE_URL}}` (`http://api:8080`) ou `${{api.PRIVATE_HOST}}` + `${{api.PORT}}`. Veja [Variáveis e referências](/conectados/variaveis-e-referencias/). - **Bancos gerenciados não ficam na rede do projeto**: moram num nó de banco e são acessados pela URL das variáveis (`DATABASE_URL` etc.), com TLS. Veja [Conexão e referências](/bancos/conexao-e-referencias/). - Tráfego do serviço para a internet segue as regras de [Isolamento](/seguranca/isolamento/). ## Criar um serviço privado ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: rp-proj-$SUF" -d "{\"name\": \"rede-$SUF\"}" | jq -r .id) curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: rp-api-$SUF" \ -d '{"name": "api", "kind": "app", "visibility": "private", "port": 8080, "source": {"type": "image", "ref": "ghcr.io/org/api:1.0"}}' ``` ```json {"id": "01j9zkjxrzywfreswk9kf9r1z7", "name": "api", "kind": "app", "visibility": "private", "port": 8080} ``` No outro serviço do projeto, defina `API_URL` como `${{api.PRIVATE_URL}}` e chame `$API_URL/...` do código. Receita completa: [Front, back e banco](/conectados/front-back-db/). ## Se algo falhar | Sintoma | O que fazer | |---|---| | O front não alcança o back | Confira o `port` do back (precisa bater com a porta em que o processo escuta), o `name` usado no hostname e se ambos estão no mesmo projeto. | | `422 unresolved_reference` no deploy | O serviço referenciado não existe no projeto ou o nome está errado. | | Quer acessar o serviço privado de fora | Não é possível; torne-o `public` (com `slug`) via `PATCH`, ou exponha só o front. | --- URL: https://docs.nebularws.com/conectados/volumes.md # Volumes Escopo: `volumes:write` (e `projects:read` para ler). ## Regras - **1 volume por serviço** neste ciclo. - `mount_path`: absoluto, não pode ser `/`, nem `/proc`, `/sys`, `/dev`, `/etc`. - `size_gb`: de 1 a 50. - O sistema de arquivos do serviço é somente leitura; o volume (e `/tmp`, 64 MB) são os únicos graváveis. - **Sobrevive a redeploy.** O redeploy recria a aplicação, nunca os dados do volume. - **Apagar** pede confirmação de identidade (step-up) **só no painel**. Com token de API, o escopo `volumes:write` é a autorização: o `DELETE` não exige step-up. ## Criar (ou substituir) o volume ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: vol-proj-$SUF" -d "{\"name\": \"vol-$SUF\"}" | jq -r .id) S=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: vol-svc-$SUF" \ -d "{\"name\": \"app\", \"kind\": \"app\", \"visibility\": \"private\", \"source\": {\"type\": \"image\", \"ref\": \"ghcr.io/org/app:1.0\"}}" | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/volume" -H "$H" \ -H "Content-Type: application/json" \ -d '{"mount_path": "/data", "size_gb": 5}' ``` ```json {"mount_path": "/data", "size_gb": 5} ``` O volume passa a existir no próximo deploy do serviço. ## Ler e apagar ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: vol2-proj-$SUF" -d "{\"name\": \"vol2-$SUF\"}" | jq -r .id) S=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services" -H "$H" -H "Content-Type: application/json" \ -H "Idempotency-Key: vol2-svc-$SUF" \ -d '{"name": "app", "kind": "app", "visibility": "private", "source": {"type": "image", "ref": "ghcr.io/org/app:1.0"}}' | jq -r .id) curl -sS -X PUT "$NEBULAR_API/v1/projects/$P/services/$S/volume" -H "$H" \ -H "Content-Type: application/json" -d '{"mount_path": "/data", "size_gb": 5}' >/dev/null curl -sS "$NEBULAR_API/v1/projects/$P/services/$S/volume" -H "$H" ``` ```json {"mount_path": "/data", "size_gb": 5} ``` Apagar (`DELETE`, resposta `204`, irreversível) exige o escopo `volumes:write`; o step-up vale só no painel: ```text DELETE /v1/projects/{p}/services/{s}/volume (Authorization: Bearer $NEBULAR_TOKEN) ``` ## Se algo falhar | Resposta | O que fazer | |---|---| | `400 invalid_request` | `mount_path` proibido ou `size_gb` fora de 1..50: corrija. | | `403 forbidden` | Falta `volumes:write`. | | `404 not_found` | Projeto/serviço inexistente ou sem acesso; ou o serviço não tem volume (no `GET`/`DELETE`). | | `quota_exceeded` | Limite de armazenamento do time: veja [Limites e cotas](/operacao/limites-e-cotas/). | --- URL: https://docs.nebularws.com/conectados/sugestoes-de-persistencia.md # Sugestões de persistência A plataforma observa seus apps e, quando percebe que um deles **precisa guardar dados mas não tem onde**, cria uma **sugestão**: uma mensagem curta explicando o problema e oferecendo a solução. A plataforma **nunca cria o recurso sozinha** — ela sugere e **espera você autorizar**, seja uma pessoa no painel ou um agent pela API. É o mesmo princípio em todo lugar: a Nebular detecta, propõe e só age com sua permissão. ## Quando uma sugestão aparece Dois gatilhos geram sugestões: 1. **Detecção no build.** Ao preparar o deploy, a plataforma olha as dependências do app (o driver de banco ou o ORM que ele usa) e deduz o tipo de persistência que ele vai querer. Por exemplo: um app Node com `better-sqlite3`, ou um app Go com `modernc.org/sqlite`, embarca um banco SQLite e vai precisar de disco persistente. 2. **Falha no deploy (sinal mais preciso).** Quando o app sobe e quebra por falta de persistência, o erro diz exatamente o que faltou: - **`activate_readonly_write`**: o app tentou gravar num caminho somente-leitura (por exemplo `/app/data`). A sugestão já vem com o caminho exato. - **Conexão recusada a um banco** (`activate_app_crashed` cujo log mostra "connection refused" numa porta de banco) e o serviço não tem banco ligado. - **`unresolved_reference`**: o deploy aponta para um serviço de banco que não existe. A sugestão **não atrapalha o deploy**: ela é informativa e fica guardada até você aceitar ou descartar. Se o recurso continua faltando, o deploy segue falhando com a mesma mensagem de sempre — nada é escondido nem quebrado. ## As duas soluções: volume ou banco A diferença mais importante é **o que o app precisa**: - **SQLite embarcado → volume.** O app já tem o banco dentro dele (um arquivo SQLite); só falta um **disco que sobreviva aos deploys**. A sugestão oferece criar um [volume](/conectados/volumes/) no caminho onde o app grava (por exemplo `/app/data`). Nada muda no código do app. - **Driver externo (PostgreSQL, MySQL, MongoDB) → banco gerenciado.** O app fala com um banco que roda **fora** dele e que ainda não existe. A sugestão oferece criar um [banco gerenciado](/bancos/postgres/) e **ligar automaticamente** com a referência `${{.DATABASE_URL}}` (onde `` é o nome do serviço-banco criado). Veja como essa ligação funciona em [Conexão e referências](/bancos/conexao-e-referencias/). Em ambos os casos a Nebular **corrige a si mesma** para aceitar o app como ele é: ela nunca edita o código do seu app. ## A plataforma nunca cria sozinha Toda sugestão nasce no estado "sugerida" e fica parada até uma decisão: - **Aceitar** cria o recurso (volume ou banco) e o liga ao serviço. O próximo deploy já usa. - **Descartar** arquiva a sugestão. Ela não volta a nascer pelo mesmo sinal na mesma janela (sem spam). Quem decide é **você**: uma pessoa no painel ou um agent pela API. Enquanto ninguém autoriza, nada é criado e nenhum dado é tocado. ## Como um agent lê as sugestões Liste as sugestões abertas de um projeto com o escopo `projects:read` ([Tokens e escopos](/seguranca/tokens-e-escopos/)): ```bash curl -sS "$NEBULAR_API/v1/projects/$P/suggestions" \ -H "Authorization: Bearer $NEBULAR_TOKEN" ``` Cada item traz o diagnóstico já pronto para mostrar a um humano, no campo `detail`: ```json { "items": [ { "id": "01j9zksf51s25jd7735q8fefyf", "service_id": "01j9zktb8c3m0xr4v6p2n1qd7e", "service_name": "polylang", "project_id": "01j9zkt0a2b3c4d5e6f7g8h9jk", "kind": "volume", "engine": "", "target_path": "/app/data", "source": "runtime_readonly", "status": "suggested", "detail": "Este app guarda dados em /app/data, mas o disco do app é somente-leitura. Posso criar um volume persistente em /app/data para os dados sobreviverem aos deploys. Criar agora?", "created_at": "2026-10-12T12:00:00Z" } ] } ``` Os campos de cada sugestão: | Campo | O que é | |---|---| | `id` | identificador da sugestão (use-o para descartar ou aceitar) | | `service_id`, `service_name` | o serviço que precisa de persistência | | `project_id` | o projeto do serviço | | `kind` | `volume` (disco persistente) ou `database` (banco gerenciado) | | `engine` | motor do banco (`sqlite`, `postgres`, `mysql`, `mongo`) ou vazio quando é volume | | `target_path` | caminho do volume (ex.: `/app/data`); nulo quando é banco | | `source` | o gatilho: `build_detect`, `runtime_readonly`, `runtime_connrefused` ou `unresolved_ref` | | `status` | `suggested`, `dismissed`, `accepted` ou `fulfilled` | | `detail` | mensagem em linguagem simples, pronta para a tela | | `created_at` | quando a sugestão foi criada | ## Como um agent descarta uma sugestão Para arquivar uma sugestão que não faz sentido, use o escopo `projects:write`: ```bash curl -sS -X POST "$NEBULAR_API/v1/projects/$P/suggestions/$SID/dismiss" \ -H "Authorization: Bearer $NEBULAR_TOKEN" ``` A sugestão passa a `dismissed` e não reaparece pelo mesmo sinal na mesma janela. ## Aceitar (criar o recurso) ainda vem a seguir Aceitar uma sugestão — ou seja, **de fato criar** o volume ou o banco e ligá-lo ao serviço — está sendo finalizado e ainda não está disponível nesta versão da API. A rota prevista é: ```text POST /v1/projects/{pid}/suggestions/{sid}/accept (escopo de escrita) ``` O formato exato do corpo dessa chamada ainda não está definido e será documentado aqui quando a criação por aceite entrar no ar. Até lá, você pode criar o recurso sugerido pela rota direta: um [volume](/conectados/volumes/) para o caso SQLite embarcado, ou um [banco gerenciado](/bancos/postgres/) com a [referência de conexão](/bancos/conexao-e-referencias/) para o caso de driver externo. ## Paridade painel e API Tudo o que o painel mostra e faz com sugestões, um agent faz pela API `/v1` — listar e descartar hoje, aceitar quando a criação por aceite entrar. Veja também o [Contrato de App Nebular](/agents/contrato-app/), que lista os erros de deploy que disparam estas sugestões, e o cookbook [Como corrigir cada erro de deploy](/agents/corrigir-deploy/) com o remédio de cada código. Quando a sugestão é um banco e o app usa SQLite embarcado, uma alternativa ao volume é [migrar para libSQL gerenciado](/agents/corrigir-deploy/#migrar-sqlite-local-para-gerenciado). --- URL: https://docs.nebularws.com/dominios/dominio-proprio.md # Domínio próprio Escopo do token: `domains:write` (e `projects:read` para ler). O serviço precisa ser `public`. ## Passos 1. **Adicione** o domínio ao serviço. A resposta traz as instruções de DNS e, enquanto não provado, o registro TXT de posse. 2. **Crie no seu DNS** o que a resposta mandar: - subdomínio: `CNAME` para `cname.` (o valor exato vem na resposta); - apex (raiz): `A` (e `AAAA`, se vier) para os IPs da plataforma; - **TXT de posse**: nome `_nebular-verify.`, valor `nebular-verify=`. É obrigatório antes da primeira ativação. - Com Cloudflare, deixe o registro em **DNS only** (nuvem cinza); o proxy laranja é recusado (`proxied`). 3. **Espere** a verificação (automática; propagação de DNS pode levar minutos): consulte o domínio com backoff (2 s, dobrando até 30 s, teto de ~10 min) até `status` ser `active`. ```bash SUF=$(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: dom-proj-$SUF" \ -d "{\"name\": \"demo-dom-$SUF\"}" | 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\": \"demo-dom-$SUF\", \"source\": {\"type\": \"image\", \"ref\": \"ghcr.io/org/app:1.2\"}}" | jq -r '.id') curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services/$S/domains" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -H "Content-Type: application/json" \ -H "Idempotency-Key: dom-add-$SUF" \ -d "{\"hostname\": \"app-$SUF.exemplo.test\"}" ``` ```json {"id": "01j9zkfgny0v4n7d9agm9tb9ht", "hostname": "app.exemplo.test", "service_id": "01j9zk3jremq8fkd3sjt7h300r", "status": "pending_dns", "dns": {"type": "CNAME", "name": "app.exemplo.test", "value": "cname.apps.nebularws.com"}, "verification": {"type": "TXT", "name": "_nebular-verify.app.exemplo.test", "value": "nebular-verify=7f3a9c"}, "created_at": "2026-10-10T12:00:00Z"} ``` Acompanhar até ficar ativo: ```bash SUF=$(date +%s)-$RANDOM H="Authorization: Bearer $NEBULAR_TOKEN" P=$(curl -sS -X POST "$NEBULAR_API/v1/projects" -H "$H" -H "Content-Type: application/json" \ -d "{\"name\": \"demo-dom-$SUF\"}" | jq -r .id) S=$(curl -sS -X PATCH "$NEBULAR_API/v1/projects/$P/services/web" -H "$H" -H "Content-Type: application/json" \ -d "{\"visibility\": \"public\", \"slug\": \"demo-dom-$SUF\", \"source\": {\"type\": \"image\", \"ref\": \"ghcr.io/org/app:1.2\"}}" | jq -r .id) D=$(curl -sS -X POST "$NEBULAR_API/v1/projects/$P/services/$S/domains" -H "$H" -H "Content-Type: application/json" \ -d "{\"hostname\": \"app-$SUF.exemplo.test\"}" | jq -r .id) delay=2 for i in $(seq 1 20); do st=$(curl -sS "$NEBULAR_API/v1/projects/$P/services/$S/domains/$D" -H "$H" | jq -r '.status') echo "tentativa $i: $st" [ "$st" = "active" ] && break sleep "$delay"; delay=$(( delay * 2 > 30 ? 30 : delay * 2 )) done [ "$st" = "active" ] ``` ## Estados | `status` | Significado | Ação | |---|---|---| | `pending_dns` | ainda sem verificação, ou a verificação achou um motivo definitivo (campo `error`: `no_records`, `wrong_target`, `proxied`, `txt_missing`) | Crie os registros da resposta (`dns` e `verification`) e espere; veja o motivo em [Problemas comuns](/dominios/problemas-comuns/). | | `verifying` | a plataforma está verificando (inclusive após falha momentânea de DNS, `error = dns_error`) | Espere com backoff. | | `active` | no ar; HTTPS emitido no primeiro acesso | Nada. | | `error` | `error = misconfigured`: o DNS mudou e não aponta mais para a plataforma (após 3 verificações falhas seguidas); `error = conflict`: o domínio já está ativo em **outro projeto** | `misconfigured`: restaure os registros (volta a `active` sozinho). `conflict`: publique o TXT de posse de **este** domínio; com a prova, a plataforma o transfere para cá. Sem a prova, ele não é seu. | ## Remover `DELETE /v1/projects/{p}/services/{s}/domains/{d}` (`domains:write`) remove o domínio; limpe também os registros DNS. ## Se algo falhar | Resposta | O que fazer | |---|---| | `409 domain_taken` | Domínio já adicionado ao time ou ativo em outro projeto: use a prova TXT (acima). | | `400 invalid_request` | Nome de domínio inválido. Corrija. | | limite de domínios do projeto | Remova um domínio ou peça aumento (veja [Limites e cotas](/operacao/limites-e-cotas/)). | | `404 not_found` | Projeto, serviço ou domínio inexistente ou de outro time. | | `429 rate_limited` | Espere `Retry-After`. | Veja também [TLS](/dominios/tls/) e [Erros](/erros/tabela/). --- URL: https://docs.nebularws.com/dominios/subdominio-gerado.md # Subdomínio gerado - Serviço com `visibility: "public"` é servido em `https://.` (o domínio de apps da plataforma; use sempre a URL que a API devolve, e a variável `PUBLIC_URL` dentro do grupo). - Serviço com `visibility: "private"` **não recebe slug nem rota pública**: só é alcançado pelo `name` dentro do grupo ([Rede privada](/conectados/rede-privada/)). - O `slug` é único na plataforma. Projetos antigos migrados mantêm o slug que já tinham, no serviço `web`. - O HTTPS é emitido no primeiro acesso. ## Criar um serviço público com slug ```bash SUF=$(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: sub-proj-$SUF" \ -d "{\"name\": \"demo-sub-$SUF\"}" | 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\": \"demo-sub-$SUF\", \"source\": {\"type\": \"image\", \"ref\": \"ghcr.io/org/app:1.2\"}}" ``` ```json {"id": "01j9zktv26djn3k6dd2yraqsbt", "name": "web", "kind": "app", "visibility": "public"} ``` ## Se algo falhar | Resposta | O que fazer | |---|---| | `409 slug_taken` | Slug já em uso. Escolha outro e repita com uma `Idempotency-Key` nova. | | `400 invalid_request` | `slug` malformado, ou `slug` informado num serviço `private` (privado não tem slug). | | `404 not_found` | Projeto inexistente ou de outro time. | Quer um nome próprio? Veja [Domínio próprio](/dominios/dominio-proprio/). Tabela completa: [Erros](/erros/tabela/). --- URL: https://docs.nebularws.com/dominios/tls.md # TLS - O HTTPS é **emitido automaticamente no primeiro acesso** a um domínio `active` (subdomínio gerado ou próprio). Você não envia certificado nem chave. - O domínio precisa estar `active` ([Domínio próprio](/dominios/dominio-proprio/)): DNS apontando para a plataforma e TXT de posse publicado. - Se o DNS está atrás de um proxy (Cloudflare laranja), a verificação falha com `proxied`: use DNS only. - Bancos gerenciados usam um certificado de CA pública no seu `public_host`; veja [Conexão e referências](/bancos/conexao-e-referencias/). Erro de certificado no primeiro acesso: confirme `status: active` e repita o acesso após alguns segundos. Persistindo, veja [Problemas comuns](/dominios/problemas-comuns/). --- URL: https://docs.nebularws.com/dominios/redirecionamentos.md # Redirecionamentos Um domínio pode ser configurado para **redirecionar para outro domínio do mesmo projeto**. Regras: - O alvo precisa ser outro domínio do mesmo projeto e **não pode ser ele próprio redirecionado** (sem cadeias, portanto sem ciclos). - Um domínio que já é alvo de redirecionamento não pode virar origem de outro. - Alvo inválido volta como erro de validação (`400`): corrija o alvo; não repita igual. Ambos os domínios precisam ter DNS e TXT corretos ([Domínio próprio](/dominios/dominio-proprio/)). Redirecionamento de caminhos arbitrários ou regras por URL não são oferecidos. Consulte a OpenAPI da v13 para o campo exato na criação/edição do domínio. Veja também [Problemas comuns](/dominios/problemas-comuns/). --- URL: https://docs.nebularws.com/dominios/problemas-comuns.md # Problemas comuns Leia `status` e o motivo (campo `error`) do domínio com `GET /v1/projects/{p}/services/{s}/domains/{d}`. | Motivo | O que significa | O que fazer | |---|---|---| | `no_records` | o nome não resolve | Crie os registros DNS da resposta (CNAME ou A/AAAA, mais o TXT) e espere propagar. | | `wrong_target` | resolve, mas não para a plataforma | Ajuste o CNAME/A para os valores da resposta. | | `proxied` | os registros estão atrás do proxy da Cloudflare | Mude para DNS only (nuvem cinza). | | `txt_missing` | DNS correto, mas falta provar a posse | Publique `_nebular-verify.` TXT `nebular-verify=`. | | `conflict` | ativo em outro projeto | Publique o TXT de posse: com a prova o domínio é transferido para este projeto. | | `dns_error` | falha momentânea ao consultar DNS | Nada: a verificação repete sozinha. Espere com backoff. | Estado `error` com `error = misconfigured`: o DNS que funcionava deixou de apontar para a plataforma; restaure os registros e o domínio reativa sozinho. Verificação manual imediata é limitada por taxa por domínio; prefira esperar com backoff (2 s, dobrando até 30 s) a repetir em laço fechado. `429` = espere `Retry-After`. Veja [Domínio próprio](/dominios/dominio-proprio/) e [Erros](/erros/tabela/). --- URL: https://docs.nebularws.com/cdn/visao-geral.md # CDN e galerias: visão geral Uma **galeria** é uma pasta de arquivos do seu time servida pela CDN da Nebular (`https://media.nebularcdn.com/...`). Com um token de API você sobe imagens, fontes, PDFs e outros arquivos, cada novo envio com o mesmo nome vira uma **versão**, e recebe duas URLs prontas para usar em sites e apps: - **URL imutável** (`url`): inclui o hash do conteúdo; nunca muda de conteúdo; cache de 1 ano. Use quando o arquivo for referenciado por algo que não deve mudar sozinho. - **URL alias** (`alias_url`): sempre aponta para a versão atual; cache curto (60 s). Use em sites: trocar a imagem não exige trocar a URL. Tudo que sobe é varrido (antivírus, regras de webshell/phishing, reputação por hash e uma varredura própria de SVG) **antes** de ser publicado. Veja [Classes de arquivo e varredura](/cdn/classes-de-arquivo-e-varredura/). Próximos passos: [Galerias e arquivos](/cdn/galerias/), [Transformar imagens](/cdn/transformar-imagens/), [Cache e purge](/cdn/cache-e-purge/). --- URL: https://docs.nebularws.com/cdn/galerias.md # Galerias e arquivos ## Galerias ### Listar galerias (`assets:read`) Parâmetros: `project` (filtra por projeto) e `cursor` (veja a receita de paginação, a receita "Listar com cursor" abaixo). ```bash curl -sS "$NEBULAR_API/v1/galleries" \ -H "Authorization: Bearer $NEBULAR_TOKEN" ``` ```json { "items": [ { "id": "gal_01J9ZK5Q", "key": "marcas-k7p2", "name": "Marcas", "bucket": {"id": "bkt_01J9ZG00", "label": "media", "kind": "shared", "host": "media.nebularcdn.com"}, "project": null, "assets_count": 12, "bytes": 4823910, "created_at": "2026-10-09T14:03:11Z" } ], "next_cursor": null, "has_more": false } ``` `key` é a identificação pública e **imutável** da galeria (nomes podem mudar): é ela que vai nas rotas e nas URLs. ### Criar galeria (`galleries:manage`) `name` (1 a 80 caracteres) é obrigatório; `project_id` (opcional) apenas organiza a galeria sob um projeto. ```bash curl -sS -X POST "$NEBULAR_API/v1/galleries" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Marcas"}' ``` ```json { "id": "gal_01J9ZK5Q", "key": "marcas-k7p2", "name": "Marcas", "bucket": {"id": "bkt_01J9ZG00", "label": "media", "kind": "shared", "host": "media.nebularcdn.com"}, "project": null, "assets_count": 0, "bytes": 0, "created_at": "2026-10-09T14:03:11Z" } ``` Guarde o `key` retornado (nos exemplos: `$GALLERY`). Cada chamada cria uma galeria nova, então não repita sem necessidade. ## Arquivos ### Enviar ou versionar (`assets:write`) Corpo `multipart/form-data` com `file` (obrigatório) e `name` (opcional; padrão: o nome do arquivo enviado normalizado). Se o nome já existir, cria uma **nova versão** e o alias passa a apontar para ela. Enviar o mesmo conteúdo (mesmo SHA-256) para o mesmo nome não cria versão nova. Respostas: **201** (limpo e já publicado), **202** (`pending`: ainda não publicado, veja [Uploads pending](#uploads-pending-202-como-esperar)) ou um erro ([Erros](/erros/tabela/)). ```bash curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -H "Idempotency-Key: logo-acme-2026-10-09" \ -F "file=@logo.png" \ -F "name=logos/acme.png" ``` ```json { "id": "ast_01J9ZK8W", "name": "logos/acme.png", "gallery_key": "marcas-k7p2", "current": { "id": "ver_01J9ZK8X", "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "bytes": 48213, "content_type": "image/png", "class": "image", "width": 1200, "height": 630, "variants": ["320", "640", "1280"], "scan": {"status": "clean", "reason": null, "engine": "clamav", "scanned_at": "2026-10-09T14:05:02Z"}, "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png", "uploaded_by": {"kind": "token", "name": "agente-deploy"}, "created_at": "2026-10-09T14:05:01Z" }, "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png", "versions_count": 1, "created_at": "2026-10-09T14:05:01Z" } ``` Use sempre a `url`/`alias_url` devolvidas pela API, sem montá-las à mão (o domínio de classes de download pode mudar). `url` e `alias_url` são `null` enquanto o arquivo não estiver `clean`. ### Idempotência (`Idempotency-Key`) Envie `Idempotency-Key` (até 255 caracteres; um UUID ou algo derivado do conteúdo) em todo upload automatizado. Mesma chave com o **mesmo corpo** devolve a mesma resposta, então repetir após timeout ou erro de rede é seguro e não cria versões duplicadas. Mesma chave com **corpo diferente** devolve `409 idempotency_conflict`: gere uma chave nova. ```bash # repetir a mesma chave com o mesmo arquivo devolve a mesma resposta curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -H "Idempotency-Key: logo-acme-2026-10-09" \ -F "file=@logo.png" \ -F "name=logos/acme.png" ``` ```json {"id": "ast_01J9ZK8W", "name": "logos/acme.png", "versions_count": 1, "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png"} ``` Mesma chave, arquivo diferente: ```bash curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -H "Idempotency-Key: logo-acme-2026-10-09" \ -F "file=@banner.png" \ -F "name=logos/acme.png" ``` ```json {"error": "idempotency_conflict", "message": "Esta Idempotency-Key já foi usada com outro conteúdo. Gere uma chave nova."} ``` ### Listar arquivos (`assets:read`) Parâmetros: `prefix` (só nomes que começam com ele) e `cursor`. ```bash curl -sS -G "$NEBULAR_API/v1/galleries/$GALLERY/assets" \ --data-urlencode "prefix=logos/" \ -H "Authorization: Bearer $NEBULAR_TOKEN" ``` ```json { "items": [ { "id": "ast_01J9ZK8W", "name": "logos/acme.png", "gallery_key": "marcas-k7p2", "current": { "id": "ver_01J9ZK8X", "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "bytes": 48213, "content_type": "image/png", "class": "image", "width": 1200, "height": 630, "variants": ["320", "640", "1280"], "scan": {"status": "clean", "reason": null, "engine": "clamav", "scanned_at": "2026-10-09T14:05:02Z"}, "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png", "uploaded_by": {"kind": "token", "name": "agente-deploy"}, "created_at": "2026-10-09T14:05:01Z" }, "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png", "versions_count": 1, "created_at": "2026-10-09T14:05:01Z" } ], "next_cursor": null, "has_more": false } ``` ### Detalhe, versões e status de varredura (`assets:read`) O nome vai na rota; use `%2F` no lugar de cada `/` (a barra literal também é aceita). ```bash curl -sS "$NEBULAR_API/v1/galleries/$GALLERY/assets/logos%2Facme.png" \ -H "Authorization: Bearer $NEBULAR_TOKEN" ``` ```json { "asset": { "id": "ast_01J9ZK8W", "name": "logos/acme.png", "gallery_key": "marcas-k7p2", "current": { "id": "ver_01J9ZK8X", "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "bytes": 48213, "content_type": "image/png", "class": "image", "width": 1200, "height": 630, "variants": ["320", "640", "1280"], "scan": {"status": "clean", "reason": null, "engine": "clamav", "scanned_at": "2026-10-09T14:05:02Z"}, "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png", "uploaded_by": {"kind": "token", "name": "agente-deploy"}, "created_at": "2026-10-09T14:05:01Z" }, "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png", "versions_count": 1, "created_at": "2026-10-09T14:05:01Z" }, "versions": [ { "id": "ver_01J9ZK8X", "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "bytes": 48213, "content_type": "image/png", "class": "image", "width": 1200, "height": 630, "variants": ["320", "640", "1280"], "scan": {"status": "clean", "reason": null, "engine": "clamav", "scanned_at": "2026-10-09T14:05:02Z"}, "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png", "uploaded_by": {"kind": "token", "name": "agente-deploy"}, "created_at": "2026-10-09T14:05:01Z" } ] } ``` ### Excluir (`assets:write`) Exclui o arquivo e purga o cache da CDN (a URL passa a dar 404). O backup retém os bytes por 30 dias. Resposta: `204` sem corpo. ```bash curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -F "file=@hero.png" -F "name=temp/hero.png" -o /dev/null curl -sS -X DELETE "$NEBULAR_API/v1/galleries/$GALLERY/assets/temp%2Fhero.png" \ -H "Authorization: Bearer $NEBULAR_TOKEN" -w 'HTTP %{http_code}\n' ``` Depois disso o `GET` do mesmo nome devolve `404 not_found`. ## Uploads pending (202): como esperar Se o scanner estiver ocupado ou fora do ar, o upload devolve **202** com `scan.status = "pending"`, `url` e `alias_url` `null`: o arquivo foi recebido mas **não está publicado**. **Não reenvie.** Consulte o detalhe do arquivo com backoff (comece em 2 s, dobre a cada tentativa até 30 s) até `scan.status` deixar de ser `pending`: `clean` (publicado, `url`/`alias_url` preenchidos) ou `rejected` (recusado; o campo `scan.reason` diz o motivo). ```bash curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -F "file=@slow.png" -F "name=site/lento.png" ``` ```json { "id": "ast_01J9ZK8W", "name": "site/lento.png", "gallery_key": "marcas-k7p2", "current": { "id": "ver_01J9ZK8X", "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "bytes": 48213, "content_type": "application/octet-stream", "class": "binary", "width": null, "height": null, "variants": [], "scan": {"status": "pending", "reason": null, "engine": null, "scanned_at": null}, "url": null, "uploaded_by": {"kind": "token", "name": "agente-deploy"}, "created_at": "2026-10-09T14:05:01Z" }, "alias_url": null, "versions_count": 1, "created_at": "2026-10-09T14:05:01Z" } ``` Enquanto `pending`, `content_type`/`class` são provisórios (`application/octet-stream`/`binary`) e `width`/`height` são `null`; valem os valores do `GET` depois que o status virar `clean`. Polling com backoff (no máximo 8 tentativas): ```bash delay=2 for attempt in 1 2 3 4 5 6 7 8; do status=$(curl -sS "$NEBULAR_API/v1/galleries/$GALLERY/assets/site%2Flento.png" \ -H "Authorization: Bearer $NEBULAR_TOKEN" | jq -r '.asset.current.scan.status') echo "tentativa $attempt: $status" [ "$status" = "pending" ] || break sleep "$delay" delay=$(( delay * 2 > 30 ? 30 : delay * 2 )) done [ "$status" = "clean" ] # sai com erro se foi recusado ou se ainda está pendente ``` ## Receitas ### (a) Subir e usar no Next com `next/image` 1. Suba a imagem e guarde a `alias_url`: ```bash curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -H "Idempotency-Key: hero-$(cksum < hero.png | tr ' ' '-')" \ -F "file=@hero.png" -F "name=site/hero.png" | jq -r '.alias_url' ``` 2. Um loader de `next/image` que acrescenta `?w=&q=` (larguras do `next/image` já estão na lista permitida): ```ts // lib/nebularLoader.ts import type { ImageLoaderProps } from "next/image"; export default function nebularLoader({ src, width, quality }: ImageLoaderProps): string { const url = new URL(src); url.searchParams.set("w", String(width)); url.searchParams.set("q", String(quality ?? 75)); return url.toString(); } ``` 3. Libere o host e use o loader (o `src` é a `alias_url` sem parâmetros): ```ts // next.config.ts import type { NextConfig } from "next"; const config: NextConfig = { images: { loader: "custom", loaderFile: "./lib/nebularLoader.ts", }, }; export default config; ``` ```tsx import Image from "next/image"; export function Hero() { return ( Banner principal ); } ``` Como o alias sempre aponta para a versão atual, trocar a imagem (receita c) atualiza o site sem novo deploy. ### (b) Subir em lote com laço shell (429, Retry-After e idempotência) A `Idempotency-Key` derivada do conteúdo torna o laço seguro de repetir inteiro: o que já foi enviado não gera versão nova. Erros permanentes (4xx exceto 429) não são repetidos; 429 e 503 esperam `Retry-After`. ```bash fail=0 for f in imagens/*.png; do name="lote/$(basename "$f")" key="lote-$name-$(cksum < "$f" | tr ' ' '-')" for try in 1 2 3 4 5; do code=$(curl -sS -o resp.json -D hdr.txt -w '%{http_code}' \ -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -H "Idempotency-Key: $key" \ -F "file=@$f" -F "name=$name") case "$code" in 201|202) echo "$name -> $code"; break ;; 429|503) wait=$(awk -F': ' 'tolower($1)=="retry-after"{print $2+0}' hdr.txt) echo "$name -> $code, aguardando ${wait:-5}s (tentativa $try)" >&2 sleep "${wait:-5}" ;; *) echo "$name FALHOU ($code): $(cat resp.json)" >&2; fail=1; break ;; esac done done [ "$fail" = 0 ] ``` Arquivos que voltarem `202` ainda precisam do polling da [Uploads pending](#uploads-pending-202-como-esperar) antes de a URL existir. ### (c) Trocar uma imagem mantendo a `alias_url` Envie o novo arquivo **com o mesmo `name`**: nasce uma nova versão e o alias passa a servir a nova imagem; a `url` imutável antiga continua válida (e imutável). Não precisa mudar nada em quem usa a `alias_url`. ```bash alias_antes=$(curl -sS "$NEBULAR_API/v1/galleries/$GALLERY/assets/site%2Fhero.png" \ -H "Authorization: Bearer $NEBULAR_TOKEN" | jq -r '.asset.alias_url') curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \ -H "Authorization: Bearer $NEBULAR_TOKEN" \ -F "file=@logo-v2.png" -F "name=site/hero.png" > nova.json jq -r '"versões: \(.versions_count)"' nova.json test "$(jq -r '.alias_url' nova.json)" = "$alias_antes" # a alias_url não mudou test "$(jq -r '.versions_count' nova.json)" = "2" ``` Se a `alias_url` ainda mostrar a imagem antiga por instantes, é o cache de 60 s do alias; a `url` imutável da nova versão já serve o novo conteúdo. ### (d) Listar com cursor (`next_cursor` / `has_more`) Siga `next_cursor` enquanto `has_more` for `true`. O cursor é opaco: não monte nem altere. ```bash cursor="" while :; do url="$NEBULAR_API/v1/galleries/$GALLERY/assets" [ -n "$cursor" ] && url="$url?cursor=$cursor" page=$(curl -sS "$url" -H "Authorization: Bearer $NEBULAR_TOKEN") echo "$page" | jq -r '.items[] | "\(.name)\t\(.alias_url)"' [ "$(echo "$page" | jq -r '.has_more')" = "true" ] || break cursor=$(echo "$page" | jq -r '.next_cursor') done ``` O mesmo vale para `GET /v1/galleries`. --- URL: https://docs.nebularws.com/cdn/transformar-imagens.md # Transformar imagens **Imagens** (`class: image`) aceitam `?w=&q=` em ambas as URLs; o formato (AVIF/WebP/original) é negociado pelo cabeçalho `Accept` (`Vary: Accept`): - `w` só aceita as larguras `16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840` (as mesmas do `next/image`); outro valor ⇒ `400` da CDN. Nunca amplia além do original. - `q` vai de 30 a 95 (padrão 75 quando só `w` é enviado) e **exige** `w`. - `w` e `q` valem **só para imagens**: em qualquer outra classe são ignorados. **Parâmetros desconhecidos** (por exemplo `?v=123` como cache-buster) também são ignorados e não geram variantes novas. Receita com `next/image`: veja [Galerias e arquivos](/cdn/galerias/#a-subir-e-usar-no-next-com-nextimage). --- URL: https://docs.nebularws.com/cdn/classes-de-arquivo-e-varredura.md # Classes de arquivo e varredura ## Classes de arquivo A classe vem do **conteúdo** (magic bytes), nunca da extensão nem do `Content-Type` enviado. | Classe | Exemplos | Servido como | |---|---|---| | `image` | png, jpeg, webp, gif, avif | inline, transformável (`?w=`, `?q=`) | | `svg` | svg | inline **só se passar na varredura de SVG**, sempre com CSP restritiva (`default-src 'none'; sandbox`) | | `font` | woff2, woff, ttf, otf | inline, CORS `*` | | `media` | mp4, webm, mp3, ogg | inline, com `Accept-Ranges` | | `document` | pdf, txt, csv, json | **download** (`Content-Disposition: attachment`); texto sempre `text/plain` | | `markup` | html, htm, xhtml, xml | **nunca renderiza**: download + `text/plain` + CSP `sandbox` | | `archive` | zip, tar.gz | download; cifrado ou impossível de varrer é recusado | | `binary` | exe, msi, dmg, apk, deb, appimage, desconhecido | download, `application/octet-stream` | As URLs são sem cookies, com `X-Content-Type-Options: nosniff`; CORS `*` só para `image` e `font`. Não use um arquivo de download (`document`, `markup`, `archive`, `binary`) como `