Conteúdo
Tutorial de segurança no Docker: 10 práticas para proteger contêineres
Os contêineres oferecem certo nível de isolamento, mas isso não garante segurança. Estas são proteções básicas que você deve aplicar do desenvolvimento à implantação, cada uma acompanhada de resultados de execução reais.
1. Não execute como root
$ docker run --rm alpine:3.23 id
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
$ docker run --rm -u 10001:10001 alpine:3.23 id
uid=10001 gid=10001 groups=10001
$ docker run --rm node:24-alpine id node
uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
- Por padrão, o usuário dentro do contêiner é root (uid=0). Se uma vulnerabilidade no programa for explorada, o invasor terá privilégios máximos no contêiner e, combinando isso com outras vulnerabilidades, poderá afetar o host.
-u 10001:10001define o usuário durante a execução, sem exigir alterações na imagem.- Muitas imagens oficiais já incluem um usuário comum, como
nodena imagemnode(uid 1000) enonrootnas imagens distroless.
A melhor prática é trocar de usuário diretamente no Dockerfile:
RUN adduser -D -u 10001 app # Alpine
# RUN useradd --system --uid 10001 app # Debian / Ubuntu
USER app
Veja exemplos práticos em aplicações Python e aplicações Node.js.
2. Use imagens base confiáveis e fixe a versão
- Dê preferência às Docker Official Images (por exemplo,
python,node,nginx) ou a imagens publicadas por organizações conhecidas. - Não use imagens de origem desconhecida, principalmente se o nome for parecido com o de uma imagem popular (por exemplo,
ngnix). - Fixe a versão, por exemplo,
node:24-alpineou, com mais precisão,node:24.21-alpine, em vez denodeoulatest. Para fixar completamente, adicione o digest:node:24-alpine@sha256:....
3. Deixe a imagem o menor possível
Cada pacote na imagem pode ter vulnerabilidades. Use slim, alpine, distroless ou builds em vários estágios e mantenha apenas o necessário para a execução. Quanto menos ferramentas desnecessárias (compiladores, curl, ssh), menos opções o invasor terá para explorá-las e mais limpos serão os resultados da varredura de vulnerabilidades.
4. Não inclua segredos na imagem
Teste prático: os segredos permanecem na imagem
O Dockerfile a seguir comete três erros comuns:
FROM alpine:3.23
ARG NPM_TOKEN
ENV DB_PASSWORD=super-secret
COPY .env /app/.env
RUN echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env
CMD ["sh"]
A última linha remove .env depois do uso, então parece estar tudo certo. Faça o build e confira:
$ docker build -q -f Dockerfile.bad --build-arg NPM_TOKEN=npm_abc123secret -t leaky .
sha256:c9523bb4d0484e3abf049d15dec57df3282ff8203574c03127a8dfc6ee37bf9b
$ docker run --rm leaky ls /app
$ docker inspect -f "{{.Config.Env}}" leaky
[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin DB_PASSWORD=super-secret]
$ docker history --no-trunc --format "{{.CreatedBy}}" leaky | head -n 4
CMD ["sh"]
RUN |1 NPM_TOKEN=npm_abc123secret /bin/sh -c echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env # buildkit
COPY .env /app/.env # buildkit
ENV DB_PASSWORD=super-secret
ls /appnão mostra nada; parece que.envfoi removido.- Mas
docker inspectmostra diretamente oDB_PASSWORD=super-secretdefinido porENV. docker historymostra oNPM_TOKEN=npm_abc123secretpassado durante o build: o valor deARGfica registrado na camada em que é usado.
E quanto ao .env? Uma imagem é formada por várias camadas sobrepostas. rm apenas marca o arquivo como “excluído” em uma nova camada; o conteúdo continua na camada de COPY. Exporte a imagem com docker save e localize .env entre as camadas:
$ docker save leaky -o leaky.tar && mkdir x && tar -xf leaky.tar -C x && for l in x/blobs/sha256/*; do tar -tzf $l 2>/dev/null | grep -q "app/.env" && tar -xzOf $l app/.env; done; rm -rf x leaky.tar
API_KEY=sk-live-1234567890abcdef
O conteúdo completo de .env foi extraído. Qualquer pessoa que consiga baixar essa imagem pode fazer o mesmo. Ao enviar uma imagem para um Registry, presuma que todos podem ver o conteúdo de cada camada.
Detecte problemas antecipadamente com docker build --check
docker build --check identifica ARG e ENV que parecem conter segredos com base nos nomes das variáveis:
$ docker build --check -f Dockerfile.bad . 2>&1 | sed -n "/Check complete/,\$p"
Check complete, 2 warnings have been found!
WARNING: SecretsUsedInArgOrEnv - https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
Do not use ARG or ENV instructions for sensitive data (ARG "NPM_TOKEN")
Dockerfile.bad:2
--------------------
1 | FROM alpine:3.23
2 | >>> ARG NPM_TOKEN
3 | ENV DB_PASSWORD=super-secret
4 | COPY .env /app/.env
--------------------
WARNING: SecretsUsedInArgOrEnv - https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
Do not use ARG or ENV instructions for sensitive data (ENV "DB_PASSWORD")
Dockerfile.bad:3
--------------------
1 | FROM alpine:3.23
2 | ARG NPM_TOKEN
3 | >>> ENV DB_PASSWORD=super-secret
4 | COPY .env /app/.env
5 | RUN echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env
--------------------
NPM_TOKEN e DB_PASSWORD foram sinalizados. Mas a verificação considera apenas os nomes de ARG e ENV; ela não detecta problemas como copiar arquivos secretos para a imagem com COPY .env. Por isso, .dockerignore continua sendo necessário.
Boas práticas
- Nunca passe senhas ou chaves de API para o Dockerfile usando
ENVouARG. - Use
.dockerignorepara excluir.env, chaves privadas e.git, evitando que sejam copiados para dentro da imagem porCOPY . .. - Passe os segredos necessários em tempo de execução usando variáveis de ambiente,
--env-fileou secrets do Compose. Consulte Variáveis de ambiente e arquivo .env. - Para segredos necessários durante o build (por exemplo, um token para instalar pacotes privados), use a montagem de secrets do BuildKit:
FROM alpine:3.23
RUN --mount=type=secret,id=npm_token \
test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)"
CMD ["sh"]
$ NPM_TOKEN=npm_abc123secret docker build --progress plain --no-cache --secret id=npm_token,env=NPM_TOKEN -f Dockerfile.good -t safe . 2>&1 | grep "token length"
#5 [stage-0 2/2] RUN --mount=type=secret,id=npm_token test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)"
#5 0.261 token length: 16
$ docker history --no-trunc --format "{{.CreatedBy}}" safe | head -n 2
CMD ["sh"]
RUN /bin/sh -c test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)" # buildkit
$ docker run --rm safe ls /run/secrets
ls: /run/secrets: No such file or directory
- Durante o build,
RUNconsegue ler o token de/run/secrets/npm_token(com 16 caracteres). - Em
docker history, aparece apenas o próprio comando, sem o valor do token. - A imagem também não contém
/run/secrets; o secret é montado somente durante a execução daqueleRUN.
5. Não exponha portas desnecessárias à internet
# Ruim: banco de dados aberto para o mundo todo
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:18
# Bom: apenas conexões locais são permitidas
docker run -d -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=secret postgres:18
No Linux, o Docker modifica as regras do iptables diretamente, e as portas abertas com -p ignoram as configurações de firewalls como ufw e firewalld: mesmo que o ufw não permita a porta 5432, conexões externas ainda podem chegar. A opção mais segura é não usar -p nos serviços internos, como bancos de dados e caches, permitindo conexões apenas de outros contêineres pela rede do Docker. Por exemplo, em Nginx como proxy reverso, somente o Nginx tem ports.
6. Restrinja as permissões do contêiner
Sistema de arquivos somente para leitura
$ docker run --rm --read-only alpine:3.23 touch /tmp/x
touch: /tmp/x: Read-only file system
$ docker run --rm --read-only --tmpfs /tmp alpine:3.23 sh -c "touch /tmp/x && echo ok"
ok
--read-only torna o sistema de arquivos do contêiner somente para leitura, impedindo que um invasor modifique o programa ou grave arquivos maliciosos. Para os locais em que o programa precisa gravar (por exemplo, /tmp), monte separadamente uma área temporária na memória com --tmpfs ou monte um Volume.
Remova Linux capabilities
No Linux, as permissões de root são divididas em várias capabilities, como CHOWN (alterar o proprietário de um arquivo) e NET_BIND_SERVICE (vincular portas abaixo de 1024). Por padrão, o Docker concede algumas capabilities ao contêiner:
$ docker run --rm alpine:3.23 chown nobody /etc/hostname && echo "chown ok"
chown ok
$ docker run --rm --cap-drop ALL alpine:3.23 chown nobody /etc/hostname
chown: /etc/hostname: Operation not permitted
$ docker run --rm --cap-drop ALL --cap-add CHOWN alpine:3.23 chown nobody /etc/hostname && echo "chown ok"
chown ok
- Por padrão,
chownpode ser executado. - Depois que
--cap-drop ALLremove todas as capabilities, até o root recebeOperation not permitted. - Use
--cap-addpara adicionar de volta somente o que for necessário.
A maioria dos aplicativos web não precisa de nenhuma capability. Você pode usar --cap-drop ALL diretamente.
Impedir a elevação de privilégios
$ docker run --rm alpine:3.23 grep NoNewPrivs /proc/self/status
NoNewPrivs: 0
$ docker run --rm --security-opt no-new-privileges alpine:3.23 grep NoNewPrivs /proc/self/status
NoNewPrivs: 1
NoNewPrivs: 1 indica que os processos no contêiner não podem obter privilégios maiores por meio de programas setuid (como sudo e su). Também é recomendável ativar essa opção ao executar o contêiner como um usuário comum.
Juntando tudo:
docker run -d \
--user 10001:10001 \
--read-only --tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges \
myapp
Em Compose:
services:
app:
image: myapp
user: "10001:10001"
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
Nunca use --privileged sem necessidade: ele concede todas as capabilities ao contêiner e permite acesso a todos os dispositivos do host, praticamente desativando todo o isolamento.
7. Limite os recursos
$ docker run --rm --pids-limit 50 alpine:3.23 sh -c "for i in \$(seq 100); do sleep 5 & done; wait" 2>&1 | sort | uniq -c
1 sh: can't fork: Resource temporarily unavailable
$ docker run -d --name limited --memory 512m --cpus 1 --pids-limit 200 nginx:1.30-alpine
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
2edbff0c81d73b83f0055b781d8d8ad20661b2b64ef1dfd5a6def334ef02723c
$ docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}" limited
NAME CPU % MEM USAGE / LIMIT PIDS
limited 0.00% 3.406MiB / 512MiB 2
$ docker inspect -f "Memory={{.HostConfig.Memory}} NanoCpus={{.HostConfig.NanoCpus}} PidsLimit={{.HostConfig.PidsLimit}}" limited
Memory=536870912 NanoCpus=1000000000 PidsLimit=200
$ docker rm -f limited
limited
- Com
--pids-limit 50, quando o contêiner tenta criar 100 processos, o limite é excedido e aparececan't fork: Resource temporarily unavailable. Isso impede que uma fork bomb derrube o host inteiro. --memory 512m --cpus 1limita a memória e a CPU. OLIMITexibido pordocker statsé 512MiB. Se um processo ultrapassar o limite de memória, o sistema o encerra (OOM), em vez de deixar a memória do host inteiro se esgotar.- A mensagem
WARNING: Your kernel does not support swap limit capabilitiesindica que o kernel do host não tem a limitação de swap ativada. O limite de memória continua valendo, mas não é possível limitar também o swap; você pode ignorar esse aviso.
Em Compose:
services:
app:
image: myapp
mem_limit: 512m
cpus: 1
pids_limit: 200
8. Proteja o socket do Docker
/var/run/docker.sock é a interface de controle do Docker. Quem pode acessá-lo pode criar qualquer contêiner, inclusive montando o diretório raiz do host. Na prática, montamos / do host no contêiner:
$ docker run --rm -v /:/host alpine:3.23 sh -c "head -n 2 /host/etc/os-release; ls /host/var/lib/docker | head -n 5"
NAME="Alpine Linux"
ID=alpine
buildkit
containerd
containers
engine-id
image
O contêiner leu diretamente /etc/os-release e /var/lib/docker do host Docker (o ambiente de teste deste tutorial usa Alpine Linux). O mesmo método também permite ler e gravar em qualquer arquivo, como /etc/shadow e chaves SSH. Portanto:
- Poder executar comandos
dockerequivale a ter privilégios de root no host. Adicionar um usuário ao grupodockeré o mesmo que conceder a ele acesso root. - Não monte
-v /var/run/docker.sock:/var/run/docker.sockem um contêiner sem necessidade, a menos que você confie totalmente naquela imagem (por exemplo, ferramentas que precisam desse acesso, como Traefik, Portainer e Trivy). - Não exponha a porta TCP do daemon do Docker (2375) na rede: isso equivale a expor os privilégios de root do host.
- Se precisar de um isolamento mais forte, pesquise o modo rootless do Docker.
9. Analise vulnerabilidades em imagens
A ferramenta de código aberto Trivy pode analisar vulnerabilidades conhecidas nos pacotes do sistema operacional e da linguagem de programação presentes em uma imagem. Execute-a em um contêiner, sem precisar instalá-la:
$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v trivy-cache:/root/.cache aquasec/trivy:latest image --quiet --scanners vuln --severity HIGH,CRITICAL nginx:1.30-alpine
Report Summary
┌───────────────────────────────────┬────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├───────────────────────────────────┼────────┼─────────────────┤
│ nginx:1.30-alpine (alpine 3.24.2) │ alpine │ 3 │
└───────────────────────────────────┴────────┴─────────────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)
nginx:1.30-alpine (alpine 3.24.2)
=================================
Total: 3 (HIGH: 3, CRITICAL: 0)
┌──────────┬─────────────────┬──────────┬────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
├──────────┼─────────────────┼──────────┼────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ libexpat │ CVE-2026-93990 │ HIGH │ fixed │ 2.8.4-r0 │ 2.8.5-r0 │ expat: Expat: XML Injection via Malformed UTF-16 Input │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-93990 │
├──────────┼─────────────────┤ │ ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ pcre2 │ CVE-2026-103111 │ │ │ 10.48-r0 │ 10.49-r0 │ pcre2: pcre2: Out-of-bounds write via crafted regular │
│ │ │ │ │ │ │ expression │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-103111 │
├──────────┼─────────────────┤ │ ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ tiff │ CVE-2026-4775 │ │ │ 4.7.1-r0 │ 4.7.2-r0 │ libtiff: libtiff: Arbitrary code execution or denial of │
│ │ │ │ │ │ │ service via signed integer... │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-4775 │
└──────────┴─────────────────┴──────────┴────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────┘
--severity HIGH,CRITICALexibe apenas vulnerabilidades de alta gravidade ou superior;--quietoculta o progresso do download do banco de dados de vulnerabilidades.- Montar
docker.sockpermite que o Trivy leia as imagens locais; o Volumetrivy-cacheguarda o banco de dados de vulnerabilidades, então não é preciso baixá-lo novamente na próxima análise. - O resultado mostra 3 vulnerabilidades de nível HIGH na Alpine base de
nginx:1.30-alpine. Todas têmStatuscomofixed, e o relatório lista aFixed Version: isso significa que a Alpine já publicou as correções, mas esta imagem ainda não as incluía quando foi construída.
Os resultados da análise mudam com o tempo: novas vulnerabilidades são descobertas todos os dias, e as imagens são atualizadas continuamente. É provável que os resultados sejam diferentes quando você executar a análise.
O Docker Desktop e as versões recentes da CLI também incluem o Docker Scout (docker scout quickview, docker scout cves), que você pode usar depois de entrar na sua conta Docker. Recomendamos automatizar a análise no fluxo de CI e impedir a implantação quando forem encontradas vulnerabilidades graves.
10. Atualizações regulares
As imagens base continuam recebendo correções de vulnerabilidades. Dando continuidade ao resultado da verificação acima, atualize os pacotes no Dockerfile e faça uma nova verificação:
$ cat Dockerfile
FROM nginx:1.30-alpine
RUN apk upgrade --no-cache
$ docker build -q -t nginx-patched .
sha256:c88ea56e7062e7703dccc5eb2fedea74c333f33982d9735753056a50858646f2
$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v trivy-cache:/root/.cache aquasec/trivy:latest image --quiet --scanners vuln --severity HIGH,CRITICAL nginx-patched
Report Summary
┌───────────────────────────────┬────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├───────────────────────────────┼────────┼─────────────────┤
│ nginx-patched (alpine 3.24.2) │ alpine │ 0 │
└───────────────────────────────┴────────┴─────────────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)
Depois que apk upgrade atualiza os pacotes vulneráveis para versões corrigidas, o número de vulnerabilidades HIGH e CRITICAL passa a ser 0.
Na prática, é melhor reconstruir a imagem periodicamente com docker build --pull (--pull baixa primeiro a versão mais recente da imagem base) e fazer um novo deploy. Você pode usar ferramentas como Dependabot e Renovate para receber alertas automáticos sobre novas versões das imagens base. Uma imagem construída uma única vez e deixada sem atualização por vários anos só acumula cada vez mais vulnerabilidades.
Lista de verificação
- O Dockerfile define
USERe não executa como root - A origem da imagem base é confiável e a versão está fixada
- Use uma imagem enxuta ou um build em vários estágios
- Não há senhas, chaves nem
.envna imagem (verifiquedocker historyedocker inspect) - Use
RUN --mount=type=secretpara segredos durante o build - Portas de serviços internos, como bancos de dados, não estão expostas externamente
- Quando apropriado, use
--read-only,--cap-drop ALLeno-new-privileges - Não use
--privilegednem montedocker.socksem necessidade - Defina limites de memória, CPU e PID
- Faça a verificação de vulnerabilidades das imagens no CI
- Reconstrua periodicamente com
--pullpara obter atualizações de segurança
Resumo
- Adicione
USERao Dockerfile para executar como um usuário diferente de root. ARG,ENVe arquivos excluídos continuam na imagem; use--secretpara segredos durante o build.- Não publique serviços que não precisam de acesso externo com
-p;-pignora o ufw. - Ter acesso a
docker.sockequivale a ser root no host. - Faça verificações regulares e reconstrua com
--pull.
Exercícios
Exercício 1: use docker history --no-trunc para verificar se há alguma senha ou token na imagem que você construiu.
Dica
Confira o conteúdo dos comandos ARG, ENV e RUN na coluna CREATED BY, além de docker inspect -f "{{json .Config.Env}}" imagem. Se encontrar algo, passe a usar --secret ou variáveis de ambiente em tempo de execução e revogue as chaves que foram expostas.
Perguntas frequentes
O root dentro do contêiner é o mesmo root do host?
No modo geral, é o mesmo uid, mas fica limitado por namespaces, capabilities e seccomp. Para que não seja o root do host, use o [modo Rootless](/pt/docker/rootless-podman/).
O que faço se a varredura da imagem encontrar muitas vulnerabilidades?
Comece pelas vulnerabilidades de alto risco com Fixed Version: reconstrua a imagem (docker build --pull) ou atualize os pacotes. Para as que não têm versão corrigida, avalie se você realmente vai usar o pacote.
Qual é a diferença entre Docker Scout e Trivy?
Ambos são ferramentas de varredura de vulnerabilidades. O Scout é integrado ao Docker Desktop e exige login; o Trivy é uma ferramenta de código aberto que pode ser executada em um contêiner.
Atualizado 14 de outubro de 2026