Pular para o conteúdo

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.

POST .../databases cria um serviço kind=database. Os motores são postgres, mariadb, sqlite; quota_mb é a cota do banco.

Janela do terminal
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"}'
{"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).

O back não tem rota pública (private); o DATABASE_URL vem do banco por referência.

Janela do terminal
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

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.

Janela do terminal
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

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 é:

  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.

Janela do terminal
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:<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).

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

Janela do terminal
# 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 '{}'
{"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).

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.

Janela do terminal
# 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 '{}'
{"id": "01j9zk0dst6zttk3bamcgshaxf", "project_id": "01j9zkk07fhzv5zsmn996cdwpb", "status": "running", "items": [{"service_id": "01j9zkff8y1dmcwa49fanx9qak", "deployment_id": "01j9zkxg2h4dbcfe129fwypfsb", "status": "queued", "layer": 0}]}

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.

Janela do terminal
# 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).

Janela do terminal
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.

Rede privada, PostgreSQL, Next.js, Go.