10 de out. de 2025·7 min de leitura

Implante uma app gerada por IA com Docker — builds seguros e repetíveis

Implante uma app gerada por IA com Docker com segurança: builds repetíveis, versões fixadas, gestão de segredos e verificações para evitar imagens que só funcionam no builder.

Implante uma app gerada por IA com Docker — builds seguros e repetíveis

Por que apps gerados por IA falham tanto ao serem implantados

"Funciona apenas no builder" significa que a app roda na máquina que construiu a imagem, mas falha assim que você executa essa mesma imagem na CI, staging ou produção. A build parece bem, mas o container depende de algo que na verdade não está dentro da imagem.

Apps gerados por IA batem mais nisso porque o código é montado a partir de padrões que assumem um ambiente amigo. Pode depender de ferramentas instaladas globalmente no computador do autor, de um banco de dados local rodando no host, ou de um ficheiro que nunca foi comitado. Às vezes o Dockerfile "passa" copiando demais do workspace, incluindo caches, artefatos compilados ou até segredos por acidente.

Você costuma ver os sintomas assim que tenta implantar em um ambiente limpo:

  • Builds passam localmente mas falham na CI com "command not found" ou bibliotecas do sistema faltando
  • A app inicia e depois cai porque uma variável de ambiente está ausente (ou um segredo foi embutido na imagem)
  • Autenticação funciona no localhost mas falha em staging por causa de URLs de callback ou configurações de cookies
  • Assets estáticos ou migrations estão faltando porque o passo de build nunca rodou no container
  • Comportamento aleatório de "às vezes funciona" causado por versões não fixadas e dependências que mudam

O objetivo é simples: uma imagem, mesmo comportamento, em qualquer lugar. Se o container precisa disso para rodar, isso deve ser declarado, instalado e configurado dentro dos passos de build e runtime, não emprestado da máquina do builder.

Escolha imagens base e fixe versões primeiro

Comece travando sobre o que seu container é construído. A maioria das falhas de "funcionou no meu laptop" acontece porque a imagem base ou runtime mudou por baixo dos seus pés.

O maior risco é usar :latest. É conveniente, mas é um alvo móvel. Uma pequena atualização da imagem base pode mudar OpenSSL, libc, Python, Node ou até o comportamento padrão do shell. Sua build pode passar hoje e falhar na próxima semana, ou pior, comportar-se diferente em produção.

Escolha uma imagem base estável (e evite mudanças silenciosas)

Escolha uma imagem base que você consiga manter estável por meses. Fixe-a a uma versão específica e, quando possível, a um digest. Fixar por digest garante os mesmos bytes da imagem toda vez, não apenas "Node 20" na data de hoje.

Também fixe a versão do runtime da linguagem. "Node 20" não é o mesmo que "Node 20.11.1". Até mudanças menores podem quebrar módulos nativos, criptografia ou scripts de build.

Decida a plataforma alvo cedo (amd64 vs arm64)

Seja explícito sobre onde a imagem vai rodar. Muitos builders usam Apple Silicon (arm64), enquanto muitos servidores rodam amd64. Dependências nativas podem compilar diferente, e alguns pacotes não fornecem binários arm64.

Exemplo: uma app Node instala uma biblioteca de processamento de imagem. No arm64 ela compila do código-fonte e passa. No amd64 ela baixa um binário pré-compilado com outra versão e trava em runtime.

Antes de escrever o resto do Dockerfile, defina isto:

  • Versão da imagem base (e digest se puder)
  • Versão do runtime (Node, Python, Java)
  • Plataforma alvo (amd64 ou arm64)

Se você herdou um repositório gerado por IA e as versões estão flutuando, comece fixando a imagem base e o runtime. Isso elimina uma categoria inteira de falhas de implantação "misteriosas".

Passo a passo: um Dockerfile que constrói igual toda vez

Imagens repetíveis vêm de escolhas chatas: fixar a tag da imagem base, confiar num lockfile e manter os passos de build previsíveis.

Aqui está um esqueleto simples de Dockerfile para uma app Node típica (API ou full-stack) que preserva cache efetivo e instala de forma determinística:

# Pin the exact base image version
FROM node:20.11.1-alpine3.19

WORKDIR /app

# Copy only dependency files first (better caching)
COPY package.json package-lock.json ./

