Pular para o conteúdo

Dockerfile Linter · Docker

Analise um Dockerfile antes que ele chegue ao review.

Dezessete regras sobre um parse real do arquivo: o apt-get update que vai servir um índice desatualizado, o segredo escrito no histórico da sua imagem, a forma exec que o Docker rodou caladamente como string de shell. Cada achado nomeia uma linha e carrega a correção — e nada do que você colar sai da aba.

Roda no seu navegador — nada do que você cola sai desta página. Como comprovamos isso

Roda no seu navegador 17 regras, DF001–DF017 Sem cadastro Atualizado em 31 de jul. de 2026

Playground do Dockerfile Linter

Examples

A build stage is one FROM and every instruction under it; a multi-stage file has several, and only the last one ships. The layer cache reuses an instruction's result until that instruction — or anything it copied — changes.

Dockerfile input

Results update as you type — press Enter to run now.

Press Esc to release keyboard focus from the editor; /Ctrl + Enter lints and leaves the editor. Tap a Line badge to jump to that line. Nothing you paste is uploaded.

Findings

Paste a Dockerfile above — or tap an example — to see line-numbered findings here, each with the reason it matters and the fix.

A Lacuna

Um Dockerfile que builda não é um Dockerfile correto.

Todo erro do catálogo abaixo builda sem reclamar. FROM node builda. apt-get update na própria camada builda — e então instala de um índice que está em cache desde o trimestre passado. CMD ['nginx'] builda, e o Docker roda caladamente /bin/sh -c "['nginx']" em vez do processo que você queria como PID 1. Um build verde não é evidência; é a ausência de evidência.

Peça o Dockerfile a um assistente e você herda a mesma classe de bug com mais confiança em cima. Dockerfiles gerados são fluentes e plausíveis: passam no review porque se parecem com todos os outros, e carregam defeitos no formato de DF007 e DF014, para os quais nenhuma falha de build vai apontar. Checar a afirmação leva segundos; o difícil é saber o que checar.

O hadolint é a resposta certa em CI, e isto não o substitui. Isto cobre o caso que ele não cobre: um arquivo que você quer verificar agora, pelo navegador, em uma máquina onde você não pode instalar um binário, sem mandar um Dockerfile cheio de hosts de registries internos e argumentos de build para o servidor de outra pessoa. Dezessete regras, uma correção em cada achado e uma lista explícita do que fica em silêncio — porque um linter que você não consegue auditar é um linter em que você precisa confiar às cegas.

Já tem um contêiner rodando? O Docker Run para Compose transforma o comando em um arquivo de serviço, e o Env Example Checker acha as variáveis que sua imagem espera e o seu ambiente nunca define.

O Pipeline

Como funciona.

Quatro passos, todos dentro da sua aba, refeitos enquanto você digita.

  1. Parsear como o BuildKit.

    Continuações, linhas de comentário inteiras dentro delas, corpos de heredoc, a forma exec JSON, os estágios e a tabela de ARG antes do primeiro FROM. Uma diretiva `# escape=` realmente troca o caractere de continuação.

  2. Analisar o parse, não o texto.

    Dezessete regras leem instruções e estágios. As regras de shell percorrem uma varredura de cada RUN que respeita aspas, então um pipe dentro de uma string não é um pipe e o corpo de um heredoc continua sendo shell.

  3. Reportar uma linha e uma correção.

    Cada achado nomeia a linha FÍSICA — a linha 4 de um RUN dobrado, não a linha em que o RUN começou — e carrega a mudança a fazer, pronta para colar em um review.

  4. Dizer o que ele não verificou.

    As regras que ele deliberadamente não roda estão nesta página, com o motivo de cada uma. Um linter que você não consegue auditar é um linter em que você precisa confiar às cegas.

Referência

O catálogo de regras.

Todas as dezessete regras: três erros, doze avisos e duas notas. Um erro significa que o Docker rejeita o arquivo ou roda caladamente outra coisa; um aviso significa que builda e está errado; uma nota é algo que vale saber. Cada achado no playground linka para a sua regra aqui.

