Front, back e banco
Objetivo: um projeto com três serviços.
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
Seção intitulada “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.
SUF=$(date +%s)-$RANDOMH="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"}'{"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), 403 (falta databases:write).
2. Criar o back (Go, privado) e ligar ao banco
Seção intitulada “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.
SUF=$(date +%s)-$RANDOMH="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
Seção intitulada “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.
SUF=$(date +%s)-$RANDOMH="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/nullWEB=$(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
Seção intitulada “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 é:
- espere o banco ficar
ready(GET .../databases/{db}); - envie o código do back e espere
ready; - 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.
SUF=$(date +%s)-$RANDOMH="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/nullcurl -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.goprintf '{"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:<nome-do-arquivo>. 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
Seção intitulada “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, semdeployment_ide comreason(sem fonte: nenhum arquivo foi enviado para este serviço). O resto do grupo segue; se tudo éskipped, o grupo terminareadysem implantar nada. Citar o serviço emservicesnã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).
# projeto novo: o web padrão é upload e ainda não recebeu arquivoSUF=$(date +%s)-$RANDOMH="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 '{}'{"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), 403 (falta deploy), 429 (espere Retry-After).
Variante com imagens publicadas
Seção intitulada “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); o exemplo usa tags só para ser curto.
# preparo (passos 1 a 3): banco, back e front do projetoSUF=$(date +%s)-$RANDOMH="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/nullAPI=$(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/nullWEB=$(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 '{}'{"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)
Seção intitulada “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.
# preparo: projeto com o front (imagem) e um deploy de grupo em andamentoSUF=$(date +%s)-$RANDOMH="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/nullG=$(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=2for 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).
7. Verificar
Seção intitulada “7. Verificar”curl -sS -o /dev/null -w '%{http_code}\n' "https://loja3-1760000000.nebularws.com/"Se der erro 5xx, leia o log de runtime do web e do api. Para voltar a versão, rollback.