# Deterministic install based on the lockfile
RUN npm ci --omit=dev

# Now copy the rest of the source
COPY . .

# Build only if your app has a build step
RUN npm run build

EXPOSE 3000

# Clear, explicit start command
CMD ["node", "server.js"]

# Add a simple healthcheck only if you can keep it stable
# HEALTHCHECK --interval=30s --timeout=3s CMD node -e "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

Três detalhes importam mais do que se espera:

  • Copie os ficheiros de dependência primeiro, instale e só então copie o resto. Assim você evita reinstalar pacotes sempre que muda um ficheiro de código.
  • Use o comando de instalação do lockfile (como npm ci) para obter as mesmas versões de dependência em cada build.
  • Mantenha o comando de start direto. Evite scripts "mágicos" que se comportam diferente entre ambientes.

Um caso comum de falha é uma app que roda localmente porque lê silenciosamente um .env e depende de ferramentas instaladas globalmente. No Docker, ambos faltam, então a build falha ou o container inicia e logo cai. Esse padrão força você a declarar o que precisa, que é a ideia principal.

Use builds multi-stage para evitar imagens de runtime inchadas

Builds multi-stage separam "build" de "run". Você compila a app em uma imagem (com ferramentas pesadas) e copia apenas o resultado final para uma segunda imagem mais enxuta que é a que realmente roda.

Isso importa com apps gerados por IA porque elas frequentemente puxam compiladores, CLIs e caches sem você notar. Se isso for parar em produção, a imagem fica grande, mais lenta para enviar e mais difícil de entender.

Stage de build vs stage de runtime (em linguagem simples)

No stage de build você instala ferramentas de compilação e dependências de desenvolvimento: compiladores TypeScript, bundlers e qualquer coisa usada só para criar o artefato final.

No stage de runtime você mantém apenas o que a app precisa para rodar. Isso torna a imagem final menor e mais previsível. Também evita que uma dependência "funcione" apenas porque uma ferramenta de build estava presente.

Uma pergunta útil: se a app já está construída, eu ainda preciso deste pacote? Se não, ele pertence ao stage de build.

O erro mais comum

As pessoas constroem com sucesso e depois esquecem de copiar algo para o stage de runtime. As peças faltantes são normalmente assets compilados (como dist/), templates/configs de runtime ou módulos instalados. Permissões também pegam: a app roda localmente e depois falha no container porque não consegue ler ou escrever num diretório.

Depois de construída a imagem, rode-a como se fosse produção. Confirme que ela consegue iniciar apenas com variáveis de ambiente e uma conexão real ao banco. Se precisar de mais alguma coisa, provavelmente ficou para trás no stage de build.

Torne instalações de dependência determinísticas

Instalações de dependência são onde as builds começam a divergir primeiro. Projetos gerados por IA frequentemente funcionam na máquina do criador porque ele acidentalmente instalou pacotes mais novos, reutilizou caches de build ou puxou uma branch Git em movimento.

Lockfiles são sua rede de segurança. Comite-os e faça o build do Docker usá-los de propósito. Para Node, isso normalmente significa package-lock.json com npm ci, ou pnpm-lock.yaml com uma instalação congelada. Para Python, use poetry.lock (Poetry) ou requirements totalmente pinados e evite latest.

Uma regra salva muita dor: nunca instale a partir de referências flutuantes como main, master ou uma tag não fixada. Se precisar puxar do Git, fixe a um SHA de commit específico.

Pacotes privados são outra armadilha. Não embuta tokens na imagem. Use segredos de build para que o build possa acessar registries privados sem deixar credenciais nas camadas. Após o passo de instalação, a imagem final não deve conter .npmrc, configuração do pip ou quaisquer ficheiros de autenticação.

Também torne scripts de build explícitos. Alguns projetos gerados dependem de comportamento postinstall oculto que baixa binários ou roda geração de código. Se algo precisa rodar, execute como um passo claro de build e falhe de maneira visível.

Lide com segredos com segurança (sem atrapalhar o desenvolvimento local)

Deployment prep done right
We set up clean builds, health checks, and safe defaults for production.

Se uma app funciona localmente mas falha, ou fica exposta, segredos muitas vezes são a razão. Uma imagem Docker é feita para ser compartilhada, cacheada e armazenada em registries. Qualquer coisa dentro dela pode vazar.

