¿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
-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 . .
Optimización de Capas y 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.
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"]
Seguridad en el Dockerfile
Un Dockerfile profesional prioriza la seguridad en cada capa:
- Usar usuarios no root: Crear un usuario específico y usarlo para ejecutar la aplicación.
- Minimizar la superficie de ataque: Usar imágenes base mínimas (alpine, slim).
- Escanear vulnerabilidades: Integrar herramientas como Trivy o Snyk en el pipeline.
- No almacenar secretos: Usar BuildKit secrets o variables de entorno externas.
- 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"]
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
.dockerignorepara 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
¿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
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"]
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.