Um Dockerfile é um arquivo pequeno com um raio de impacto desproporcional. Essas poucas dezenas de linhas decidem qual código, quais pacotes do sistema operacional e quais privilégios acabam rodando em produção, muitas vezes em milhares de instâncias de container. É por isso que a segurança em Dockerfile merece o mesmo cuidado que você dá ao código da aplicação: uma imagem base descuidada, um COPY . . perdido ou um segredo passado por ARG podem silenciosamente entregar a um atacante um ponto de apoio que nenhuma quantidade de ferramentas de runtime desfará por completo. Acertar o Dockerfile é o lugar mais barato e mais precoce para fortalecer um container.
A parte animadora é que um Dockerfile seguro é, em grande medida, uma questão de hábitos bem compreendidos, e não de ferramentas exóticas. Este mergulho profundo percorre as práticas que mais importam para o hardening de containers: escolher imagens base mínimas e confiáveis, rodar como um usuário sem privilégios, usar builds multi-stage para reduzir a superfície de ataque, manter segredos fora das layers, minimizar o que você instala, copiar apenas o necessário, fixar versões, adicionar um healthcheck, restringir o runtime e escanear cada image antes de distribuí-la. Faz parte do nosso guia mais amplo de Docker container security, que conecta essas boas práticas de Dockerfile no nível da image com as preocupações de registro e de runtime.
Comece por uma imagem base mínima e confiável
Cada linha do seu Dockerfile se apoia na imagem base, então a imagem base é o seu maior risco herdado. Uma image completa de ubuntu ou node traz um shell, um gerenciador de pacotes e dezenas de bibliotecas que você nunca vai chamar, mas que um atacante pode. Prefira uma variante mínima, uma tag -slim ou, melhor ainda, uma image distroless que contenha apenas o runtime da sua linguagem e suas dependências, sem shell e sem gerenciador de pacotes para abusar.
Igualmente importante: saiba exatamente o que você está baixando. Use imagens oficiais ou de publishers verificados e fixe-as por digest em vez de uma tag móvel. Uma tag como :latest ou até :20 pode apontar para um conteúdo diferente amanhã em relação a hoje, o que quebra silenciosamente a reprodutibilidade e permite que uma mudança upstream entre sem revisão.
# Fragile: the tag can move under you
FROM node:latest
# Better: a slim, digest-pinned base you can reproduce and audit
FROM node:20.11.1-slim@sha256:2b3f1c...
Fixar por digest significa que o build é reproduzível byte a byte e que qualquer mudança na imagem base se torna um commit deliberado e revisável, em vez de uma surpresa silenciosa.
Rode como um usuário non-root
Por padrão, os processos dentro de um container rodam como root. Se um atacante escapa da aplicação, root no container somado a um bug de kernel ou a uma má configuração pode se tornar root no host. A maioria das cargas de trabalho nunca precisa disso. Crie um usuário sem privilégios e mude para ele antes que o processo principal do container inicie.
# Create a dedicated, unprivileged user and own the app directory
RUN groupadd --system app && useradd --system --gid app --no-create-home app
WORKDIR /app
COPY --chown=app:app . .
USER app
USER app garante que o entrypoint rode sem privilégios. Combine isso com a remoção das capabilities do Linux de que você não precisa, para que até as permissões que o root normalmente teria sejam retiradas. As capabilities são aplicadas em runtime, mas o Dockerfile é onde você sinaliza e documenta a intenção, e onde um USER non-root faz o padrão de menor privilégio se sustentar.
Dois detalhes são fáceis de esquecer. Primeiro, coloque a instrução USER depois de qualquer etapa que genuinamente precise de direitos elevados, como instalar pacotes, para que essas etapas ainda funcionem enquanto o processo em execução permanece sem privilégios. Segundo, garanta que o usuário sem privilégios realmente seja dono dos arquivos e diretórios em que ele precisa escrever em runtime; o COPY --chown cuida dos arquivos que você copia, e um chown explícito cobre os diretórios em que a aplicação cria dados. Se você pular isso, o container inicia com o usuário correto, mas falha no momento em que tenta escrever, o que tende a empurrar as equipes de volta ao root por frustração.
Use builds multi-stage
Construir software exige compiladores, headers, pacotes de desenvolvimento e, às vezes, credenciais. Nada disso pertence à image que você faz o deploy. Um build multi-stage permite fazer o trabalho pesado em um estágio e copiar apenas o artefato final para um estágio final limpo e mínimo.
# Stage 1: build with the full toolchain
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/app ./cmd/app
# Stage 2: ship only the binary on a distroless runtime
FROM gcr.io/distroless/static-debian12@sha256:...
COPY --from=build /bin/app /bin/app
USER nonroot:nonroot
ENTRYPOINT ["/bin/app"]
A image de runtime não tem compilador, nem shell, nem cache de build, então a superfície de ataque encolhe drasticamente. Builds multi-stage também são a maneira mais limpa de garantir que ferramentas de build e arquivos intermediários nunca vazem para o que roda em produção.
Mantenha segredos totalmente fora da image
Esse é o erro que mais machuca, porque é invisível no container em execução, mas permanente na image. Segredos passados por ARG ou ENV, ou copiados como arquivos, são gravados nas layers da image, e essas layers vivem para sempre no histórico da image. Apagar um arquivo em uma layer posterior não o remove: qualquer pessoa que possa baixar a image pode rodar docker history ou descompactar as layers e lê-lo.
# Wrong: the token is now baked into a layer for good
ARG NPM_TOKEN
RUN npm ci
Em vez disso, use os secret mounts do BuildKit. O segredo fica disponível apenas durante aquela única instrução RUN e nunca é gravado em uma layer.
# syntax=docker/dockerfile:1
FROM node:20.11.1-slim@sha256:...
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci
Você então o fornece no momento do build com docker build --secret id=npm_token,src=./npm_token.txt. A credencial cumpre sua função e não deixa rastro na image final. Para segredos de runtime, injete-os pelo seu orquestrador ou por um gerenciador de segredos, em vez do Dockerfile, para que nada sensível jamais faça parte do artefato.
Minimize layers, pacotes e caches
Cada pacote que você instala é mais código que pode carregar uma vulnerabilidade, e cada cache remanescente é peso morto e superfície de ataque extra. Instale apenas o necessário, evite extras recomendados mas desnecessários e faça a limpeza no mesmo RUN, para que ela caia na mesma layer.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
--no-install-recommends impede que o apt puxe pacotes opcionais, e remover as listas de pacotes na mesma layer evita que elas inchem a image. Menos pacotes significam menos CVEs para triar e uma superfície menor para defender.
Copie apenas o necessário e use um .dockerignore
COPY . . é conveniente e perigoso. Ele varre alegremente todo o seu diretório de trabalho para dentro da image, incluindo o histórico .git, arquivos .env locais, credenciais e artefatos de CI. Copie caminhos específicos e reforce isso com um .dockerignore, para que arquivos sensíveis nunca entrem no contexto de build em primeiro lugar.
# Copy dependency manifests first, then only the source you need
COPY package*.json ./
RUN npm ci
COPY src ./src
# .dockerignore
.git
.env
node_modules
*.log
**/secrets/*
Um bom .dockerignore também acelera os builds ao manter o contexto pequeno, então ele se paga duas vezes.
Fixe versões de dependências e pacotes
A reprodutibilidade é uma propriedade de segurança. Se um build pode puxar uma versão diferente de uma dependência ou pacote do SO a cada execução, você não consegue raciocinar sobre o que de fato está na image, e uma release upstream comprometida pode entrar despercebida. Fixe as dependências da aplicação por meio de um lockfile versionado e instale a partir dele, e fixe os pacotes do SO em versões conhecidas onde sua distribuição base suportar.
# Install from the committed lockfile, not "whatever is latest"
COPY package.json package-lock.json ./
RUN npm ci
npm ci (e seus equivalentes em outros ecossistemas) instala exatamente o que o lockfile especifica e falha se os dois divergirem, que é precisamente o comportamento que você quer em um pipeline de build.
Adicione um healthcheck e restrinja o runtime
Um HEALTHCHECK dá ao seu orquestrador um sinal confiável de quando um container está genuinamente atendendo, e não apenas iniciado, o que o ajuda a substituir prontamente instâncias comprometidas ou travadas.
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD ["/bin/app", "healthcheck"]
O Dockerfile também prepara o terreno para um runtime restrito. Projete a image de modo que ela possa rodar com um sistema de arquivos raiz somente leitura, escrevendo apenas em volumes explicitamente montados ou em tmpfs, e de modo que não precise de binários setuid ou setgid. Binários setuid e setgid permitem que um programa rode com os privilégios do dono do arquivo, independentemente de quem o invoca, que é exatamente o tipo de caminho latente de escalada que você não quer parado em um container. Você pode encontrar e remover esses bits durante o build e, depois, impor o sistema de arquivos raiz somente leitura e a remoção de capabilities ao rodar o container.
# Remove setuid/setgid bits from binaries you do not need them on
RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + || true
Uma image construída para rodar em modo somente leitura e sem privilégios, sem caminhos de escalada por setuid, dá a um atacante muito menos espaço para persistir, adulterar o sistema de arquivos ou escalar privilégios. O Dockerfile não pode acionar a flag de somente leitura sozinho, isso é uma configuração de runtime, mas construir a image para tolerá-la é o que torna indolor ativá-la depois.
Verifique, escaneie e rotule no CI
A salvaguarda final é a automação. Um Dockerfile que parecia adequado no trimestre passado pode estar repleto de CVEs recém-divulgadas hoje, porque vulnerabilidades são encontradas em dependências que você já distribuiu. Escaneie cada image em cada build no CI, faça o pipeline falhar diante de achados de alta severidade e adicione labels de proveniência para que você possa rastrear qualquer image até sua origem.
LABEL org.opencontainers.image.source="https://example.com/org/repo" \
org.opencontainers.image.revision="$GIT_SHA"
O escaneamento automatizado é o que transforma todas as práticas acima de boas intenções em um padrão obrigatório. Ele pega a imagem base vulnerável que ninguém notou, o segredo que escorregou para uma layer e o pacote que adquiriu uma CVE crítica desde a última release. Padrões inseguros e falhas do tipo injeção continuam sendo um tema recorrente no OWASP Top 10 (2025), e as imagens de container são um dos lugares onde essas fraquezas se acumulam silenciosamente.
Como a Rainforest ajuda
A Rainforest traz suas imagens de container para o mesmo fluxo de segurança do resto do seu código. Nosso container security scanning analisa imagens em busca de imagens base vulneráveis e desatualizadas, segredos expostos embutidos em layers, configuração arriscada e padrões inseguros, e depois expõe os achados com a severidade e o contexto de que sua equipe precisa para agir. Como ele roda como parte da mais ampla plataforma de application security testing, o mesmo escaneamento dispara em cada build, então uma imagem arriscada é sinalizada antes de chegar a um registro, e não depois de um incidente.
O ganho são imagens em que você pode de fato confiar: bases mínimas e fixadas, non-root por padrão, sem segredos nas layers e um portão de CI que mantém cada build honesto. Quer ver isso contra suas próprias imagens? Agende uma demonstração e vamos percorrer tudo com a sua stack.