Regra Severidade O que ela pega
DF001 erro A primeira instrução precisa ser FROM
DF002 aviso Fixe a imagem base em uma tag
DF003 erro COPY --from precisa nomear um estágio anterior
DF004 aviso Use COPY para arquivos locais, não ADD
DF005 aviso Verifique um ADD remoto
DF006 aviso WORKDIR deveria ser absoluto
DF007 aviso Update e instalação no mesmo RUN
DF008 aviso Limpe o cache de pacotes na mesma camada
DF009 aviso Não grave segredos em ENV nem em ARG
DF010 aviso Não rode o estágio final como root
DF011 aviso Use WORKDIR em vez de cd
DF012 aviso Não canalize um download para um shell
DF013 aviso Um CMD e um ENTRYPOINT por estágio
DF014 erro A forma exec precisa ser JSON válido
DF015 aviso Tire o sudo dos passos de build
DF016 nota MAINTAINER está obsoleto
DF017 nota Copie o manifesto antes de instalar dependências
DF001 erro

A primeira instrução precisa ser FROM

Só ARG (e comentários) podem vir antes do FROM. Qualquer outra coisa não tem imagem sobre a qual agir, e o Docker recusa o build. Um arquivo sem nenhum FROM cai na mesma regra.

Em vez de

DOCKERFILE
RUN apt-get update
FROM debian:bookworm-slim

Escreva

DOCKERFILE
FROM debian:bookworm-slim
RUN apt-get update
DF002 aviso

Fixe a imagem base em uma tag

Uma referência sem tag significa :latest, e :latest se move. Fixações por digest, scratch, referências a um estágio anterior e tags que vêm de um argumento de build não resolvível ficam de fora.

Em vez de

DOCKERFILE
FROM node
FROM node:latest

Escreva

DOCKERFILE
FROM node:22-bookworm-slim
DF003 erro

COPY --from precisa nomear um estágio anterior

Um estágio só pode copiar de outro definido acima dele. Um índice numérico igual ou posterior ao estágio atual, e um nome solto que não corresponde a nenhum estágio, quebram o build com “invalid from flag value”. Tudo que carrega registry, tag ou digest é tratado como imagem externa e fica de fora.

Em vez de

DOCKERFILE
FROM alpine:3.20
COPY --from=builder /out /srv

Escreva

DOCKERFILE
FROM golang:1.23-alpine AS builder
FROM alpine:3.20
COPY --from=builder /out /srv
DF004 aviso

Use COPY para arquivos locais, não ADD

O ADD extrai arquivos tar locais automaticamente e pode baixar URLs, o que faz uma cópia simples se comportar de formas que a linha não diz. Extrair um arquivo é o único trabalho pelo qual vale manter o ADD, então um ADD cujas origens são todas tarballs fica em silêncio.

Em vez de

DOCKERFILE
ADD entrypoint.sh /entrypoint.sh

Escreva

DOCKERFILE
COPY entrypoint.sh /entrypoint.sh
DF005 aviso

Verifique um ADD remoto

Um ADD remoto grava numa camada o que o servidor devolver, e nada no build percebe quando isso muda. O --checksum= do BuildKit satisfaz a regra; baixar e verificar em um mesmo RUN também.

Em vez de

DOCKERFILE
ADD https://example.com/tool.tgz /tmp/tool.tgz

Escreva

DOCKERFILE
RUN curl -fsSL https://example.com/tool.tgz -o /tmp/tool.tgz \
 && echo "<sha256sum>  /tmp/tool.tgz" | sha256sum -c -
DF006 aviso

WORKDIR deveria ser absoluto

Um WORKDIR relativo é resolvido contra o WORKDIR anterior, então o diretório que ele seleciona depende das linhas acima — e muda no momento em que elas são reordenadas. Caminhos que começam com uma variável ficam de fora.

Em vez de

DOCKERFILE
WORKDIR app

Escreva

DOCKERFILE
WORKDIR /app
DF007 aviso

Update e instalação no mesmo RUN

Cada RUN é a própria camada. Uma camada de update em cache mais uma camada de instalação reconstruída significa instalar de um índice de pacotes que pode ter meses. Cobre apt-get, apt e apk.

Em vez de

DOCKERFILE
RUN apt-get update
RUN apt-get install -y curl

Escreva

DOCKERFILE
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*
DF008 aviso

Limpe o cache de pacotes na mesma camada

