Anatomía de un Dockerfile Profesional

Lectura
20 min~8 min lectura
CONCEPTO CLAVE: Un Dockerfile es un archivo de texto plano que contiene todas las instrucciones necesarias para construir una imagen Docker. La diferencia entre un Dockerfile amateur y uno profesional radica en la optimización, seguridad y mantenibilidad del mismo.

¿Qué es un Dockerfile?

Un Dockerfile es el corazón de la contenerización en Docker. Es un archivo de texto plano que utiliza su propio lenguaje de dominio específico (DSL) para definir paso a paso cómo debe configurarse un contenedor. Cada instrucción en el Dockerfile crea una capa (layer) en la imagen final, y entender cómo estas capas interactúan es fundamental para dominar Docker.

Cuando ejecutamos docker build, Docker lee el Dockerfile desde arriba hacia abajo, ejecutando cada instrucción en secuencia. El resultado es una imagen inmutable que puede desplegarse en cualquier ambiente que tenga Docker instalado.

Estructura Básica de un Dockerfile

Un Dockerfile profesional sigue una estructura lógica bien definida. Veamos las secciones principales:

Sección Instrucciones Propósito
Base FROM Define la imagen base
Metadatos LABEL, MAINTAINER Información sobre la imagen
Variables ENV, ARG Configuración de ambiente
Dependencias RUN, COPY Instalación de paquetes
Configuración WORKDIR, USER, EXPOSE Configuración del entorno
Arranque CMD, ENTRYPOINT Comando inicial

Instrucciones Fundamentales

FROM - La Base de Todo

La instrucción FROM es la más importante y debe ser siempre la primera instrucción ejecutable (después de comentarios y parser directives). Define la imagen base sobre la cual construiremos.

# Forma recomendada actual
FROM node:18-alpine

# Usar versiones específicas, nunca :latest en producción
FROM python:3.11-slim-bookworm
FROM eclipse-temurin:17-jre-alpine
💡 Consejo profesional: Usa imágenes oficiales con versiones específicas. Las imágenes -alpine son más pequeñas y seguras, mientras que las -slim ofrecen un buen balance entre tamaño y compatibilidad.

LABEL - Metadatos que Importan

Los labels permiten añadir metadatos a la imagen sin afectar su funcionamiento. Son fundamentales para la documentación y el mantenimiento.

LABEL maintainer="[email protected]"
LABEL version="2.1.0"
LABEL description="API REST para gestión de usuarios"
LABEL org.opencontainers.image.source="https://github.com/empresa/api-users"
LABEL org.opencontainers.image.licenses="MIT"

ARG vs ENV - Variables en el Dockerfile

Entender la diferencia entre ARG y ENV es crucial:

Característica ARG ENV
Disponible durante build ✅ Sí ✅ Sí
Disponible en contenedor ❌ No ✅ Sí
Persiste en imagen ❌ No ✅ Sí
Puede recibir valores externos ✅ docker build --build-arg ⚠️ Limitado
# ARG: para parámetros de construcción
ARG NODE_ENV=production
ARG APP_VERSION

# ENV: para variables de runtime
ENV NODE_ENV=production
ENV APP_HOME=/app
ENV PATH="$APP_HOME/bin:$PATH"

WORKDIR - El Directorio de Trabajo

La instrucción WORKDIR establece el directorio de trabajo para las instrucciones siguientes. Es preferible usar WORKDIR en lugar de múltiples instrucciones RUN cd.

# ❌ No recomendado
RUN mkdir -p /app/src
RUN cd /app/src
RUN npm install

# ✅ Recomendado
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
⚠️ Advertencia: WORKDIR crea automáticamente el directorio si no existe. Sin embargo, es buena práctica crear explícitamente la estructura de directorios antes para evitar confusiones.

Optimización de Capas y Caché

📌 Punto clave: Cada instrucción RUN crea una nueva capa. El orden de las instrucciones afecta directamente la velocidad de construcción y la reutilización del caché.

Docker cachea cada capa resultante. Cuando una capa cambia, todas las capas posteriores deben reconstruirse. La optimización del orden de instrucciones es fundamental para builds rápidos.

Estrategia de Copia Inteligente

# ✅ Orden optimizado: lo que cambia menos va primero
FROM node:18-alpine

WORKDIR /app

# 1. Copiar solo archivos de dependencias primero
COPY package*.json ./

# 2. Instalar dependencias (se cachea si package*.json no cambió)
RUN npm ci --only=production

# 3. Copiar el código fuente (cambia frecuentemente)
COPY . .

# 4. Exponer puerto y usuario
EXPOSE 3000
USER node

Esta estructura permite que Docker reutilice el caché de npm install incluso cuando el código fuente cambia, reduciendo drásticamente los tiempos de build.

Ver más: Técnicas avanzadas de caché

Existen técnicas adicionales para maximizar el uso del caché:

  • mounts de tipo cache: Para dependencias que no deben persistir en la imagen final.
  • .dockerignore: Excluir archivos innecesarios del contexto de build.
  • BuildKit secreto: Para copiar archivos sensibles sin incluirlos en la imagen.

Multi-Stage Builds: El Estándar Profesional

Los multi-stage builds son una técnica avanzada que permite crear imágenes finales pequeñas y seguras. Consisten en usar múltiples instrucciones FROM para separar el ambiente de construcción del ambiente de producción.

# Etapa 1: Construcción
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

# Etapa 2: Producción (imagen limpia y pequeña)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app

