# App pronto para a Nebular e para escalar

Para a versão curta e normativa, que serve de briefing a um agent, veja o [Contrato de App Nebular](/agents/contrato-app/).

Esta página é para quem escreve o app (pessoa ou agent). Ela junta o que a plataforma **faz hoje** com um conjunto de hábitos que evitam reescrever o app quando houver mais de uma cópia dele rodando.

## O que existe hoje e o que não existe

| Tema | Hoje | Futuro |
|---|---|---|
| Cópias do serviço | **Uma** por serviço. Não há campo de réplicas na API. | Réplicas **não existem** e **não há data prometida**. Esta página só prepara o app para o dia em que existirem. |
| Deploy | Blue/green: o container novo passa no health check antes de receber tráfego; o antigo some após uma drenagem. Se o novo falha, o anterior continua no ar. | Sem mudança no app. |
| Recursos | `resources.memory_mb` e `resources.cpus` por serviço ([Escala e recursos](/operacao/escala-e-recursos/)). Serviços novos nascem com 512 MB e 0.5 CPU; quando o serviço não define nada, o runtime usa 1 GiB e 1 vCPU. Declare `resources` explícito e não dependa do padrão. | — |
| Escalar | Só **vertical** (mais memória/CPU, até o máximo do plano). | Horizontal, se e quando existir. |
| Disco | Raiz somente leitura. Graváveis: `/tmp` (tmpfs, 64 MB, sem exec) e o [volume](/conectados/volumes/) do serviço (1 por serviço, 1 a 50 GB). | Volume continua pertencendo a um serviço. |

**Escreva hoje como se houvesse N cópias**: custa pouco agora. Cron e worker gerenciados também não existem.

## Princípios (12-factor aplicados à Nebular)

### 1. Processo sem estado

Cada container é **efêmero**: roda sob gVisor (`runsc`), é recriado a cada deploy e pode ser reiniciado (limite de memória estourado, queda do app). Nada que esteja só na memória ou no disco do container sobrevive a isso.

Não guarde em variável global, arquivo local ou memória do processo:

- **sessão de login** (use token assinado, ou tabela de sessões no banco);
- **cache que precisa ser coerente** entre requisições (um cache só de otimização, que pode estar vazio a qualquer momento, é aceitável);
- **uploads** (veja a seção de réplicas);
- **contadores, filas e locks** (use o banco).

Teste simples: reinicie o app no meio de um fluxo. Se o usuário perde algo, havia estado local.

### 2. Config por variável de ambiente

Toda configuração vem do ambiente: URL do banco, chaves, URLs de outros serviços. Nada de arquivo de config gravado no build com valores de produção, e nada de segredo no repositório.

- Variáveis do serviço e do projeto: [Variáveis e referências](/conectados/variaveis-e-referencias/). Para apontar para outro serviço use `${{api.PRIVATE_URL}}`; para um valor comum a vários serviços, `${{shared.NOME}}`.
- **Banco gerenciado**: o app lê `DATABASE_URL` (via referência `${{db.…}}`, veja [Conexão e referências](/bancos/conexao-e-referencias/)). Não monte a URL na mão nem use outro banco.
- Mudar variável só vale no **próximo deploy**.
- Segredo nunca em `NEXT_PUBLIC_*` ou equivalente de bundle de browser.

### 3. Escute em `0.0.0.0:$PORT`

O app lê a variável `PORT` e escuta em `0.0.0.0`, não em `localhost`/`127.0.0.1`. A porta declarada no serviço (`port`) sempre vence e é a que o container recebe em `PORT`. Portas abaixo de 1024 não estão disponíveis (Go: `"0.0.0.0:" + os.Getenv("PORT")`; Node: `server.listen(PORT, "0.0.0.0")`).

### 4. Health check barato

Sem `healthcheck_path`, qualquer resposta HTTP em `/` basta. Com ele, o container novo só recebe tráfego quando `GET <healthcheck_path>` responde **2xx ou 3xx** (redirecionamento não é seguido). O tempo total de espera é de cerca de 60 s, seguido de uma janela de estabilização (cerca de 30 s) em que o container não pode cair.

Faça o endpoint responder rápido e sem efeito colateral: `200 OK` sem consultar nada, ou no máximo um `SELECT 1`. **Não** rode migration, aquecimento pesado nem chamada a terceiros dentro dele. Um app que só abre a porta depois de terminar um trabalho longo falha em `activate_no_http`.

### 5. Shutdown gracioso no `SIGTERM`