O índice de pacotes é commitado junto com a camada, então apagá-lo em um RUN posterior não recupera nada. Satisfeito por rm -rf /var/lib/apt/lists/* no apt, --no-cache no apk e dnf/yum clean all — e totalmente suprimido quando o RUN carrega um --mount=type=cache, porque aí o cache nunca entra na imagem.

Em vez de

DOCKERFILE
RUN apt-get update && apt-get install -y curl

Escreva

DOCKERFILE
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*
DF009 aviso

Não grave segredos em ENV nem em ARG

Todo valor de ENV e ARG fica guardado no histórico da imagem e é legível com docker history por qualquer um que possa dar pull. Sobrescrevê-lo em uma camada posterior não o remove. Só são apontados nomes que parecem uma credencial E carregam um valor não vazio.

Em vez de

DOCKERFILE
ARG NPM_TOKEN=npm_liveTokenValue
ENV DB_PASSWORD=hunter2

Escreva

DOCKERFILE
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci
DF010 aviso

Não rode o estágio final como root

Sem USER o processo roda como uid 0. Só o estágio FINAL é verificado — estágios de build legitimamente precisam de root — e dentro dele só o ÚLTIMO USER conta, porque é esse que o contêiner assume.

Em vez de

DOCKERFILE
FROM node:22-bookworm-slim
COPY . /app
CMD ["node", "/app/server.js"]

Escreva

DOCKERFILE
FROM node:22-bookworm-slim
COPY --chown=node:node . /app
USER node
CMD ["node", "/app/server.js"]
DF011 aviso

Use WORKDIR em vez de cd

Um cd só dura o RUN em que está escrito. A instrução seguinte volta a começar no WORKDIR do estágio, e essa é uma causa comum de “no such file or directory” no COPY ou no CMD que vem depois.

Em vez de

DOCKERFILE
RUN cd /src && make

Escreva

DOCKERFILE
WORKDIR /src
RUN make
DF012 aviso

Não canalize um download para um shell

curl … | sh executa o que o servidor devolver, sem assinatura e sem checksum. Detectado tanto dentro de linhas RUN dobradas quanto em corpos de heredoc, e reportado na linha em que o download está escrito.

Em vez de

DOCKERFILE
RUN curl -fsSL https://get.example.com/install.sh | sh

Escreva

DOCKERFILE
RUN curl -fsSL https://get.example.com/install.sh -o /tmp/install.sh \
 && echo "<sha256sum>  /tmp/install.sh" | sha256sum -c - \
 && sh /tmp/install.sh
DF013 aviso

Um CMD e um ENTRYPOINT por estágio

O Docker mantém o último de cada estágio e descarta o resto em silêncio. A contagem é por estágio, então um estágio de build com o próprio CMD está tudo bem, e instruções envolvidas em ONBUILD não contam.

Em vez de

DOCKERFILE
CMD ["node", "server.js"]
CMD ["node", "worker.js"]

Escreva

DOCKERFILE
CMD ["node", "server.js"]
# o worker roda como o próprio contêiner, não como um segundo CMD
DF014 erro

A forma exec precisa ser JSON válido

O Docker só lê o argumento como array em forma exec quando ele é parseado como JSON — aspas duplas, sem vírgula final. Caso contrário ele executa tudo em silêncio como forma shell via /bin/sh -c, colchetes inclusive.

Em vez de

DOCKERFILE
CMD ['nginx', '-g', 'daemon off;']

Escreva

DOCKERFILE
CMD ["nginx", "-g", "daemon off;"]
DF015 aviso

Tire o sudo dos passos de build

Um passo de build já roda como o USER do estágio — root, a menos que você tenha mudado — sem TTY, e a maioria das imagens base não traz sudo. Troque de usuário explicitamente em vez disso.

Em vez de

DOCKERFILE
RUN sudo apt-get install -y curl

Escreva

DOCKERFILE
USER root
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*
USER app
DF016 nota

MAINTAINER está obsoleto

Obsoleto desde o Docker 1.13, em 2017. Ainda builda, mas nada o lê — o label OCI é o que registries e scanners exibem.

Em vez de

DOCKERFILE
MAINTAINER ops@example.com

Escreva

DOCKERFILE
LABEL org.opencontainers.image.authors="ops@example.com"
DF017 nota

Copie o manifesto antes de instalar dependências

Um COPY . mais acima no estágio do que a instalação de dependências significa que qualquer mudança só de código-fonte invalida a camada de instalação e reinstala tudo. Reconhece npm, yarn, pnpm, pip -r, bundler, composer, go mod download e cargo. Combine a reordenação com um .dockerignore para que node_modules e .git nem entrem no contexto.

Em vez de

DOCKERFILE
COPY . /app
RUN npm ci

Escreva

DOCKERFILE
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

O Limite

O que ele deliberadamente não aponta.

Cada um desses itens foi considerado e descartado, e a mesma lista está em um comentário no topo do motor. Um silêncio que você pode ler vale mais do que uma regra que você aprende a ignorar.

Fixar versões além da tag

apt-get install curl sem versão e pip install django sem == são ambos legítimos. O DF002 pede uma tag; nunca exige um digest nem um conjunto de pacotes fixado.

EXPOSE

Por si só não publica nada. A ausência não quebra nada e um a mais não custa nada — pura opinião.

HEALTHCHECK

Ignorado por completo pelo Kubernetes, que é onde este público roda suas imagens.

Caches de download do pip e do npm

Reais, mas pequenos, e as flags mudam entre versões das ferramentas. O DF008 cobre os gerenciadores de pacotes do sistema, onde estão os megabytes.

update; install unidos por ponto e vírgula

Isso esconde um update que falhou, mas os dois comandos ESTÃO na mesma camada, então o DF007 não tem nada a dizer. Uma regra separada sobre operadores de shell seria ruído.

Tags que vêm de uma variável não resolvível

FROM node:$TAG é decidido em tempo de build pelo --build-arg. Adivinhar para o que resolve seria uma resposta confiante e errada.

COPY --from apontando para uma imagem de registry

Qualquer coisa com barra, dois-pontos ou @digest é uma referência externa legal, então o DF003 fica calado.

Semântica profunda de shell

Aqui não existe AST de shell: nenhum raciocínio sobre set -e, nenhuma análise de código de saída, nenhum rastreio de variáveis. O scanner respeita aspas e para aí. Ele também não lê o argv de um RUN em forma exec como texto de shell.

Heredocs não terminados, aspas desbalanceadas e dialetos # syntax=

O Docker reporta isso por conta própria, e um parse parcial não deve inventar uma regra em cima.

Tudo que está fora do arquivo

Nenhuma imagem é baixada, nenhum registry é consultado e nenhum .dockerignore é lido, porque nada disso está no texto que você colou.

Limites, ditos em vez de escondidos: o linter analisa até 200.000 caracteres, guarda no máximo 20 achados por regra e 200 no total, e informa o número real sempre que um limite se aplica.

Próximo passo

Cole o relatório no review.

O “Copy report” entrega a execução inteira como texto puro — uma linha por achado, com o número da linha, o id da regra e a correção. E depois siga: transforme o comando docker run em um serviço do Compose, ou confira se as variáveis que a imagem espera realmente existem.

report.txt
Dockerfile Linter — 1 error, 12 warnings, 2 info across 15 lines

ERRORS (1)
  L14 DF014: CMD looks like a JSON array but is not valid JSON.

WARNINGS (12)
  L1 DF002: Base image "ubuntu" has no tag, so Docker resolves it to :latest.
  L6 DF007: apt-get update runs without an install in the same RUN.
  L8 DF012: Piping a download straight into a shell.

FAQ

Suas perguntas, respondidas.

Toque em uma pergunta para expandir a resposta.

Dezessete regras, de DF001 a DF017, cada uma um erro que ainda assim builda: uma imagem base sem tag ou em :latest, um COPY --from que nomeia um estágio que ainda não existe, ADD onde COPY seria o certo, um download remoto sem verificação, um WORKDIR relativo, um apt-get update sozinho na própria camada, um cache de pacotes deixado dentro da imagem, um segredo escrito no histórico da imagem por ENV ou ARG, um estágio final que roda como root, cd em vez de WORKDIR, curl canalizado para um shell, um segundo CMD que vence em silêncio, uma forma exec JSON que não é JSON válido, sudo em um passo de build, MAINTAINER e um COPY . que destrói o cache da instalação de dependências. Cada regra tem a própria subseção nesta página, com um trecho antes e depois.

Não, e não tenta ser. O hadolint é um binário que você instala, e ele combina as próprias regras com tudo o que o ShellCheck reporta sobre o shell dentro das suas linhas RUN — excelente em CI e a resposta certa quando você pode impor uma toolchain. Isto cobre o caso que ele não cobre: um Dockerfile que você quer verificar agora, pelo navegador, em uma máquina onde você não pode instalar nada, sem mandar o arquivo para um servidor. Dezessete regras em vez de uma cauda longa, cada uma com a correção, e mais abaixo uma lista explícita do que ele se recusa a apontar.

Não. O parser e todas as dezessete regras são JavaScript rodando na sua aba — não há servidor, não há chamada de API e não há log, então 0 bytes são enviados. Aqui isso pesa mais do que na maioria das ferramentas: Dockerfiles carregam hostnames de registries internos, índices de pacotes privados, argumentos de build e, de vez em quando, uma credencial que ninguém deveria ter commitado.

Porque cada RUN é uma camada separada com a própria entrada de cache. Quando você depois edita só a linha de instalação, o Docker reaproveita a camada de update em cache — que pode ter semanas ou meses — e roda a instalação contra esse índice de pacotes desatualizado. O resultado é uma versão que você não esperava ou um 404 de um pacote que já foi substituído. Juntar os dois em um único RUN faz o índice e a instalação compartilharem a mesma entrada de cache, então eles nunca podem se contradizer.

Nada, até quem publica dar um push. :latest é um ponteiro móvel, e um FROM sem tag significa :latest, então a imagem base que seu build usou no mês passado e a que ele usa hoje à noite podem ser versões maiores diferentes sem nenhuma mudança do seu lado — e nada no arquivo registra qual você realmente testou. Fixe uma tag que você testou. Se precisar de rebuilds idênticos byte a byte, acrescente também o digest (@sha256:…). Este linter pede uma tag e nunca exige um digest: esse trade-off é seu.

Porque o Docker não rejeita — ele silenciosamente faz outra coisa. A forma exec só é usada quando o argumento é parseado como JSON, e JSON exige aspas duplas. Com aspas simples tudo cai de volta para a forma shell, então o Docker executa /bin/sh -c "['nginx']" e os colchetes e as aspas passam a fazer parte do comando. Nada avisa em tempo de build; o contêiner simplesmente não sobe, ou sobe um shell que não é o processo que você queria como PID 1.

Não diretamente, e ele diz isso em vez de adivinhar. Um .dockerignore é um arquivo separado, então um linter que só lê o Dockerfile não pode saber se node_modules, .git e os artefatos de build estão entrando no contexto de build. O que ele consegue é identificar o padrão que faz um .dockerignore ausente custar caro — um COPY . antes da instalação de dependências, que é o DF017 — e colocar o conselho sobre .dockerignore no texto de correção dessa regra.

Os dois foram considerados e deliberadamente descartados. EXPOSE é documentação: por si só não publica nada, então a ausência não quebra nada e um a mais não custa nada. HEALTHCHECK é ignorado por completo pelo Kubernetes, que é onde essas imagens de fato rodam, então apontá-lo treinaria este público a ignorar o linter. A lista completa do que fica em silêncio, com o motivo de cada item, está no painel “O que ele deliberadamente não aponta” acima do FAQ.

Até 200.000 caracteres — cerca de quatro mil linhas de texto denso, duas ordens de magnitude além de qualquer Dockerfile real. Acima disso ele recusa com uma mensagem em vez de congelar sua aba, porque uma entrada desse tamanho é um log de build ou um arquivo empacotado, não um Dockerfile. Os achados também têm limite, 20 por regra e 200 no total, e o painel informa o limite e o número real sempre que um deles se aplica.

More free, private DevOps tools.

O Dockerfile Linter é uma das ferramentas do OpsCanopy — uma copa crescente de validadores, conversores e testadores que rodam no navegador e nunca tocam um servidor.

39 free tools, every one offline-capable — opscanopy.com works with no signup and nothing uploaded.

Relacionados: Docker Run para Compose para transformar um comando de contêiner em arquivo de serviço, Env Example Checker para as variáveis que uma imagem espera, o GitHub Actions Validator e o GitLab CI Validator para o pipeline que constrói a imagem, e o Conversor JSON ↔ YAML quando a configuração em volta precisa ser remodelada — ou navegue pelo diretório de ferramentas completo.

Fornecido como está, por conveniência; sempre confirme configuração crítica contra a ferramenta que vai consumi-la. O OpsCanopy é gratuito e aberto.