Nunca embuta valores sensíveis na imagem, nem "só para testes". Isso inclui chaves de API, segredos de cliente OAuth, senhas de banco, chaves de assinatura JWT e certificados privados.

Uma regra simples: passe segredos em runtime, não durante docker build. Valores em tempo de build podem ficar presos em camadas da imagem e logs, especialmente se um Dockerfile gerado usar ARG e depois fizer echo, escrever num ficheiro de config ou embutir em código frontend.

Em vez disso, deixe a sua plataforma de deploy injetar segredos quando o container iniciar (seu gerenciador de segredos, configurações de env vars ou um cofre encriptado). Mantenha o Dockerfile focado em instalar dependências e copiar código, não em ligar credenciais.

Para desenvolvimento local, mantenha a conveniência sem comitar segredos:

  • Use um ficheiro .env local e adicione-o ao .gitignore
  • Comite um .env.example com nomes de placeholders (sem valores reais)
  • Falhe rápido com um erro claro quando uma env var obrigatória estiver ausente

Exemplo: localmente você usa DATABASE_URL do .env. Em produção, você define o mesmo DATABASE_URL nas definições de segredo do host. Mesmo caminho de código, valores diferentes, nada sensível dentro da imagem.

Builds repetíveis: marcar e rastrear o que mudou

Trate cada build de imagem como um recibo datado. O maior erro é reusar uma tag como latest (ou mesmo v1) para conteúdos diferentes. É assim que você tem implantações de "funcionou ontem".

Uma tag deveria significar um conjunto exato de bits. Use tags que digam o que você enviou e que sejam difíceis de alterar silenciosamente.

Um padrão prático é uma versão de release para humanos (como v1.4.2) mais um SHA de commit para precisão (como sha-3f2c1a9). Se você usar uma tag de ambiente como prod, faça dela um ponteiro para uma versão imutável.

Para tornar builds auditáveis, registe as entradas que afetam a imagem final. Guarde isso num lugar que seu time realmente leia (um ficheiro BUILDINFO curto, notas de release ou labels na imagem):

  • Digest da imagem base (não apenas node:20, o digest exato)
  • Hash do lockfile (package-lock.json, yarn.lock, poetry.lock, etc.)
  • Comando de build e argumentos importantes (por exemplo NODE_ENV=production)
  • Versão de migration (se sua app altera o banco)

Um guia útil para decidir quando reconstruir:

  • Apenas mudança de código: rebuild da camada da app, deps inalteradas se o lockfile não mudou
  • Mudança no lockfile: rebuild das deps e da app, rode testes novamente
  • Mudança no digest da imagem base: rebuild completo e reteste
  • Mudança de segredo: rode rotação em deploy (não reconstrua a imagem)

Hardening básico de container para não especialistas em segurança

Stop builder-only builds
We diagnose hidden dependencies, missing assets, and config gaps in AI-made apps.

Se você consegue empacotar uma app num container, você também consegue deixar mais difícil de ser explorada. Você não precisa de skills avançadas de segurança. Alguns padrões cobrem a maioria dos problemas em imagens apressadas.

Comece evitando executar como root quando puder. Muitos Dockerfiles gerados rodam tudo como root porque "simplesmente funciona". Em produção, isso transforma um pequeno bug em um incidente maior. Crie um usuário, dê propriedade da pasta da app e rode o processo como esse usuário.

Permissões importam também. Quando um container falha ao escrever um ficheiro, o conserto rápido comum é chmod -R 777. Isso normalmente cria um problema maior. Decida quais pastas devem ser graváveis (logs, uploads, temp) e dê apenas essas permissões de escrita.

Se você mantém ferramentas de build na imagem final, também está dando mais ferramentas a um atacante. Builds multi-stage ajudam porque compiladores e package managers ficam no stage de build.

Armadilhas comuns que causam imagens que só funcionam no builder

Uma imagem que só funciona no builder roda bem para quem a construiu e então quebra na CI ou produção. Normalmente acontece porque o Dockerfile depende da sua configuração local.

