Saltar al contenido

Dockerfile Linter · Docker

Analiza un Dockerfile antes de que llegue a revisión.

Diecisiete reglas sobre un parse real del archivo: el apt-get update que servirá un índice obsoleto, el secreto escrito en el historial de tu imagen, la forma exec que Docker ejecutó calladamente como cadena de shell. Cada hallazgo nombra una línea y lleva la corrección — y nada de lo que pegues sale de la pestaña.

Se ejecuta en tu navegador: nada de lo que pegas sale de esta página. Cómo lo demostramos

Se ejecuta en tu navegador 17 reglas, DF001–DF017 Sin registro Actualizado el 31 jul 2026

Playground del 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.

La Brecha

Un Dockerfile que compila no es un Dockerfile correcto.

Todos los errores del catálogo de abajo compilan sin problema. FROM node compila. apt-get update en su propia capa compila — y luego instala desde un índice cacheado desde el trimestre pasado. CMD ['nginx'] compila, y Docker ejecuta calladamente /bin/sh -c "['nginx']" en lugar del proceso que querías como PID 1. Un build verde no es evidencia; es la ausencia de evidencia.

Pídele el Dockerfile a un asistente y heredas la misma clase de bug con más seguridad encima. Los Dockerfiles generados son fluidos y plausibles: pasan la revisión porque se parecen a todos los demás, y llevan defectos con la forma de DF007 y DF014 a los que ningún build fallido apuntará nunca. Comprobar la afirmación lleva segundos; lo difícil es saber qué comprobar.

hadolint es la respuesta correcta en CI, y esto no lo sustituye. Esto cubre el caso que él no cubre: un archivo que quieres revisar ahora, desde el navegador, en una máquina donde no puedes instalar un binario, sin mandar un Dockerfile lleno de hosts de registries internos y argumentos de build al servidor de otra persona. Diecisiete reglas, una corrección en cada hallazgo y una lista explícita de lo que se queda en silencio — porque un linter que no puedes auditar es un linter en el que tienes que confiar a ciegas.

¿Ya tienes un contenedor corriendo? Docker Run a Compose convierte el comando en un archivo de servicio, y Env Example Checker detecta las variables que tu imagen espera y tu entorno nunca define.

La Pipeline

Cómo funciona.

Cuatro pasos, todos dentro de tu pestaña, que se repiten mientras escribes.

  1. Parsear como BuildKit.

    Continuaciones, líneas de comentario completas dentro de ellas, cuerpos de heredoc, la forma exec JSON, las etapas y la tabla de ARG previa al primer FROM. Una directiva `# escape=` de verdad cambia el carácter de continuación.

  2. Analizar el parse, no el texto.

    Diecisiete reglas leen instrucciones y etapas. Las reglas de shell recorren un escaneo de cada RUN que respeta las comillas, así que una tubería dentro de una cadena no es una tubería y el cuerpo de un heredoc sigue siendo shell.

  3. Informar de una línea y una corrección.

    Cada hallazgo nombra la línea FÍSICA — la línea 4 de un RUN plegado, no la línea en la que empezó el RUN — y lleva el cambio que hay que hacer, listo para pegar en una revisión.

  4. Decir lo que no comprobó.

    Las reglas que se niega a ejecutar están en esta página con el motivo de cada una. Un linter que no puedes auditar es un linter en el que tienes que confiar a ciegas.

Referencia

El catálogo de reglas.

Las diecisiete reglas: tres errores, doce advertencias y dos notas. Un error significa que Docker rechaza el archivo o ejecuta calladamente otra cosa; una advertencia significa que compila y está mal; una nota es algo que conviene saber. Cada hallazgo del playground enlaza a su regla aquí.

Regla Gravedad Qué detecta
DF001 error La primera instrucción debe ser FROM
DF002 advertencia Fija la imagen base a un tag
DF003 error COPY --from debe nombrar una etapa anterior
DF004 advertencia Usa COPY para archivos locales, no ADD
DF005 advertencia Verifica un ADD remoto
DF006 advertencia WORKDIR debería ser absoluto
DF007 advertencia Update e instalación en el mismo RUN
DF008 advertencia Limpia la caché de paquetes en la misma capa
DF009 advertencia No metas secretos en ENV ni en ARG
DF010 advertencia No hagas que la etapa final corra como root
DF011 advertencia Usa WORKDIR en lugar de cd
DF012 advertencia No canalices una descarga a una shell
DF013 advertencia Un CMD y un ENTRYPOINT por etapa
DF014 error La forma exec debe ser JSON válido
DF015 advertencia Quita sudo de los pasos de build
DF016 nota MAINTAINER está obsoleto
DF017 nota Copia el manifiesto antes de instalar dependencias
DF001 error

