Creación de Dockerfiles: Guía Completa con Mejores Prácticas

Los Dockerfiles son el plano para crear imágenes Docker, definiendo todo desde el sistema operativo base hasta las dependencias de la aplicación y configuraciones de tiempo de ejecución. Esta guía completa te enseña cómo escribir Dockerfiles eficientes, seguros y listos para producción para cualquier aplicación.

Tabla de Contenidos

Introducción a los Dockerfiles

Un Dockerfile es un documento de texto que contiene instrucciones para construir una imagen Docker. Cada instrucción crea una capa en la imagen final, y Docker almacena en caché estas capas para acelerar construcciones subsecuentes. Comprender cómo escribir Dockerfiles eficientes es crucial para crear imágenes de contenedor optimizadas, seguras y mantenibles.

Por Qué Importan los Dockerfiles

  • Reproducibilidad: El mismo Dockerfile produce imágenes idénticas
  • Control de Versiones: Rastrea cambios de imágenes como código fuente
  • Automatización: Integra con pipelines CI/CD
  • Documentación: Sirve como documentación de infraestructura
  • Portabilidad: Construye una vez, ejecuta en cualquier lugar

Requisitos Previos

Antes de crear Dockerfiles, asegúrate de tener:

  • Docker Engine instalado y ejecutándose
  • Comprensión básica de conceptos de Docker
  • Conocimiento de las dependencias de tu aplicación
  • Editor de texto para escribir Dockerfiles
  • Acceso a terminal para construir imágenes

Verifica la instalación de Docker:

docker --version
docker info

Conceptos Básicos de Dockerfile

Estructura Básica

Un Dockerfile consiste en instrucciones (en mayúsculas) seguidas de argumentos:

# Comment
INSTRUCTION arguments

Creando Tu Primer Dockerfile

Crea un archivo llamado Dockerfile (sin extensión):

mkdir my-docker-app
cd my-docker-app
nano Dockerfile

Ejemplo simple:

# Use official base image
FROM ubuntu:22.04

# Set working directory
WORKDIR /app

# Copy application files
COPY app.py .

# Install dependencies
RUN apt-get update && apt-get install -y python3

# Define command to run
CMD ["python3", "app.py"]

Nomenclatura de Archivos

  • Nombre estándar: Dockerfile (recomendado)
  • Nombres personalizados: Dockerfile.dev, Dockerfile.prod
  • Construir con nombre personalizado: docker build -f Dockerfile.dev .

Instrucciones Esenciales de Dockerfile

FROM - Imagen Base

Especifica la imagen padre para tu construcción:

# Official image
FROM ubuntu:22.04

# Specific version (recommended)
FROM node:18.17.0-alpine

# Multiple stages
FROM node:18 AS builder
FROM nginx:alpine AS production

Mejores Prácticas:

  • Siempre especifica etiquetas de versión (evita latest)
  • Usa imágenes oficiales cuando sea posible
  • Prefiere imágenes basadas en Alpine para menor tamaño

LABEL - Metadatos

Agrega metadatos a las imágenes:

LABEL maintainer="[email protected]"
LABEL version="1.0"
LABEL description="Production web application"
LABEL org.opencontainers.image.source="https://github.com/user/repo"

WORKDIR - Directorio de Trabajo

Establece el directorio de trabajo para instrucciones subsecuentes:

# Set working directory
WORKDIR /app

# Creates directory if it doesn't exist
WORKDIR /var/www/html

# Relative paths work too
WORKDIR /app
WORKDIR src  # Now in /app/src

Mejor Práctica: Usa rutas absolutas y establece WORKDIR antes de COPY/ADD.

COPY vs ADD

Copia archivos del contexto de construcción a la imagen:

# COPY - Simple file copying (preferred)
COPY package.json .
COPY src/ /app/src/
COPY --chown=appuser:appuser app.py /app/

# ADD - Advanced features (use sparingly)
ADD https://example.com/file.tar.gz /tmp/  # Downloads URL
ADD archive.tar.gz /app/  # Auto-extracts archives

Mejor Práctica: Usa COPY a menos que necesites las características especiales de ADD.

RUN - Ejecutar Comandos

Ejecuta comandos durante la construcción de la imagen:

# Shell form (runs in /bin/sh -c)
RUN apt-get update && apt-get install -y curl

# Exec form (recommended)
RUN ["/bin/bash", "-c", "echo hello"]