Ao trocar de versão, a plataforma envia `SIGTERM` ao container antigo (e `SIGKILL` depois de um prazo curto, na ordem de segundos). O app deve parar de aceitar conexões novas, terminar as requisições em andamento e sair com código 0.

Em Go, `signal.NotifyContext(ctx, syscall.SIGTERM)` + `srv.Shutdown(ctx)`; em Node, `process.on("SIGTERM", () => server.close(() => process.exit(0)))`. Evite `npm start` encadeado em shell: o sinal pode não chegar ao app.

### 6. Logs no stdout, em JSON

A plataforma lê stdout/stderr ([Logs](/operacao/logs/)), mascarando valores sensíveis. Não grave arquivo de log. Uma linha JSON por evento, com `request_id`. Em falha de boot, logue a causa antes de sair: o erro do deploy usa as últimas linhas do container.

### 7. Assets e dados gerados entram no BUILD, nunca no boot

O diretório do app (`/app`) é **somente leitura** em runtime. Tudo que o app precisa ter em disco tem de existir quando o container sobe.

Caso real: uma aplicação de música tentava baixar o SoundFont (`scripts/get-soundfont.sh`) ao iniciar e gravar em `data/soundfonts/FluidR3_GM.sf2`. O boot falhava com `open data/soundfonts/...: no such file or directory` e o deploy terminava em `activate_app_crashed`. Caminhos corretos:

1. **Baixar no build** (`build_command` ou etapa do `Dockerfile`), fixando versão e hash, e ler de dentro do artefato.
2. **Commitar** o arquivo, se o tamanho permitir.
3. Se é grande e não muda por deploy, baixar **uma vez** para um volume e reaproveitar.

Nunca: baixar ou gerar em `/app/...` no boot.

### 8. Escreva só em `/tmp` ou no volume

- `/tmp`: 64 MB, na memória, sem exec, apagado a cada reinício e deploy.
- Volume de serviço: sobrevive a deploys, mas é de **um** serviço. Serve para dados que não precisam ser vistos por outra cópia (ex.: arquivo [SQLite](/bancos/sqlite/), cache refazível).

Se o app grava em `/app/data`, crie o volume nessa pasta ou leia o caminho de uma variável (`DATA_DIR`), nunca fixo no código.

### 9. Idempotência

Requisições chegam duas vezes (retry, deploy no meio da chamada). Aceite `Idempotency-Key` com `UNIQUE` no banco, use `INSERT ... ON CONFLICT DO NOTHING` e registre efeitos externos (e-mail, cobrança) como "já enviado".

### 10. Sem cron interno

Não há agendador nem worker gerenciado, e `setInterval`/cron dentro do app reinicia a cada deploy e rodaria N vezes com réplicas. Alternativas: um **agendador externo** (cron na internet ou GitHub Actions `schedule`) chamando um endpoint idempotente (`POST /internal/jobs/limpeza` com segredo no cabeçalho e lock no banco); trabalho sob demanda disparado pela ação do usuário; ou **fila no banco** (veja abaixo). O serviço de worker também precisa responder HTTP hoje.

### 11. Migrations idempotentes e compatíveis

No blue/green a versão antiga e a nova convivem por segundos no **mesmo banco**. Faça migrations idempotentes e protegidas por **advisory lock** (duas cópias subindo juntas não migram duas vezes), em dois tempos (adicione a coluna antes de usá-la; remova a antiga só no deploy seguinte) e sem migração pesada no boot, que atrasa o health check ([Migrações](/bancos/migracoes/)).

## Pronto para réplicas (quando existirem)

Réplicas **não existem hoje**. Quando existirem, o tráfego será dividido entre cópias idênticas, sem afinidade: duas requisições do mesmo usuário podem cair em cópias diferentes. Cada linha abaixo é um defeito latente desse dia.

| Se o app faz isto | Com N cópias vira | Faça isto |
|---|---|---|
| Sessão em memória ou em arquivo | usuário "desloga" ao cair em outra cópia | token assinado (JWT/cookie assinado) ou tabela de sessões no banco gerenciado |
| Upload gravado em `/tmp`, no disco ou no volume do serviço (que é de uma cópia só) | arquivo existe só em uma cópia e some no reinício ou não é visto pelas outras | envie direto para a [CDN/galeria](/cdn/visao-geral/) ou outro armazenamento de objetos por API, e guarde só a referência no banco |
| Fila ou lock em memória | dois consumidores pegam o mesmo item | fila no banco (`FOR UPDATE SKIP LOCKED`) e lock pelo banco (`pg_advisory_lock`) |
| Job periódico dentro do app | roda N vezes | agendador externo chamando endpoint idempotente, com lock no banco |

