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
Playground do Dockerfile Linter
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.
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.
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.
-
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.
-
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.
-
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.
-
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 |
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
RUN apt-get update
FROM debian:bookworm-slim Escreva
FROM debian:bookworm-slim
RUN apt-get update 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
FROM node
FROM node:latest Escreva
FROM node:22-bookworm-slim 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
FROM alpine:3.20
COPY --from=builder /out /srv Escreva
FROM golang:1.23-alpine AS builder
FROM alpine:3.20
COPY --from=builder /out /srv 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
ADD entrypoint.sh /entrypoint.sh Escreva
COPY entrypoint.sh /entrypoint.sh 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
ADD https://example.com/tool.tgz /tmp/tool.tgz Escreva
RUN curl -fsSL https://example.com/tool.tgz -o /tmp/tool.tgz \
&& echo "<sha256sum> /tmp/tool.tgz" | sha256sum -c - 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
WORKDIR app Escreva
WORKDIR /app 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
RUN apt-get update
RUN apt-get install -y curl Escreva
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* 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
RUN apt-get update && apt-get install -y curl Escreva
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* 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
ARG NPM_TOKEN=npm_liveTokenValue
ENV DB_PASSWORD=hunter2 Escreva
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci 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
FROM node:22-bookworm-slim
COPY . /app
CMD ["node", "/app/server.js"] Escreva
FROM node:22-bookworm-slim
COPY --chown=node:node . /app
USER node
CMD ["node", "/app/server.js"] 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
RUN cd /src && make Escreva
WORKDIR /src
RUN make 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
RUN curl -fsSL https://get.example.com/install.sh | sh Escreva
RUN curl -fsSL https://get.example.com/install.sh -o /tmp/install.sh \
&& echo "<sha256sum> /tmp/install.sh" | sha256sum -c - \
&& sh /tmp/install.sh 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
CMD ["node", "server.js"]
CMD ["node", "worker.js"] Escreva
CMD ["node", "server.js"]
# o worker roda como o próprio contêiner, não como um segundo CMD 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
CMD ['nginx', '-g', 'daemon off;'] Escreva
CMD ["nginx", "-g", "daemon off;"] 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
RUN sudo apt-get install -y curl Escreva
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
USER app 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
MAINTAINER ops@example.com Escreva
LABEL org.opencontainers.image.authors="ops@example.com" 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
COPY . /app
RUN npm ci Escreva
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.
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.
O que o Dockerfile Linter verifica?
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.
Isso é o hadolint?
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.
Meu Dockerfile sai do meu navegador em algum momento?
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.
Por que apt-get update sozinho é um problema?
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.
O que há de errado com a tag :latest?
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.
Por que CMD ['nginx'] é erro e não aviso?
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.
Ele consegue dizer se eu preciso de um .dockerignore?
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.
Por que ele não aponta EXPOSE ou HEALTHCHECK ausentes?
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.
De que tamanho pode ser o Dockerfile?
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.