# Multiple commands (chain with &&)
RUN apt-get update && \
    apt-get install -y \
        python3 \
        python3-pip \
        curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Install Python packages
RUN pip install --no-cache-dir -r requirements.txt

Mejores Prácticas:

  • Encadena comandos con && para reducir capas
  • Limpia en el mismo comando RUN
  • Usa --no-cache-dir para instalaciones de pip
  • Elimina cachés del gestor de paquetes

ENV - Variables de Entorno

Establece variables de entorno:

# Set single variable
ENV NODE_ENV=production

# Set multiple variables
ENV APP_HOME=/app \
    APP_USER=appuser \
    APP_PORT=3000

# Use in subsequent commands
ENV PATH="/app/bin:${PATH}"

ARG - Argumentos de Construcción

Define variables de tiempo de construcción:

# Define argument with default
ARG NODE_VERSION=18
ARG BUILD_DATE

# Use in FROM
FROM node:${NODE_VERSION}-alpine

# Use in RUN
RUN echo "Built on ${BUILD_DATE}"

# ARG vs ENV
ARG BUILD_ENV=dev
ENV RUNTIME_ENV=${BUILD_ENV}  # Convert ARG to ENV

Construye con argumentos:

docker build --build-arg NODE_VERSION=20 --build-arg BUILD_DATE=$(date -u +"%Y-%m-%d") .

EXPOSE - Documentar Puertos

Documenta qué puertos escucha el contenedor:

# Single port
EXPOSE 8080

# Multiple ports
EXPOSE 80 443

# With protocol
EXPOSE 8080/tcp
EXPOSE 53/udp

Nota: EXPOSE es solo documentación. Usa el flag -p al ejecutar el contenedor.

USER - Establecer Usuario

Especifica qué usuario ejecuta el contenedor:

# Create user and switch
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser

# Switch to user by UID
USER 1000

# Switch back to root if needed
USER root
RUN apt-get install -y something
USER appuser

Mejor Práctica: Siempre ejecuta contenedores como usuario no root.

VOLUME - Puntos de Montaje

Crea puntos de montaje:

# Define volumes
VOLUME /data
VOLUME ["/var/log", "/var/db"]

Nota: No puedes especificar ruta del host en Dockerfile. Usa el flag -v al ejecutar.

CMD vs ENTRYPOINT

Define el comando predeterminado del contenedor:

# CMD - Can be overridden
CMD ["nginx", "-g", "daemon off;"]
CMD ["python", "app.py"]
CMD node server.js  # Shell form

# ENTRYPOINT - Main executable
ENTRYPOINT ["python", "app.py"]

# ENTRYPOINT + CMD (arguments)
ENTRYPOINT ["python"]
CMD ["app.py"]
# Override: docker run image script.py

# Use both for flexibility
ENTRYPOINT ["./docker-entrypoint.sh"]
CMD ["start"]

Mejores Prácticas:

  • Usa ENTRYPOINT para el ejecutable principal
  • Usa CMD para argumentos predeterminados
  • Prefiere la forma exec sobre la forma shell

HEALTHCHECK

Define verificación de salud del contenedor:

# HTTP health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

# Simple health check
HEALTHCHECK CMD pg_isready -U postgres || exit 1

# Disable inherited health check
HEALTHCHECK NONE

ONBUILD

Agrega instrucciones de activación:

# In base image
ONBUILD COPY package.json /app/
ONBUILD RUN npm install

# Triggers when image is used as base
FROM my-base-image  # Executes ONBUILD instructions

Construcción de Imágenes Docker

Comando de Construcción Básico

# Build from current directory
docker build -t my-app:latest .

# Build from different directory
docker build -t my-app:latest /path/to/context

# Build with custom Dockerfile
docker build -t my-app:latest -f Dockerfile.prod .

Contexto de Construcción

El contexto de construcción es el conjunto de archivos en la RUTA o URL especificada:

# Current directory
docker build .

# Specific directory
docker build /path/to/context

# Git repository
docker build https://github.com/user/repo.git#branch

Etiquetado de Imágenes

# Single tag
docker build -t my-app:1.0 .

# Multiple tags
docker build -t my-app:1.0 -t my-app:latest .

# With registry
docker build -t registry.example.com/my-app:1.0 .

Argumentos de Construcción

# Pass build arguments
docker build --build-arg ENV=production --build-arg VERSION=1.0 .

# Multiple arguments from file
docker build --build-arg-file build-args.txt .

Archivo .dockerignore