# Copiar solo el artifact compilado
COPY --from=builder /app/target/myapp.jar ./myapp.jar

# Usuario no root para seguridad
USER 1001

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "myapp.jar"]
💡 Resultado: Esta imagen de producción solo contiene el JRE (no el JDK), las dependencias descargadas y el artefacto compilado. El tamaño puede reducirse de ~800MB a ~200MB o menos.

Seguridad en el Dockerfile

Un Dockerfile profesional prioriza la seguridad en cada capa:

  1. Usar usuarios no root: Crear un usuario específico y usarlo para ejecutar la aplicación.
  2. Minimizar la superficie de ataque: Usar imágenes base mínimas (alpine, slim).
  3. Escanear vulnerabilidades: Integrar herramientas como Trivy o Snyk en el pipeline.
  4. No almacenar secretos: Usar BuildKit secrets o variables de entorno externas.
  5. Firmar imágenes: Usar Docker Content Trust para verificar la integridad.
# Crear usuario no root
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

# Cambiar propiedad de archivos
COPY --chown=appuser:appgroup . .

# Cambiar a usuario seguro
USER appuser

# En CMD/ENTRYPOINT, no usar shell que puede permitir injection
ENTRYPOINT ["java", "-jar", "app.jar"]
⚠️ Peligro: Nunca pongas contraseñas, tokens o claves privadas en el Dockerfile. Incluso en imágenes privadas, estos valores quedan grabados en el historial de capas y pueden ser extraídos.

CMD vs ENTRYPOINT - Configurando el Arranque

La elección entre CMD y ENTRYPOINT determina cómo se comporta el contenedor:

Instrucción Uso Ejemplo
CMD Comando por defecto, sobrescribible CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT Comando fijo, argumentos se appenden ENTRYPOINT ["python", "app.py"]
Ambas ENTRYPOINT fijo + CMD como args ENTRYPOINT ["python"] CMD ["app.py"]
# CMD: sobrescribible en docker run
CMD ["python", "app.py"]
# docker run mi-imagen python other.py  # sobrescribe CMD

# ENTRYPOINT: argumentos se añaden
ENTRYPOINT ["python", "app.py"]
# docker run mi-imagen --debug  # ejecuta: python app.py --debug

# Combinación: patrón profesional
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]

Buenas Prácticas Recap

Un Dockerfile bien escrito es aquel que es fácil de leer, rápido de construir, pequeño en tamaño y seguro por defecto.
  • ✅ Usar versiones específicas en lugar de :latest
  • ✅ Preferir imágenes oficiales y minimalistas
  • ✅ Combinar instrucciones RUN para reducir capas
  • ✅ Ordenar argumentos alfanuméricamente cuando sea posible
  • ✅ Usar .dockerignore para excluir archivos innecesarios
  • ✅ Documentar con comentarios significativos
  • ❌ Evitar instalar paquetes innecesarios
  • ❌ No usar ADD para archivos locales cuando COPY es suficiente
  • ❌ Evitar ejecutar como root
🧠 Quiz: Anatomía de un Dockerfile Profesional

¿Por qué es importante copiar primero el archivo package.json antes de copiar todo el código en un Dockerfile de Node.js?

  • A) Porque package.json debe estar en mayúsculas
  • B) Porque Docker puede cachear la capa de npm install cuando solo cambia el código
  • C) Porque npm requiere permisos especiales
  • D) No hay ninguna razón especial, es solo convención
Respuesta correcta: B. Al copiar package.json primero y ejecutar npm install antes de copiar el código fuente, Docker puede reutilizar el caché de esa capa cuando solo cambia el código. Esto acelera significativamente los builds incrementales.

Ejemplo Completo: Dockerfile Profesional

# syntax=docker/dockerfile:1

# Etapa 0: Dependencias (para caché)
FROM python:3.11-slim-bookworm AS deps
RUN pip install --no-cache-dir poetry
COPY pyproject.toml poetry.lock* ./
RUN poetry install --no-root --no-ansi --no-interaction

# Etapa 1: Builder
FROM deps AS builder
COPY . .
RUN poetry build

# Etapa 2: Producción
FROM python:3.11-slim-bookworm AS production

# Variables de ambiente
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# Usuario no root
RUN groupadd --gid 1001 appgroup \
    && useradd --uid 1001 --gid appgroup --shell /bin/bash appuser

WORKDIR /app

# Copiar solo lo necesario
COPY --from=builder /app/dist ./dist
COPY --from=deps ${PYTHONPATH} ${PYTHONPATH}

# Archivos estáticos
COPY --chown=appuser:appgroup ./static ./static
COPY --chown=appuser:appgroup ./templates ./templates

USER appuser

EXPOSE 8000

# Usar formulario exec de CMD
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
📌 Recuerda: Este Dockerfile demonstra las mejores prácticas: multi-stage build para reducir tamaño, usuario no root, variables de ambiente seguras, y estructura optimizada para caché. Adáptalo a las necesidades específicas de tu proyecto.

Conclusión

Un Dockerfile profesional no es solo un archivo que funciona; es un documento que demuestra conocimiento técnico, pensamiento de seguridad y prácticas de ingeniería de software. La anatomía que hemos analizado —desde la selección de la imagen base hasta la configuración del usuario de ejecución— forma el esqueleto de contenedores robustos y mantenibles.

En la próxima lección exploraremos técnicas avanzadas como BuildKit, Build Arguments condicionales y patrones de construcción complejos que levarán tus Dockerfiles al siguiente nivel.