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

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

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