Excluye archivos del contexto de construcción:

# .dockerignore
.git
.gitignore
.env
node_modules
npm-debug.log
Dockerfile
.dockerignore
README.md
*.md
.vscode
.idea

Opciones de Construcción

# No cache
docker build --no-cache -t my-app:latest .

# Pull latest base image
docker build --pull -t my-app:latest .

# Specify target stage
docker build --target production -t my-app:latest .

# Set memory limit
docker build --memory 2g -t my-app:latest .

# Squash layers (experimental)
docker build --squash -t my-app:latest .

Construcciones Multi-Etapa

Las construcciones multi-etapa crean imágenes de producción optimizadas al separar entornos de construcción y tiempo de ejecución:

Construcción Multi-Etapa Básica

# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
USER node
CMD ["node", "dist/server.js"]

Múltiples Etapas

# Dependencies stage
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Test stage
FROM builder AS tester
RUN npm run test

# Production stage
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Construye etapa específica:

# Build and test
docker build --target tester -t my-app:test .

# Build production
docker build --target production -t my-app:latest .

Copiar Desde Imágenes Externas

# Copy from specific image
FROM alpine:latest
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

Ejemplos del Mundo Real

Aplicación Node.js

# Multi-stage Node.js app
FROM node:18-alpine AS builder

WORKDIR /app

# Copy dependency files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && \
    npm cache clean --force

# Copy source code
COPY . .

# Build application
RUN npm run build

# Production stage
FROM node:18-alpine

# Add non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy built files and dependencies
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./

# Set environment
ENV NODE_ENV=production

# Switch to non-root user
USER nodejs

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD node healthcheck.js

# Start application
CMD ["node", "dist/server.js"]

Aplicación Python Flask

FROM python:3.11-slim AS builder

WORKDIR /app

