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
Playground del 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.
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.
-
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.
-
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.
-
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.
-
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 |
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
RUN apt-get update
FROM debian:bookworm-slim Escribe
FROM debian:bookworm-slim
RUN apt-get update 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
FROM node
FROM node:latest Escribe
FROM node:22-bookworm-slim 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
FROM alpine:3.20
COPY --from=builder /out /srv Escribe
FROM golang:1.23-alpine AS builder
FROM alpine:3.20
COPY --from=builder /out /srv 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
ADD entrypoint.sh /entrypoint.sh Escribe
COPY entrypoint.sh /entrypoint.sh 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
ADD https://example.com/tool.tgz /tmp/tool.tgz Escribe
RUN curl -fsSL https://example.com/tool.tgz -o /tmp/tool.tgz \
&& echo "<sha256sum> /tmp/tool.tgz" | sha256sum -c - 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
WORKDIR app Escribe
WORKDIR /app 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
RUN apt-get update
RUN apt-get install -y curl Escribe
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* 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
RUN apt-get update && apt-get install -y curl Escribe
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* 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
ARG NPM_TOKEN=npm_liveTokenValue
ENV DB_PASSWORD=hunter2 Escribe
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci 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
FROM node:22-bookworm-slim
COPY . /app
CMD ["node", "/app/server.js"] Escribe
FROM node:22-bookworm-slim
COPY --chown=node:node . /app
USER node
CMD ["node", "/app/server.js"] 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
RUN cd /src && make Escribe
WORKDIR /src
RUN make 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
RUN curl -fsSL https://get.example.com/install.sh | sh Escribe
RUN curl -fsSL https://get.example.com/install.sh -o /tmp/install.sh \
&& echo "<sha256sum> /tmp/install.sh" | sha256sum -c - \
&& sh /tmp/install.sh 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
CMD ["node", "server.js"]
CMD ["node", "worker.js"] Escribe
CMD ["node", "server.js"]
# el worker corre como su propio contenedor, no como un segundo CMD 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
CMD ['nginx', '-g', 'daemon off;'] Escribe
CMD ["nginx", "-g", "daemon off;"] 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
RUN sudo apt-get install -y curl Escribe
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 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
MAINTAINER ops@example.com Escribe
LABEL org.opencontainers.image.authors="ops@example.com" 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
COPY . /app
RUN npm ci Escribe
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.
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.
¿Qué comprueba el Dockerfile Linter?
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.
¿Esto es hadolint?
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.
¿Mi Dockerfile sale alguna vez de mi navegador?
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.
¿Por qué apt-get update solo es un problema?
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.
¿Qué tiene de malo el tag :latest?
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.
¿Por qué CMD ['nginx'] es un error y no una advertencia?
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.
¿Puede decirme si necesito un .dockerignore?
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.
¿Por qué no señala un EXPOSE o un HEALTHCHECK que falta?
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.
¿De qué tamaño puede ser el Dockerfile?
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.