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.

A falha de onde este guia parte: o container informa Up com a porta 8080 mapeada, uma requisição a ele devolve HTTP 000 e, dentro do container, o app escuta em 127.0.0.1 porta 3000 enquanto a plataforma passou PORT 8080

Tradução do original em inglês, testado em 9 de outubro de 2026. Código, números e datas são os do original. Read in English

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

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:

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

E toda requisição a ele falha:

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:

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.

No seu laptop, você é o loopback. Num container, você é a única coisa que não é.

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:

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

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:

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

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

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.

{
  "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:

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:

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 aconteceuNotas restantes
docker restart1 — sobrevive
Container substituído a partir da mesma imagem0 — sumiu
Container substituído, com um volume montado1 — 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:

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

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 timeoutporta errada, ou vinculado a 127.0.0.1
npm error Missing script: "start" no log do deploysem script start, arquivo de entrada não é server.js
Nada — funcionaa chave está na imagem, e vai continuar até alguém encontrá-la
Dados corretos depois de um restart, vazios depois de um deployestado 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.

Tagsdeploymentdockernodeexpressenvironment-variablestroubleshooting

Perguntas frequentes

Por que meu app roda localmente mas não depois do deploy?

Na maioria das vezes porque ele escuta no endereço errado. Uma porta fixa no código, vinculada a 127.0.0.1, funciona na sua máquina, onde você é o loopback, e fica inalcançável numa plataforma que roteia o tráfego para uma porta que ela escolheu. Em 9 de outubro de 2026, um container exatamente nesse estado informou Up com a porta mapeada enquanto toda requisição a ele devolvia HTTP 000, porque dentro dele o app escutava em 127.0.0.1:3000 e a plataforma tinha passado PORT=8080.

Em que porta um app Node deve escutar em produção?

Na que a plataforma entrega, em todas as interfaces: const PORT = process.env.PORT || 3000 e depois app.listen(PORT, '0.0.0.0'). O fallback mantém o desenvolvimento local funcionando. Vincular 0.0.0.0 em vez de 127.0.0.1 é a metade que as pessoas esquecem, e é a metade que torna o container alcançável de fora dele.

Preciso de um script start no package.json para fazer o deploy?

Depende do nome que você deu ao arquivo de entrada, o que é uma resposta pior do que parece. O npm fornece um script start padrão, node server.js, então um app cujo arquivo de entrada é server.js roda sem script start nenhum. Chame-o de app.js ou index.js e o mesmo package.json falha com npm error Missing script: "start". Escreva o script explicitamente e a pergunta nunca aparece.

Os dados em um container são perdidos ao reiniciar?

Não ao reiniciar; ao substituir, que é o que um deploy faz. Medido em 9 de outubro de 2026: uma nota gravada num arquivo dentro do container sobreviveu ao docker restart e sumiu quando o container foi substituído por um novo a partir da mesma imagem. As plataformas substituem o container a cada deploy, a cada evento de escala e a cada migração de host, então tudo o que é gravado no filesystem do container é temporário por design.

Onde as API keys devem ficar em um app em produção?

No ambiente, nunca no código-fonte. Uma chave colada num arquivo vai dentro da imagem: rodar grep na cópia do arquivo que está na própria imagem a imprime, sem precisar iniciar o container nem alcançar o app em execução. Leia-a com process.env.OPENAI_API_KEY e defina o valor nas configurações de ambiente da plataforma, onde ela não está no seu repositório nem na imagem.

Leitura complementar

Revisado e verificado por Back4app Engineering, Editorial review na Back4app

Testado em 9 de outubro de 2026 — Local: Docker 29.4.3 no macOS, arm64 · App: Node 22, Express 5 · Backend: Back4app, plano Free, USA East.

Mais da Back4app Engineering

Containers · tutorial

How to Deploy a Node.js App With Docker and a Database, Without Managing a Server [Full Repo]

Containers · tutorial

How to Host a Static Website on Back4app Containers With Docker and nginx [Full Repo]

Backend · error-guide

Why Does the API Return 101, 119, 142 or 209? Every Backend Error Code, Reproduced [Full Repo]