# Install system dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc && \
    rm -rf /var/lib/apt/lists/*

# Copy requirements
COPY requirements.txt .

# Install Python dependencies
RUN pip install --user --no-cache-dir -r requirements.txt

# Production stage
FROM python:3.11-slim

WORKDIR /app

# Copy dependencies from builder
COPY --from=builder /root/.local /root/.local

# Copy application
COPY . .

# Create non-root user
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app

# Update PATH
ENV PATH=/root/.local/bin:$PATH

# Switch to non-root user
USER appuser

# Expose port
EXPOSE 5000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD python -c "import requests; requests.get('http://localhost:5000/health', timeout=2)"

# Run application
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]

Aplicación Go

# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy source code
COPY . .

# Build binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Production stage
FROM alpine:latest

# Install ca-certificates for HTTPS
RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy binary from builder
COPY --from=builder /app/main .

# Expose port
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost:8080/health || exit 1

# Run binary
CMD ["./main"]

Aplicación Java Spring Boot

# Build stage
FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /app

# Copy pom.xml
COPY pom.xml .

# Download dependencies
RUN mvn dependency:go-offline

# Copy source code
COPY src ./src

# Build application
RUN mvn clean package -DskipTests

# Production stage
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

# Create non-root user
RUN addgroup -S spring && adduser -S spring -G spring

# Copy JAR from builder
COPY --from=builder /app/target/*.jar app.jar

# Change ownership
RUN chown spring:spring app.jar

# Switch to non-root user
USER spring

# Expose port
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost:8080/actuator/health || exit 1

# Run application
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Sitio Estático con Nginx

# Build stage (optional, for building static assets)
FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine

# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf

# Copy static files
COPY --from=builder /app/dist /usr/share/nginx/html

# Create non-root user (nginx already exists)
RUN chown -R nginx:nginx /usr/share/nginx/html && \
    chown -R nginx:nginx /var/cache/nginx && \
    chown -R nginx:nginx /var/log/nginx && \
    chown -R nginx:nginx /etc/nginx/conf.d

RUN touch /var/run/nginx.pid && \
    chown -R nginx:nginx /var/run/nginx.pid

# Switch to non-root user
USER nginx

# Expose port
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost:8080 || exit 1

# Start nginx
CMD ["nginx", "-g", "daemon off;"]

Técnicas de Optimización

Caché de Capas

# Bad - Changes to code invalidate all layers
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install

# Good - Dependencies cached separately
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

Minimizar Capas

# Bad - Multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get clean

# Good - Single layer
RUN apt-get update && \
    apt-get install -y \
        curl \
        git && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Usar .dockerignore

node_modules
.git
.env
*.log
.DS_Store
coverage
.vscode

Elegir Imágenes Base Más Pequeñas

# Large (900MB+)
FROM ubuntu:22.04

# Medium (200MB)
FROM node:18

# Small (50MB)
FROM node:18-slim

# Smallest (40MB)
FROM node:18-alpine

Eliminar Archivos Innecesarios

RUN apt-get update && \
    apt-get install -y build-essential && \
    # ... compile something ... && \
    apt-get remove -y build-essential && \
    apt-get autoremove -y && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Mejores Prácticas de Seguridad

Ejecutar como Usuario No Root

# Create and use non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser

# Or with Alpine
RUN addgroup -S appuser && adduser -S appuser -G appuser
USER appuser

Escanear en Busca de Vulnerabilidades

# Using Docker Scout
docker scout cves my-app:latest

# Using Trivy
trivy image my-app:latest

# Using Snyk
snyk container test my-app:latest

Usar Etiquetas Específicas

# Bad - unpredictable
FROM node:latest

# Good - predictable and secure
FROM node:18.17.0-alpine3.18

Minimizar Superficie de Ataque

# Use distroless images for minimal attack surface
FROM gcr.io/distroless/nodejs:18

# Or minimal Alpine
FROM alpine:3.18

No Incluir Secretos

# Bad - secrets in image
ENV API_KEY=secret123

# Good - pass at runtime
# docker run -e API_KEY=secret123 my-app

Usar COPY en Lugar de ADD

# Preferred
COPY app.py .

# Avoid unless needed
ADD archive.tar.gz /app/

Verificar Descargas

# Verify checksum
RUN curl -fsSL https://example.com/file -o /tmp/file && \
    echo "expected_hash /tmp/file" | sha256sum -c -

Solución de Problemas

La Construcción Falla en el Comando RUN

# Show build output
docker build --progress=plain .

# Debug specific layer
docker run -it <layer_id> sh

Tamaño de Imagen Demasiado Grande

# Check layer sizes
docker history my-app:latest

# Analyze with dive
dive my-app:latest

El Caché No Funciona

# Force rebuild without cache
docker build --no-cache .

# Check what changed
docker build --progress=plain .

Errores de Permiso Denegado

# Ensure proper ownership
COPY --chown=appuser:appuser app.py /app/

# Or fix after copy
RUN chown -R appuser:appuser /app

Conclusión

Escribir Dockerfiles efectivos es fundamental para el éxito de la contenedorización. Esta guía cubrió todo desde la sintaxis básica hasta construcciones multi-etapa avanzadas y prácticas de seguridad.

Conclusiones Clave

  • Optimización de Capas: Ordena las instrucciones de menos a más frecuentemente cambiantes
  • Construcciones Multi-Etapa: Separa entornos de construcción y tiempo de ejecución
  • Seguridad Primero: Siempre ejecuta como no root, usa etiquetas específicas, escanea vulnerabilidades
  • El Tamaño Importa: Usa imágenes Alpine, minimiza capas, aprovecha .dockerignore
  • Mejores Prácticas: Sigue convenciones, documenta con LABEL, implementa verificaciones de salud

Lista de Verificación de Dockerfile

  • Usar etiquetas de versión específicas para imágenes base
  • Implementar construcciones multi-etapa para lenguajes compilados
  • Ejecutar contenedores como usuario no root
  • Agregar instrucción de verificación de salud
  • Crear archivo .dockerignore
  • Minimizar número de capas
  • Limpiar en el mismo comando RUN
  • Usar COPY en lugar de ADD
  • Establecer WORKDIR apropiado
  • Documentar puertos expuestos
  • Agregar etiquetas de metadatos
  • Implementar logging apropiado

Próximos Pasos

  1. Practica: Construye Dockerfiles para tus aplicaciones
  2. Optimiza: Usa dive para analizar y reducir el tamaño de la imagen
  3. Asegura: Implementa escaneo de vulnerabilidades en CI/CD
  4. Documenta: Agrega metadatos LABEL completos
  5. Prueba: Crea etapas de prueba en construcciones multi-etapa
  6. Automatiza: Integra construcciones en pipelines CI/CD
  7. Monitorea: Rastrea tamaños de imagen y tiempos de construcción

Con estas mejores prácticas de Dockerfile, estás equipado para crear imágenes de contenedor eficientes, seguras y mantenibles para cualquier stack de aplicaciones.