La primera instrucción debe ser FROM

Solo ARG (y los comentarios) pueden ir antes de FROM. Cualquier otra cosa no tiene imagen sobre la que actuar, y Docker rechaza el build. Un archivo sin ningún FROM cae en la misma regla.

En vez de

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

Escribe

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

Fija la imagen base a un tag

Una referencia sin tag significa :latest, y :latest se mueve. Los pines por digest, scratch, las referencias a una etapa anterior y los tags que vienen de un argumento de build no resoluble se dejan en paz.

En vez de

DOCKERFILE
FROM node
FROM node:latest

Escribe

DOCKERFILE
FROM node:22-bookworm-slim
DF003 error

COPY --from debe nombrar una etapa anterior

Una etapa solo puede copiar de otra definida por encima. Un índice numérico igual o posterior a la etapa actual, y un nombre suelto que no coincide con ninguna etapa, rompen el build con «invalid from flag value». Todo lo que lleve registry, tag o digest se trata como imagen externa y se deja en paz.

En vez de

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

Escribe

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

Usa COPY para archivos locales, no ADD

ADD extrae automáticamente los archivos tar locales y puede descargar URLs, lo que hace que una copia simple se comporte de formas que la línea no dice. Extraer un archivo es el único trabajo por el que vale la pena conservar ADD, así que un ADD cuyas fuentes son todas tarballs se queda en silencio.

En vez de

DOCKERFILE
ADD entrypoint.sh /entrypoint.sh

Escribe

DOCKERFILE
COPY entrypoint.sh /entrypoint.sh
DF005 advertencia

Verifica un ADD remoto

Un ADD remoto mete en una capa lo que el servidor devuelva, y nada en el build se da cuenta cuando eso cambia. El --checksum= de BuildKit satisface la regla; descargar y verificar en un mismo RUN también.

En vez de

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

Escribe

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

WORKDIR debería ser absoluto

Un WORKDIR relativo se resuelve contra el WORKDIR anterior, así que el directorio que selecciona depende de las líneas de arriba — y cambia en cuanto se reordenan. Las rutas que empiezan por una variable se dejan en paz.

En vez de

DOCKERFILE
WORKDIR app

Escribe

DOCKERFILE
WORKDIR /app
DF007 advertencia

Update e instalación en el mismo RUN

Cada RUN es su propia capa. Una capa de update cacheada más una capa de instalación reconstruida significa instalar desde un índice de paquetes que puede tener meses. Cubre apt-get, apt y apk.

En vez de

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

Escribe

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

Limpia la caché de paquetes en la misma capa

