Crear un Dockerfile que funcione es solo el primer paso. Crear un Dockerfile optimizado, que genere imágenes pequeñas, seguras y rápidas de construir, es lo que distingue a un profesional. Aquí veremos las técnicas más importantes para lograrlo.
Cuando ejecutas `docker build`, el primer paso que realiza Docker es enviar todo el directorio del contexto de build (normalmente `.`) al demonio de Docker. Si en tu directorio tienes carpetas como `node_modules`, logs, archivos temporales o directorios `.git`, todos ellos serán enviados, ralentizando el build y, peor aún, podrían terminar dentro de tu imagen si usas un `COPY . .`.
El archivo .dockerignore
soluciona esto. Funciona exactamente igual que un `.gitignore`: especificas los archivos y carpetas que quieres que Docker ignore por completo.
Un .dockerignore
típico podría ser:
.git
.vscode
node_modules
npm-debug.log
Dockerfile
.dockerignore
README.md
Al usar este archivo, mantienes tu contexto de build limpio y evitas que archivos innecesarios o sensibles terminen en tu imagen.
Cada instrucción `RUN`, `COPY` y `ADD` en tu Dockerfile crea una nueva capa. Tener demasiadas capas puede hacer que la imagen sea más grande y lenta de descargar. Una práctica común es encadenar comandos `RUN` usando el operador `&&`.
No recomendado (crea 3 capas):
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
Recomendado (crea 1 capa):
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
Fíjate que también incluimos la limpieza de la caché de `apt` en la misma capa. Si lo hiciéramos en un `RUN` separado, los archivos de caché seguirían existiendo en una capa anterior, y el tamaño de la imagen no se reduciría.
Esta técnica es revolucionaria para aplicaciones que necesitan un proceso de compilación o transpilación (ej. Go, Java, C#, React, Angular, TypeScript).
El problema es que para compilar la aplicación, necesitas un entorno con todas las herramientas de desarrollo (el compilador, SDKs, dependencias de desarrollo), que puede ser muy pesado. Pero para ejecutar la aplicación, solo necesitas el binario o los archivos transpilados, sin todas esas herramientas.
Un multi-stage build te permite usar múltiples bloques `FROM` en un solo Dockerfile. Cada bloque es una "etapa" de construcción. Puedes copiar artefactos de una etapa a otra, descartando todo lo demás.
Go produce un único binario ejecutable. Veamos cómo empaquetarlo eficientemente.
# ----- Etapa 1: Build -----
# Usamos una imagen completa de Go para compilar la aplicación
FROM golang:1.19 AS builder
WORKDIR /app
COPY . .
# Compilamos la aplicación, deshabilitando CGO para crear un binario estático
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/main .
# ----- Etapa 2: Final -----
# Usamos una imagen base mínima, ya que no necesitamos Go para ejecutar el binario
FROM alpine:latest
WORKDIR /root/:
# Copiamos solo el binario compilado desde la etapa 'builder'
COPY --from=builder /app/main .
# Comando para ejecutar la aplicación
CMD ["./main"]
Análisis del proceso:
El resultado es una imagen final extremadamente pequeña que solo contiene el sistema operativo base Alpine y el binario ejecutable. Toda la imagen de Go, el código fuente y las herramientas de compilación se descartan por completo. Esta técnica puede reducir el tamaño de una imagen de cientos de MB a solo unos pocos MB.