Hadolint en modo Web
Actualmente los mundos de CI/CD, QA (Quality Assurance / Control de Calidad), DevOps y CiberSeguridad estan tan interrelacionados que comparten o solapan algunas de sus funciones.
Dockerfile el punto de partida
Si nos centramos en la parte de contenedores Docker, una de las primeras cosas que afecta tanto a la calidad, como a la eficiencia del despliegue y ejecución posterior, como a toda la parte de la ciberseguridad es el Dockerfile.
A partir del Dockerfile, se crea la imagen que llegará a Producción, se parte de una imagen base a la que se van incluyendo acciones, comandos, opciones, etc, que afectarán a la imagen final.
Es por ello que es imprescindible disponer de un fichero Dockerfile que siga las mejores recomendaciones de calidad y seguridad para la construcción de la imagen final.
Debe de ser en este punto inicial donde los departamentos de ingeniería deben de comenzar a poner límites o al menos revisar que se parte de un fichero que cumple unos mínimos.
Hadolint
Hadolint es una herramienta de análisis estático (linter) diseñada específicamente para evaluar, validar y mejorar el código de los Dockerfiles.
Su objetivo principal es asegurar que tus archivos sigan las mejores prácticas de la industria en cuanto a seguridad, rendimiento y mantenibilidad antes de construir la imagen del contenedor.
¿Qué hace exactamente?
- Analiza sintaxis y lógica: Parsea el Dockerfile en un Árbol de Sintaxis Abstracta (AST) para detectar errores estructurales.
- Incorpora ShellCheck: Analiza de forma inteligente el código Bash dentro de las instrucciones
RUN, detectando posibles fallos en scripts. - Explicación detallada: No solo te dice que hay un error, sino que te explica por qué es un problema y cómo solucionarlo.
- Configuración personalizable: Puedes omitir reglas específicas que no apliquen a tu proyecto usando el archivo
.hadolint.yamlo utilizando comentarios inline (# hadolint ignore=DLxxxx).
Beneficios clave
- Optimiza el tamaño de las imágenes: Detecta si estás instalando paquetes innecesarios, limpiando mal la caché o usando imágenes base demasiado pesadas.
- Mejora la seguridad: Identifica malas prácticas como usar etiquetas
latest, exponer puertos innecesarios o ejecutar contenedores como usuariorootpor defecto. - Integración en el flujo de trabajo: Se puede usar directamente desde la línea de comandos, como un contenedor Docker, incrustado en editores de código (como VS Code), o automatizado en tus entornos de CI/CD para bloquear subidas con código incorrecto.
Cómo Obtenerlo
Desde su página web puedes acceder a los binarios, su github o incluso analizar el código subiendolo on-line

Nuestro propio Halolint Web
Seguramente por normativas de seguridad en nuestra empresa no se permita subir nada a webs de terceros.
A pesar de que podemos instalar Hadolint en modo local o incluso usarlo en modo CLI desde un contenedor, podemos mejorar esto para facilitar el trabajo a los desarrolladores de nuestra empresa poniendo a su disposición un servicio web que haga una pre-validación antes de que su Dockerfile pase por todas las validaciones del entorno CI/CD y evitar su frustación.
Os dejo una forma sencilla de crear nuestro propio servicio web interno para ofrecer Hadolint dentro de nuestra organización.

Sólo vamos a necesitar 3 ficheros. Crea un directorio para tener a mano todo el proyecto completo.
├── server.js # El backend. Sencillo mediante Node.js (el que ejecuta Hadolint)
├── index.html # La interfaz web elegante
└── Dockerfile # El contenedor que unifica todoVeamos ahora con cada uno de los ficheros.
server.js
const express = require('express');
const { spawn } = require('child_process');
const path = require('path');
const app = express();
const PORT = 3000;
// Limitar el tamaño del Dockerfile a 1MB por seguridad
app.use(express.json({ limit: '1mb' }));
app.use(express.static(path.join(__dirname)));
app.post('/analyze', (req, res) => {
const { dockerfile } = req.body;
if (!dockerfile) {
return res.status(400).json({ error: 'El contenido del Dockerfile está vacío.' });
}
// Ejecutamos hadolint pasándole el formato JSON y leyendo de stdin (-)
const hadolint = spawn('hadolint', ['--format', 'json', '-']);
let stdout = '';
let stderr = '';
hadolint.stdout.on('data', (data) => { stdout += data; });
hadolint.stderr.on('data', (data) => { stderr += data; });
hadolint.on('close', (code) => {
// Hadolint devuelve código 1 o 2 si encuentra fallos, por lo que no dependemos del código de salida 0
if (stderr && !stdout) {
return res.status(500).json({ error: 'Error interno de Hadolint', details: stderr });
}
try {
const results = JSON.parse(stdout || '[]');
res.json({ success: true, results });
} catch (e) {
res.status(500).json({ error: 'Error al procesar la respuesta del linter.' });
}
});
// Escribimos el Dockerfile en el stdin de Hadolint y cerramos el flujo
hadolint.stdin.write(dockerfile);
hadolint.stdin.end();
});
app.listen(PORT, () => {
console.log(`Hadolint Web corriendo en http://localhost:${PORT}`);
});index.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hadolint Web Linter</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/theme/dracula.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/dockerfile/dockerfile.min.js"></script>
<style>
.CodeMirror { height: 350px; border-radius: 0.5rem; font-family: Fira Code, monospace; font-size: 14px; }
</style>
</head>
<body class="bg-slate-900 text-slate-100 min-h-screen flex flex-col justify-between font-sans">
<header class="border-b border-slate-800 bg-slate-900/50 backdrop-blur py-4 px-6">
<div class="max-w-5xl mx-auto flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight text-indigo-400 flex items-center gap-3">
<span class="text-4xl sm:text-5xl">🐋</span> Hadolint UI <span class="text-slate-500 font-normal text-lg sm:text-xl ml-1"></span>
</h1>
<span class="text-xs font-mono bg-slate-800 text-slate-400 px-2.5 py-1 rounded-md border border-slate-700 self-start sm:self-auto">
Docker Linter by SoloConlinux
</span>
</div>
</header>
<main class="flex-grow max-w-5xl w-full mx-auto p-6 space-y-6">
<div class="bg-slate-850 border border-slate-800 rounded-xl p-4 shadow-xl space-y-4">
<label class="block text-sm font-medium text-slate-300">Pega tu Dockerfile aquí:</label>
<textarea id="dockerfile-editor">FROM debian:latest RUN apt-get update && apt-get install -y git</textarea>
</div>
<!-- Botones de Acción -->
<div class="flex flex-col sm:flex-row gap-3">
<!-- Botón para Cargar Archivo Local -->
<button id="btn-upload" type="button" class="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-200 font-medium py-2.5 px-4 rounded-lg transition-all duration-200 cursor-pointer border border-slate-700 flex items-center justify-center gap-2">
Cargar Dockerfile local
</button>
<!-- Input oculto que abrirá el explorador de archivos -->
<input type="file" id="file-input" accept="Dockerfile,*" class="hidden">
<!-- Botón de Procesar -->
<button id="btn-process" class="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white font-medium py-2.5 px-4 rounded-lg transition-all duration-200 cursor-pointer shadow-lg shadow-indigo-600/20 active:scale-[0.99]">
Analizar Dockerfile
</button>
</div>
<div class="bg-slate-850 border border-slate-800 rounded-xl p-4 shadow-xl space-y-3">
<h2 class="text-sm font-medium text-slate-300">Resultado del Análisis:</h2>
<div id="result-box" class="bg-slate-950 p-4 rounded-lg border border-slate-800 min-h-[100px] font-mono text-sm overflow-x-auto text-slate-400">
Elige un Dockerfile y presiona "Analizar" para ver los resultados.
</div>
</div>
</main>
<footer class="text-center py-4 text-xs text-slate-600 border-t border-slate-800">
SoloConLinux · Hadolint Web Wrapper
</footer>
<script>
// Inicializar CodeMirror con temática Dracula
const editor = CodeMirror.fromTextArea(document.getElementById("dockerfile-editor"), {
mode: "dockerfile",
theme: "dracula",
lineNumbers: true,
indentUnit: 4
});
const btn = document.getElementById('btn-process');
const resultBox = document.getElementById('result-box');
const severityColors = {
error: { bg: 'bg-red-500/10', border: 'border-red-500/30', text: 'text-red-400', label: 'ERROR' },
warning: { bg: 'bg-amber-500/10', border: 'border-amber-500/30', text: 'text-amber-400', label: 'WARNING' },
info: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', text: 'text-blue-400', label: 'INFO' },
style: { bg: 'bg-teal-500/10', border: 'border-teal-500/30', text: 'text-teal-400', label: 'STYLE' }
};
btn.addEventListener('click', async () => {
btn.disabled = true;
btn.innerText = 'Analizando...';
resultBox.innerHTML = '<span class="text-indigo-400 animate-pulse">Procesando reglas de Hadolint...</span>';
try {
const response = await fetch('/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dockerfile: editor.getValue() })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Error desconocido');
if (data.results.length === 0) {
resultBox.innerHTML = '<div class="text-emerald-400 font-medium">¡Perfecto! Tu Dockerfile no tiene ninguna advertencia y cumple todas las buenas prácticas de Hadolint.</div>';
} else {
resultBox.innerHTML = '';
data.results.forEach(item => {
const sev = severityColors[item.level.toLowerCase()] || severityColors.info;
const card = `
<div class="mb-3 p-3 rounded-lg border ${sev.bg} ${sev.border} flex flex-col md:flex-row md:items-center justify-between gap-2">
<div>
<span class="inline-block px-1.5 py-0.5 rounded text-xs font-bold ${sev.text} bg-slate-900 border ${sev.border} mr-2">${sev.label}</span>
<span class="text-slate-200">Línea ${item.line}:</span>
<span class="text-slate-300 ml-1">${item.message}</span>
</div>
<a href="https://github.com/hadolint/hadolint/wiki/${item.code}" target="_blank" class="text-xs text-indigo-400 hover:underline shrink-0 font-sans">${item.code} ↗</a>
</div>
`;
resultBox.innerHTML += card;
});
}
} catch (error) {
resultBox.innerHTML = `<div class="text-red-400 font-medium">Error: ${error.message}</div>`;
} finally {
btn.disabled = false;
btn.innerText = 'Analizar Dockerfile';
}
});
// Obtener los nuevos elementos del DOM
const btnUpload = document.getElementById('btn-upload');
const fileInput = document.getElementById('file-input');
// Al hacer clic en el botón visual, activamos el input oculto
btnUpload.addEventListener('click', () => {
fileInput.click();
});
// Cuando el usuario selecciona un archivo
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
// Leer el archivo como texto plano
reader.onload = (e) => {
const contenido = e.target.result;
// Inyectamos el contenido en el editor de CodeMirror
editor.setValue(contenido);
// Limpiamos el input para permitir cargar el mismo archivo otra vez si se modifica
fileInput.value = '';
};
reader.readAsText(file);
});
</script>
</body>
</html>Dockerfile
# --- Stage 1: Obtener Hadolint ---
FROM hadolint/hadolint:latest-alpine AS hadolint-bin
# --- Stage 2: Construir nuestra App ---
FROM node:20-alpine
# Copiamos el binario de Hadolint desde la primera etapa
COPY --from=hadolint-bin /bin/hadolint /usr/local/bin/hadolint
# Definir directorio de trabajo
WORKDIR /app
# Copiar archivos de dependencias e instalar Express
COPY server.js index.html ./
RUN npm init -y && npm install express
# Exponer el puerto de la aplicación web
EXPOSE 3000
# Ejecutar el servidor
CMD ["node", "server.js"]Construir la imagen de hadolint-web
Para construir la imagen que permitirá iniciar nuestro servicio web, tan sólo debemos ejecutar lo siguiente:
docker build -t hadolint-web .Recuerda que cada cambio que realices sobre server.js o index.html debes de volver a crear la nueva versión de la imagen.
Iniciar Hadolint Web
Ahora sólo tienes que iniciar la imagen y exponer el puerto en el que quieres que se publique el servicio web.
En el ejemplo que os muestro, lo vamos a iniciar en un contenedor local y ejecutándose en el puerto 3000 el mismo por el que responde internamente.
El comando para iniciar el contenedor hadolint-web es:
# Iniciar en puerto 3000 (http://localhost:3000)
docker run -d -p 3000:3000 --name hadolint-web hadolint-webRecuerda que si has creado una nueva imagen debes de eliminar este contenedor e iniciar uno nuevo para que recargar los cambios:
# Eliminar contenedor previo
docker rm --force hadolint-web
# Iniciar nueva version
docker run -d -p 3000:3000 --name hadolint-web hadolint-web
Ejecución y Análisis vía Web
Os dejo un ejemplo del análisis de un Dockerfile real.

Ajustes en el Dockerfile
Debemos realizar los ajustes necesarios para poder obtener un resultado correcto, ya que seguramente el departamento de CI/CD, Calidad o el de Seguridad dependiendo de sus criterios pueden que no acepten ese Dockerfile para crear una imagen.
Exclusiones en Hadolint
Sin embargo si en algun momento queremos que Hadolint ignore una línea por algún motivo, la forma de indicarlo es simplemente escribir encima de la línea que se quiera ignorar el `DLxxx" el siguiente texto con la sintaxis:
# Ignorar DL3027
# hadolint ignore=DL3027
&& apt -y install --no-install-recommends ....
# Ingnorar más de un DL en una línea (separamos con comas cada DL)
# hadolint ignore=DL3008,DL3015
RUN apt-get update && apt-get install -y curl wgetEl mismo Dockerfile anterior si ignoramos los DL se mostrará como correcto:

Con este servicio Web, el grupo de Desarrollo no tiene que esperar a que se completen las operaciones de CI/CD o de Calidad y puede tener de forma preliminar un resultado de la revisión de su Dockerfile e irlo corrigiendo sin depender de otros grupos.
Espero que os sea útil y ahorre mucho tiempo de procesamiento CI/CD