El índice de paquetes se commitea con la capa, así que borrarlo en un RUN posterior no recupera nada. Se satisface con rm -rf /var/lib/apt/lists/* para apt, --no-cache para apk y dnf/yum clean all — y se suprime del todo cuando el RUN lleva un --mount=type=cache, porque entonces la caché nunca entra en la imagen.

En vez de

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

Escribe

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

No metas secretos en ENV ni en ARG

Cada valor de ENV y ARG queda guardado en el historial de la imagen y es legible con docker history por cualquiera que pueda hacer pull. Sobrescribirlo en una capa posterior no lo elimina. Solo se señalan los nombres que parecen una credencial Y llevan un valor no vacío.

En vez de

DOCKERFILE
ARG NPM_TOKEN=npm_liveTokenValue
ENV DB_PASSWORD=hunter2

Escribe

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

No hagas que la etapa final corra como root

Sin USER el proceso corre como uid 0. Solo se comprueba la etapa FINAL — las etapas de build necesitan root legítimamente — y dentro de ella solo cuenta el ÚLTIMO USER, porque es el que el contenedor acaba usando.

En vez de

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

Escribe

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

Usa WORKDIR en lugar de cd

Un cd solo dura el RUN en el que está escrito. La siguiente instrucción vuelve a arrancar en el WORKDIR de la etapa, y esa es una causa habitual de «no such file or directory» en el COPY o el CMD siguiente.

En vez de

DOCKERFILE
RUN cd /src && make

Escribe

DOCKERFILE
WORKDIR /src
RUN make
DF012 advertencia

No canalices una descarga a una shell

curl … | sh ejecuta lo que el servidor devuelva, sin firma y sin checksum. Se detecta igual dentro de líneas RUN plegadas y de cuerpos de heredoc, y se informa en la línea donde está escrita la descarga.

En vez de

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

Escribe

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 advertencia

Un CMD y un ENTRYPOINT por etapa

Docker conserva el último de cada etapa y descarta el resto en silencio. Se cuenta por etapa, así que una etapa de build con su propio CMD está bien, y las instrucciones envueltas en ONBUILD no cuentan.

En vez de

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

Escribe

DOCKERFILE
CMD ["node", "server.js"]
# el worker corre como su propio contenedor, no como un segundo CMD
DF014 error

La forma exec debe ser JSON válido

Docker solo lee el argumento como array en forma exec cuando parsea como JSON — comillas dobles, sin coma final. Si no, ejecuta todo en silencio como forma shell a través de /bin/sh -c, corchetes incluidos.

En vez de

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

Escribe

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

Quita sudo de los pasos de build

Un paso de build ya corre como el USER de la etapa — root, salvo que lo hayas cambiado — sin TTY, y la mayoría de las imágenes base no traen sudo. Cambia de usuario de forma explícita.

En vez de

DOCKERFILE
RUN sudo apt-get install -y curl

Escribe

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 Docker 1.13, en 2017. Sigue compilando, pero nada lo lee: la etiqueta OCI es lo que muestran los registries y los escáneres.

En vez de

DOCKERFILE
MAINTAINER ops@example.com

Escribe

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

Copia el manifiesto antes de instalar dependencias

Un COPY . más arriba en la etapa que la instalación de dependencias significa que cualquier cambio solo de código fuente invalida la capa de instalación y reinstala todo. Reconoce npm, yarn, pnpm, pip -r, bundler, composer, go mod download y cargo. Combina el reordenamiento con un .dockerignore para que node_modules y .git no lleguen ni al contexto.

En vez de

DOCKERFILE
COPY . /app
RUN npm ci

Escribe

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

El Límite

Lo que deliberadamente no señala.

Cada uno de estos puntos se consideró y se descartó, y la misma lista está en un comentario al principio del motor. Un silencio que puedes leer vale más que una regla que aprendes a ignorar.

Fijar versiones más allá del tag

apt-get install curl sin versión y pip install django sin == son ambos legítimos. DF002 pide un tag; nunca exige un digest ni un conjunto de paquetes fijado.

EXPOSE

Por sí solo no publica nada. Su ausencia no rompe nada y uno de más no cuesta nada — pura opinión.

HEALTHCHECK

Kubernetes lo ignora por completo, y ahí es donde este público corre sus imágenes.

Cachés de descarga de pip y npm

Reales pero pequeñas, y los flags cambian entre versiones de las herramientas. DF008 cubre los gestores de paquetes del sistema, donde están los megabytes.

update; install unidos con punto y coma

Oculta un update fallido, pero los dos comandos SÍ están en la misma capa, así que DF007 no tiene nada que decir. Una regla aparte sobre operadores de shell sería ruido.

Tags que vienen de una variable no resoluble

FROM node:$TAG se decide en tiempo de build con --build-arg. Adivinar a qué resuelve sería una respuesta segura y equivocada.

COPY --from apuntando a una imagen de registry

Todo lo que lleve barra, dos puntos o @digest es una referencia externa legal, así que DF003 se calla.

Semántica profunda de shell

Aquí no hay AST de shell: nada de razonar sobre set -e, ni análisis de códigos de salida, ni seguimiento de variables. El escáner respeta las comillas y se detiene ahí. Tampoco lee el argv de un RUN en forma exec como texto de shell.

Heredocs sin terminar, comillas desbalanceadas y dialectos # syntax=

Docker informa de eso por su cuenta, y un parse parcial no debe inventar una regla encima.

Todo lo que está fuera del archivo

No se descarga ninguna imagen, no se consulta ningún registry y no se lee ningún .dockerignore, porque nada de eso está en el texto que has pegado.

Límites, dichos en lugar de escondidos: el linter analiza hasta 200.000 caracteres, guarda como máximo 20 hallazgos por regla y 200 en total, y te dice la cifra real siempre que se aplica un tope.

Siguiente paso

Pega el informe en la revisión.

«Copy report» te da toda la ejecución como texto plano: una línea por hallazgo, con el número de línea, el id de la regla y la corrección. Y luego sigue: convierte el comando docker run en un servicio de Compose, o comprueba que las variables que la imagen espera existen de verdad.

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

Tus preguntas, respondidas.

Toca una pregunta para desplegar la respuesta.

Diecisiete reglas, de DF001 a DF017, y cada una es un error que aun así compila: una imagen base sin tag o con :latest, un COPY --from que nombra una etapa que todavía no existe, ADD donde corresponde COPY, una descarga remota sin verificar, un WORKDIR relativo, un apt-get update abandonado en su propia capa, una caché de paquetes que se queda dentro de la imagen, un secreto escrito en el historial de la imagen por ENV o ARG, una etapa final que corre como root, cd en lugar de WORKDIR, curl canalizado a una shell, un segundo CMD que gana en silencio, una forma exec JSON que no es JSON válido, sudo en un paso de build, MAINTAINER, y un COPY . que destruye la caché de la instalación de dependencias. Cada regla tiene su propia subsección en esta página con un fragmento de antes y después.

No, y no pretende serlo. hadolint es un binario que instalas, y combina sus propias reglas con todo lo que ShellCheck dice sobre la shell dentro de tus líneas RUN — excelente en CI y la respuesta correcta cuando puedes imponer un toolchain. Esto cubre el caso que hadolint no cubre: un Dockerfile que quieres revisar ahora mismo, desde el navegador, en una máquina donde no puedes instalar nada, sin enviar el archivo a un servidor. Diecisiete reglas en lugar de una cola larga, cada una con su corrección, y más abajo una lista explícita de lo que se niega a señalar.

No. El parser y las diecisiete reglas son JavaScript ejecutándose en tu pestaña — no hay servidor, ni llamada a una API, ni registro, así que se suben 0 bytes. Aquí importa más que en la mayoría de las herramientas: los Dockerfiles llevan nombres de host de registries internos, índices de paquetes privados, argumentos de build y, de vez en cuando, una credencial que alguien no debería haber commiteado.

Porque cada RUN es una capa aparte con su propia entrada de caché. Cuando después editas solo la línea de instalación, Docker reutiliza la capa de update ya cacheada — que puede tener semanas o meses — y ejecuta la instalación contra ese índice de paquetes obsoleto. El resultado es o una versión que no esperabas o un 404 de un paquete que ya fue reemplazado. Unirlos en un solo RUN hace que el índice y la instalación compartan entrada de caché, así que nunca pueden contradecirse.

Nada, hasta que el publicador hace push. :latest es un puntero móvil, y un FROM sin tag significa :latest, así que la imagen base que usó tu build el mes pasado y la que usa esta noche pueden ser versiones mayores distintas sin que tú hayas cambiado nada — y nada en el archivo registra cuál probaste de verdad. Fija un tag que hayas probado. Si necesitas rebuilds idénticos byte a byte, añade también el digest (@sha256:…). Este linter pide un tag y nunca exige un digest: esa decisión es tuya.

Porque Docker no lo rechaza: silenciosamente hace otra cosa. La forma exec solo se usa cuando el argumento parsea como JSON, y JSON exige comillas dobles. Con comillas simples todo cae de vuelta a la forma shell, así que Docker ejecuta /bin/sh -c "['nginx']" y los corchetes y las comillas pasan a formar parte del comando. Nada avisa en tiempo de build; el contenedor simplemente no arranca, o arranca una shell que no es el proceso que querías como PID 1.

No directamente, y lo dice en lugar de adivinar. Un .dockerignore es un archivo separado, así que un linter que solo lee el Dockerfile no puede saber si node_modules, .git y tus artefactos de build están entrando en el contexto de build. Lo que sí puede hacer es detectar el patrón que encarece la falta de un .dockerignore — un COPY . antes de la instalación de dependencias, que es DF017 — y poner el consejo del .dockerignore en el texto de corrección de esa regla.

Ambos se consideraron y se descartaron a propósito. EXPOSE es documentación: por sí solo no publica nada, así que su ausencia no rompe nada y uno de más no cuesta nada. HEALTHCHECK lo ignora por completo Kubernetes, que es donde estas imágenes acaban corriendo, así que señalarlo entrenaría a este público a ignorar el linter. La lista completa de lo que se queda en silencio, con el motivo de cada punto, está en el panel «Lo que deliberadamente no señala» sobre las FAQ.

Hasta 200.000 caracteres — unas cuatro mil líneas de texto denso, dos órdenes de magnitud por encima de cualquier Dockerfile real. Por encima de eso se niega con un mensaje en lugar de congelar tu pestaña, porque una entrada así es un log de build o un archivo empaquetado, no un Dockerfile. Los hallazgos también tienen tope, 20 por regla y 200 en total, y el panel indica el tope y la cifra real siempre que alguno se aplica.

More free, private DevOps tools.

El Dockerfile Linter es una herramienta de OpsCanopy — una copa de árbol en crecimiento de validadores, convertidores y testers basados en el navegador que nunca tocan un servidor.

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

Relacionado: Docker Run a Compose para convertir un comando de contenedor en un archivo de servicio, Env Example Checker para las variables que espera una imagen, el GitHub Actions Validator y el GitLab CI Validator para la pipeline que construye la imagen, y el Convertidor JSON ↔ YAML cuando hay que remodelar la configuración de alrededor — o explora el directorio de tools completo.

Se ofrece tal cual, por comodidad; verifica siempre la configuración crítica contra la herramienta que la va a consumir. OpsCanopy es gratuito y abierto.