Os culpados habituais:

  • Um .dockerignore que esconde algo que você realmente precisa. Às vezes se ignora dist/, prisma/, migrations/ ou até um lockfile.
  • Ferramentas globais acidentalmente requeridas. Se o build assume que tsc, vite, pnpm ou Poetry está instalado globalmente, pode "funcionar por acidente" numa máquina e falhar em todas as outras.
  • Módulos nativos se comportando diferente entre plataformas. Qualquer coisa que constrói código nativo pode quebrar quando o builder é macOS/Windows e a produção é Linux.
  • Env vars em tempo de build usadas como config de runtime. Embutir API_URL, configurações de auth ou feature flags na build pode fazer a imagem parecer bem em um ambiente e quebrada em outro.

Um reality check rápido: reconstrua sem cache e com um contexto limpo, então rode o container com apenas as env vars que você planeja definir em produção.

Verificações rápidas antes do deploy que você faz em 10 minutos

Antes de enviar, faça uma passada que imite produção. Essas checagens pegam as surpresas mais comuns.

  • Reconstrua do zero uma vez (cache desativado) para não depender de camadas antigas.
  • Inicie o container com apenas as env vars necessárias e confirme que falta de config falha de forma clara.
  • Rode sem bind mounts. A imagem deve conter tudo que precisa.
  • Verifique ordem de startup: migrations antes do processo web, e scripts de seed (se houver) são seguros.
  • Escaneie logs por config ausente, erros de permissão e falhas de conexão ao DB.

Uma falha típica: localmente a app parece bem porque você tinha chaves extras no .env, mais um bind mount que escondia artefatos de build faltantes. Em produção, ela inicia, tenta gravar uploads numa pasta que não existe, migrations não rodam, e você só vê um vago "500". Um build sem cache mais um run com env vars mínimas geralmente expõe isso em minutos.

Exemplo: do sucesso local para uma imagem Docker segura para produção

Harden a generated codebase
Refactor risky patterns and close common holes like exposed secrets and SQL injection.

Uma história comum é gerar uma pequena web app com uma ferramenta de IA, rodá-la bem no laptop e depois vê-la falhar num VPS ou serviço gerenciado. Localmente você tem a versão certa do Node, um .env preenchido e um cache aquecido. Em produção, o container inicia frio, sem ficheiros ocultos e sem configuração interativa.

Para diagnosticar rápido, compare versões de runtime primeiro. Se sua imagem usa node:latest ou python:3, você está aceitando mudanças silenciosas. Depois, cheque variáveis de ambiente ausentes: chaves de auth, URLs de banco e callbacks OAuth frequentemente existem na sua máquina mas não no ambiente de deploy. Por fim, confirme que o output de build existe. Muitos projetos dependem de um passo de build local, mas a imagem Docker só copia o código-fonte, então não há nada para servir.

Um caminho prático de correção normalmente é:

  • Fixar a imagem base e as versões do runtime.
  • Comitar e aplicar instalação a partir do lockfile.
  • Mover segredos para env vars em runtime (não embutidos na imagem, não copiados do .env).
  • Usar builds multi-stage para que a imagem de runtime contenha apenas artefatos e dependências de produção.

Sucesso significa a mesma tag de imagem rodando localmente, na CI e em produção sem passos de "só precisa definir este ficheiro".

Próximos passos se sua app gerada por IA ainda não implantar

Se você tentou o básico e ainda falha, pare de adivinhar e escreva uma definição simples de pronto para deploy. Mantenha curta:

  • Versões exatas de runtime (imagem base, runtime da linguagem, package manager)
  • Como segredos são fornecidos em runtime (e o que nunca deve ser embutido na imagem)
  • Um smoke test e um health check que você pode rodar após o deploy
  • Regras de tag de release (uma tag por build, ligada a um commit)
  • Onde checar os logs primeiro

Então decida se conserta o que você tem ou reconstrói do zero. Conserte a base atual quando o fluxo principal funcionar e as falhas forem principalmente de empacotamento e configuração. Reconstrua quando fluxos básicos continuarem quebrando, modelos de dados forem confusos ou cada mudança causar novos erros.

Se você está vendo falhas apenas em produção como autenticação quebrada, segredos expostos, arquitetura spaghetti ou problemas óbvios de segurança (incluindo riscos de SQL injection), geralmente é mais rápido buscar uma segunda opinião cedo. FixMyMess (fixmymess.ai) foca em diagnosticar e reparar codebases gerados por IA para que se comportem de forma consistente em produção, começando com uma auditoria de código gratuita para apontar o que realmente está quebrando.