# Por que seu app funciona localmente e quebra quando você faz o deploy

O container diz Up e nada responde. Três dos quatro motivos são uma linha cada, e o quarto é o único que exige uma decisão.

URL: https://www.back4app.com/blog/pt/por-que-meu-app-funciona-localmente-mas-quebra-no-deploy
Publicado: 2026-10-31 · Testado: 2026-10-09
Testado em: Local: Docker 29.4.3 no macOS, arm64 · App: Node 22, Express 5 · Backend: Back4app, plano Free, USA East
Publisher: Back4app Engineering

Você pediu a uma ferramenta de IA um app de notas com resumos por IA, e ela entregou. Ele roda. Você abre no localhost, ele salva notas, parece pronto.

Então você faz o deploy, e a plataforma diz que o container está **Up** — e nada responde. Sem stack trace, sem log de crash, sem pista. Só um container de aparência saudável e uma requisição que dá timeout.

Este guia pega exatamente esse app, reproduz as quatro falhas que o deixaram nesse estado e mostra a saída real de cada uma. Três são uma linha. A quarta é a única que exige que você decida alguma coisa.

![Arquitetura: os quatro pontos em que um app gerado por IA quebra entre um laptop e um container em deploy, e como cada um aparece visto de fora](/blog/blog-assets/app-works-locally-crashes-when-deployed/architecture-diagram.svg)

## Por que um container saudável não responde nada?

Porque "rodando" e "alcançável" são afirmações diferentes, e a plataforma só consegue verificar a primeira. Um container cujo processo não encerrou está `Up`, seja lá o que esse processo estiver fazendo.

Este é o estado em que o app em deploy realmente estava. Visto de fora, o container parece correto: está rodando, e a porta está mapeada:

```text
STATUS            PORTS
Up 10 minutes     0.0.0.0:8080->8080/tcp
```

E toda requisição a ele falha:

```text
GET localhost:8080  →  HTTP 000  (0.000658s)
```

`000` não é um status que o app devolveu. É o curl informando que nada aceitou a conexão. Olhar dentro do container explica o motivo:

```text
netstat -tln | grep LISTEN
tcp   0   0   127.0.0.1:3000   0.0.0.0:*   LISTEN

printenv PORT
8080
```

A plataforma ofereceu a porta 8080 e o app a ignorou, escutando na 3000. Pior: ele se vinculou a `127.0.0.1`, a interface de loopback, então mesmo uma requisição chegando na porta 3000 de fora do container não encontraria nada. Dois erros moram nessa única linha, e o segundo é o que as pessoas deixam passar.

Como medimos: o container foi construído a partir do app sem modificações, rodado com `PORT=8080` e a porta publicada, e sondado do host com curl; o socket em escuta foi lido de dentro do container com `netstat`.

## Como fazer o app escutar onde a plataforma está olhando?

Leia a porta do ambiente e vincule todas as interfaces. Uma linha substitui os dois erros:

```js
// Stack: Node 22 + Express 5 | File: server.js
const PORT = process.env.PORT || 3000;
app.listen(PORT, '0.0.0.0', () => console.log(`listening on 0.0.0.0:${PORT}`));
```

O `|| 3000` mantém o desenvolvimento local funcionando exatamente como antes, então esta não é uma mudança que você precisa lembrar de desfazer. `0.0.0.0` significa "toda interface que este container tem", que é o que o torna alcançável de fora dele.

Com essa única linha alterada, o mesmo container responde:

```text
GET /          → HTTP 200
GET /healthz   → HTTP 200
listening on   0.0.0.0:8080
```

## A falta de um script start quebra mesmo o deploy?

Às vezes, e quais vezes é a parte surpreendente. O npm traz um script start padrão, e se você se beneficia dele ou não depende do nome do seu arquivo de entrada.

Este `package.json` não tem script `start` nenhum:

```json
{
  "scripts": { "dev": "node server.js" }
}
```

Rode `npm start` contra ele duas vezes, mudando só o nome do arquivo:

```text
entry = server.js   →   the app starts
entry = app.js      →   npm error Missing script: "start"
```

O npm substitui por `node server.js` quando existe um arquivo com esse nome, e por nada caso contrário. Então o mesmo script ausente é inofensivo num projeto e fatal no seguinte, decidido por uma escolha de nome que a ferramenta de IA fez por você.

Como medimos: dois diretórios com o `package.json` idêntico, diferindo apenas em o arquivo de entrada ser `server.js` ou `app.js`, cada um rodado com `npm start` num container `node:22-alpine` limpo.

```json
{
  "scripts": {
    "dev": "node server.js",
    "start": "node server.js"
  }
}
```

## O que acontece com uma chave que você colou no código?

Ela vai junto. Uma chave num arquivo-fonte está na imagem, e qualquer pessoa que consiga baixar a imagem consegue lê-la sem iniciar nada:

```text
docker run --rm notes-app grep -n "sk-proj" server.js
11:const OPENAI_API_KEY = 'sk-proj-REDACTED-this-was-pasted-into-the-file';
```

Sem app rodando, sem requisição de rede, sem acesso à sua conta: só a imagem e um comando. Ela também está no seu histórico do git desde o primeiro commit, o que sobrevive a qualquer remoção posterior da linha.

