← Blog

Boas práticas de segurança em Dockerfile

Boas práticas de segurança em Dockerfile: fixe imagens base confiáveis, rode como non-root, use multi-stage, mantenha segredos fora e escaneie tudo no CI.

Bruno Baldo·5 de out. de 2026·Atualizado em 14 de set. de 2026·10 min de leitura·Revisado por Rainforest Technologies
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.

Perguntas frequentes

O que torna um Dockerfile inseguro?

Os culpados de sempre são uma imagem base inchada ou não confiável, rodar como root, segredos passados por ARG, ENV ou arquivos copiados (que persistem nas layers da image), um COPY . . amplo que varre o .git e o .env, imagens base e dependências não fixadas e pular o escaneamento de imagens no CI. Cada um deles ou amplia a superfície de ataque ou deixa dados sensíveis na image. Seguir as boas práticas de segurança em Dockerfile, bases mínimas e fixadas, um usuário non-root, builds multi-stage, segredos do BuildKit e escaneamento no CI, remove a maior parte desse risco.

Devo usar :latest ou fixar digests de imagem?

Fixe digests. Uma tag como :latest é um ponteiro móvel que pode resolver para conteúdos diferentes ao longo do tempo, o que quebra a reprodutibilidade e permite que mudanças upstream entrem no seu build sem revisão. Fixar por digest (image@sha256:...) torna os builds reproduzíveis byte a byte e transforma qualquer mudança na imagem base em um commit deliberado e revisável. Use uma tag de versão legível para humanos, se quiser, mas ancore-a a um digest.

Por que os containers não devem rodar como root?

Porque root dentro do container está a um bug de distância de root no host. Se um atacante compromete a aplicação, rodar como root lhe dá a alavancagem para explorar uma falha de kernel ou uma má configuração e escapar do container. A maioria das cargas de trabalho nunca precisa de root, então crie um USER dedicado e sem privilégios, use COPY --chown para lhe dar os arquivos de que ele precisa e remova as capabilities do Linux para impor o menor privilégio.

Como mantenho segredos fora de uma imagem Docker?

Nunca passe segredos por ARG, ENV ou arquivos de credenciais copiados: eles são gravados nas layers da image e permanecem legíveis no histórico da image mesmo que você os apague depois. Para segredos de build, use o --mount=type=secret do BuildKit, que expõe o valor apenas durante um único RUN e nunca o grava em uma layer. Para segredos de runtime, injete-os pelo seu orquestrador ou por um gerenciador de segredos, em vez da image. Adicione um .dockerignore para que arquivos de credenciais nunca entrem no contexto de build e escaneie as imagens no CI para pegar vazamentos.

O que é um build multi-stage?

Um build multi-stage usa mais de um FROM em um único Dockerfile. Um estágio de build inicial carrega toda a toolchain, compiladores, gerenciadores de pacotes, dependências de desenvolvimento, e faz o trabalho pesado; depois, um estágio final copia apenas o artefato acabado para uma image de runtime limpa e mínima. O resultado é distribuído sem ferramentas de build ou arquivos intermediários, então a superfície de ataque é muito menor e os segredos de build nunca chegam à produção.

Bruno Baldo

Escrito por

Bruno Baldo

CMO

Um pouco de marketing e um pouco de curiosidade e temos a receita pra criar um apaixonado por cyber!

Continue lendo