Pular para o conteúdo

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.

  • Token com os escopos projects:write, deploy, env:write e logs:read (veja Token de API e 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.

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/<x>. Com vários programas em cmd/, veja 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.

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 ou um banco.
  • Segredo vai em variável do serviço (env:write), nunca no código, nunca em NEXT_PUBLIC_* ou equivalente de bundle de browser.
// 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))
}

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/<x> 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.

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

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

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

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.

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

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.

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

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.

Janela do terminal
# 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 <public_url>/healthz (esperado 200; exige DNS e o container no ar, por isso esse último passo não roda no teste).

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

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.
failed: “nenhum package main” Ponha package main na raiz ou em cmd/<x>, 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/).

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

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