A correção é a linha que a ferramenta de IA deveria ter escrito desde o início, mais definir o valor onde a plataforma guarda as variáveis de ambiente:

```js
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
```

Depois dessa mudança, o mesmo grep não devolve nada, enquanto o app em execução continua informando a chave como configurada: ela está presente em tempo de execução e ausente do artefato.

Como medimos: `grep -c "sk-"` contra a cópia de `server.js` na própria imagem devolveu 0, e a resposta `/` do próprio app informou `keyConfigured: true` com o valor fornecido como variável de ambiente.

## Por que as notas sumiram, e por que não ao reiniciar?

Porque um deploy não reinicia o seu container: ele o substitui. Esta é a falha que mais custa tempo às pessoas, porque o teste óbvio passa.

O app gravava as notas num arquivo dentro do container. Três rodadas da mesma verificação, com uma nota salva:

| O que aconteceu | Notas restantes |
| --- | --- |
| `docker restart` | **1** — sobrevive |
| Container substituído a partir da mesma imagem | **0** — sumiu |
| Container substituído, com um volume montado | **1** — sobrevive |

Um restart mantém a camada gravável do próprio container, então o arquivo continua lá e o armazenamento parece persistente. A substituição descarta essa camada.

As plataformas substituem o container a cada deploy, a cada evento de escala e a cada migração de host, o que significa que os dados nunca estiveram seguros: o teste de restart simplesmente nunca fez a pergunta certa.

Como medimos: uma nota postada pelo app, depois o container reiniciado, depois removido e recriado a partir da mesma imagem, depois o par repetido com um volume nomeado montado no diretório do app.

## Onde os dados devem ficar, afinal?

Fora do container, em algo cujo trabalho é continuar existindo amanhã. Um volume é a resposta menor e funciona, mas amarra os dados a um host e deixa backups, migrações e acesso concorrente por sua conta.

A versão no repositório companion envia as notas para um backend gerenciado, de modo que o container não guarda estado nenhum:

```js
// Stack: Node 22 | File: server.js
const PARSE = 'https://parseapi.back4app.com';
const parseHeaders = {
  'X-Parse-Application-Id': process.env.PARSE_APP_ID,
  'X-Parse-REST-API-Key': process.env.PARSE_REST_KEY,
  'Content-Type': 'application/json',
};
```

Depois, o mesmo teste destrutivo que antes perdeu tudo:

```text
note posted            →  1 note
container replaced     →  1 note
```

O container ficou descartável, que é o que você queria dele desde o começo. Repare que as credenciais chegam do mesmo jeito que a API key chega agora: as duas correções são o mesmo hábito aplicado duas vezes.

## Quais erros dizem qual falha você tem?

Quase nenhum, e é por isso que este guia começa pelos sintomas. As quatro falhas produzem quase nenhuma saída de diagnóstico, e essa é a característica que as define.

| O que você vê | Qual falha |
| --- | --- |
| Container `Up`, requisições devolvem `HTTP 000` ou dão timeout | porta errada, ou vinculado a `127.0.0.1` |
| `npm error Missing script: "start"` no log do deploy | sem script start, arquivo de entrada não é `server.js` |
| Nada — funciona | a chave está na imagem, e vai continuar até alguém encontrá-la |
| Dados corretos depois de um restart, vazios depois de um deploy | estado no filesystem do container |

Só a segunda se anuncia. A primeira parece um problema de rede, a terceira parece sucesso e a quarta parece um bug de restart. Se o log do seu deploy está vazio e o seu app está inalcançável, comece pelo endereço de escuta antes de suspeitar da plataforma.

## Quando um container é o lugar errado para este app?

Quando o que você está colocando em deploy não contém servidor nenhum. Um app de página única sem nada por trás é uma pilha de arquivos estáticos, e embrulhá-lo em Node para servi-los compra um processo para supervisionar e nada mais: sirva os arquivos diretamente.

Também é o formato errado quando o trabalho é um agendamento, e não uma requisição. Um relatório noturno não precisa de algo escutando numa porta; precisa de algo que roda e encerra, e um container mantido aberto por um servidor web se encaixa mal nisso.

E se o seu app precisa de verdade de um disco local gravável (um conversor de imagens trabalhando com arquivos temporários grandes, digamos), um container serve, mas planeje para o disco estar vazio a cada início. Isso é uma restrição para projetar em torno, não um bug para corrigir depois.

## O que você acrescentaria ao deploy-failure lab em seguida?

Um endpoint de health check que falhe com honestidade. O `/healthz` do repositório devolve `{ "ok": true }` incondicionalmente, o mesmo otimismo que a plataforma mostrou ao chamar de `Up` o container quebrado; apontá-lo para o backend de que o app depende faria com que ele significasse alguma coisa.

Depois disso, uma verificação em tempo de build. Três dessas quatro falhas são visíveis no código-fonte antes de qualquer deploy: uma porta fixa no código, um script start ausente, uma string começando com `sk-`. Um script curto no CI que recuse o build as pegaria mais cedo do que qualquer log de deploy.