### Fila e lock no banco (Postgres)

```sql
UPDATE jobs SET status = 'running', started_at = now()
WHERE id = (
  SELECT id FROM jobs WHERE status = 'pending'
  ORDER BY created_at LIMIT 1
  FOR UPDATE SKIP LOCKED
)
RETURNING *;

```

Use pool de conexões pequeno por cópia (5 a 10): com N cópias o total é N vezes o pool e o banco tem limite de conexões.

## Rede e egress

Serviços do projeto se falam pelo nome ([Rede privada](/conectados/rede-privada/)). Saída para a internet é permitida; **bloqueados**: SMTP direto (use e-mail por API HTTPS), rede interna, host e metadados da nuvem ([Isolamento](/seguranca/isolamento/)). 

## Checklist: meu app está pronto para a Nebular?

- [ ] Escuta em `0.0.0.0` na porta de `PORT` e a `port` do serviço bate com ela.
- [ ] `healthcheck_path` responde 2xx/3xx rápido, sem trabalho pesado.
- [ ] Nada é gravado em `/app`; escritas vão só para `/tmp` ou para o volume, e o caminho vem de variável de ambiente.
- [ ] Assets, modelos e arquivos gerados são baixados ou criados **no build** (ou commitados).
- [ ] Toda config e todo segredo vêm de variável; `DATABASE_URL` vem da referência do banco.
- [ ] O boot não depende de outro serviço estar de pé (tenta de novo e degrada em vez de abortar).
- [ ] `SIGTERM` encerra o app limpo em poucos segundos.
- [ ] `resources.memory_mb`/`cpus` declarados com folga para o pico (o estouro de memória reinicia o container).
- [ ] Migrations idempotentes, com lock e compatíveis com a versão anterior.
- [ ] Sem cron, fila ou lock em memória; sem sessão ou upload em disco local.

## Erros comuns

Um deploy que falha termina em `failed` com um `error.code` estável ([Deploys e status](/operacao/deploys-e-status/)); leia também o log de build e do container ([Logs](/operacao/logs/)). A **versão anterior continua no ar**.

| `error.code` | Causa mais comum | Correção |
|---|---|---|
| `activate_readonly_write` | o app tentou gravar em `/app/...` (`EROFS`, `mkdir`, `open ...: read-only file system`) | gravar em `/tmp`; ou criar um volume na pasta indicada pela mensagem; ou gerar o arquivo no build |
| `activate_app_crashed` | o processo caiu logo ao iniciar: variável ausente, arquivo ausente (asset não gerado no build), exceção no boot, banco inalcançável | ler a "primeira linha de erro" na mensagem e o log do container; definir a variável; incluir o arquivo no build; tratar a falha de conexão sem abortar |
| `activate_no_http` | o app roda mas não respondeu na porta: escuta em `127.0.0.1`/outra porta, ou demora a abrir a porta, ou `healthcheck_path` devolve 4xx/5xx | escutar em `0.0.0.0:$PORT`; declarar `port`; abrir a porta primeiro e fazer o trabalho pesado depois; corrigir o caminho do health check |
| `activate_oom` | o app usou mais memória que o limite do serviço | subir `resources.memory_mb`, reduzir caches e concorrência; novo deploy |
| `image_requires_root` | a imagem exige root (container encerrou nos primeiros 30 s) | declarar `build.user` (`uid[:gid]`, nunca root) ou usar uma variante sem privilégio |
| `build_failed` e família (`build_install_failed`, `build_script_failed`, `build_detect_failed`, `build_clone_failed`, `build_copy_failed`) | instalação de dependência, script de build ou detecção do builder falhou | ler o log de build; fixar versões; declarar `build.builder`/`build_command`; `root_dir` correto em monorepo |
| `build_oom`, `build_timeout`, `build_disk_full` | o build estourou memória, tempo ou disco | reduzir paralelismo e o que entra no artefato; tirar etapas pesadas do build |
| `unresolved_reference` (`422` ao disparar o deploy) | `${{svc.VAR}}` aponta para variável ou serviço que não existe | criar a variável/serviço ou corrigir o nome ([Tabela de erros](/erros/tabela/)) |

## Para agents

Confirme `PORT`, escrita só em `/tmp`/volume e config por variável antes de codar; gere assets no build; não crie cron nem fila em memória (proponha agendador externo); não diga que o app "tem réplicas" (hoje há uma cópia por serviço); em falha, leia `error.code` antes de mexer no código.
