Cambiar tema

Optimización de Dockerfile en la práctica: 5 trucos para reducir el tamaño de la imagen un 80%

Easton editorial illustration: observability control panel

La barra de progreso en la terminal llevaba 30 minutos atascada en «Pushing to registry».

3,2 GB.

Mi primera imagen Docker de una app Node.js, siguiendo tutoriales paso a paso. El build salió bien, pero el tamaño me pilló por sorpresa. A la mañana siguiente, un compañero preguntó en Slack: «¿Tu imagen metió todo el sistema operativo? El disco de mi portátil casi se llena».

¿La imagen base Ubuntu? ¿node_modules? ¿Las herramientas de compilación? Da igual: un API service simple terminó con una imagen 50 veces más grande que el código del proyecto. Tras revisar la documentación oficial y las mejores prácticas de Docker, bajé ese monstruo de 3,2 GB a 180 MB. Un 94% menos.

180 MB
Tamaño optimizado
De 3,2 GB a 180 MB (94% menos)

En este artículo repaso los 5 trucos que más me sirvieron. No solo el cómo, sino el porqué funciona — entender el principio importa más que memorizar comandos.

Primero entiende por qué las imágenes Docker son tan grandes

Antes de los trucos, conviene ver de dónde viene el problema.

Las imágenes Docker son por capas. Cada instrucción RUN, COPY o ADD crea una capa nueva del sistema de archivos. Esas capas se apilan y forman la imagen final. La clave: cada capa solo añade, no elimina.

Ejemplo. En el Dockerfile escribes:

RUN apt-get update
RUN apt-get install -y build-essential
RUN rm -rf /var/lib/apt/lists/*

En la superficie, la última línea borra la caché de apt. Pero en realidad esa caché ya quedó guardada en la segunda capa. La tercera solo marca «estos archivos ya no están», pero los datos siguen ocupando espacio en la imagen.

Es como hacer una foto cada vez que ordenas una habitación al mudarte: aunque al final tires la basura, las fotos con basura viajan contigo. Un poco absurdo, pero así funciona el copy-on-write de Docker.

Con docker history en una imagen sin optimizar verás que muchas capas suman mucho más de lo que realmente necesitas.

Otro detalle fácil de pasar por alto: la imagen base fija el piso de tamaño. ubuntu:20.04 pesa 72 MB por sí sola; node:16 llega a 1,09 GB — está basada en Debian completo, con herramientas del sistema que quizá nunca uses.

Con eso claro, la estrategia es obvia: menos capas, imágenes base más ligeras e instalar y limpiar en la misma capa.

Truco 1: elegir bien la imagen base desde el inicio

La imagen base es como la ubicación al comprar una casa. Si eliges mal, el techo de optimización ya está puesto.

Comparación rápida:

  • node:16 → 1,09 GB
  • node:16-slim → 240 MB
  • node:16-alpine → 174 MB
  • alpine:latest → 5,6 MB

La diferencia salta a la vista. Al pasar de node:16 a node:16-alpine, la imagen bajó de 1,2 GB a 400 MB sin tocar código.

¿Qué es Alpine Linux?

Una distribución Linux pensada para contenedores. Minimalista: solo lo esencial. Usa musl libc en lugar de glibc estándar y apk en lugar de apt.

Ventajas claras:

  • Tamaño mínimo (5 MB frente a 72 MB de Ubuntu)
  • Seguridad (superficie de ataque reducida)
  • Arranque rápido

Pero hay trampas.

La trampa de compatibilidad de Alpine

Con musl libc, algunos binarios precompilados pueden fallar. Me pasó una vez: el proyecto dependía de un módulo nativo de Node.js en C++ y en Alpine saltó «library not found». Media tarde hasta ver que era un tema de libc.

Recomendación práctica:

  1. Prueba primero la variante Alpine (sufijo -alpine)
  2. Si hay problemas de compatibilidad, usa -slim (Debian recortado)
  3. Solo como último recurso la imagen estándar; en la práctica es poco frecuente

El cambio en código es trivial:

# Antes
FROM node:16

# Después
FROM node:16-alpine

Una línea. 800 MB menos.

Cómo comprobar el efecto

Tras el build:

docker images your-image-name

Mira la columna SIZE. Si sigue grande, el problema no es solo la imagen base — sigue leyendo.

Truco 2: fusionar instrucciones RUN para reducir capas

Fácil de entender, pero a menudo se ignora en la práctica.

Como vimos, cada RUN crea una capa. Y lo importante: borrar archivos solo funciona dentro de la misma capa.

Mal ejemplo:

# Mal (crea 3 capas)
RUN apt-get update
RUN apt-get install -y python3 gcc
RUN rm -rf /var/lib/apt/lists/*

Así, la caché de apt (decenas de MB) queda en la segunda capa. El borrado de la tercera solo marca que «ya no están», pero los datos siguen en la imagen.

Lo correcto es encadenar con &&:

# Bien (solo 1 capa)
RUN apt-get update && \
    apt-get install -y python3 gcc && \
    rm -rf /var/lib/apt/lists/*

Instalación y limpieza en la misma capa: el borrado es real.

Para qué sirve la barra invertida

El \ permite partir el comando en varias líneas sin perder legibilidad. Sin él, todo en una línea es un desastre.

Cuándo fusionar y cuándo no

No todo RUN debe fusionarse. Regla simple:

  • Fusionar: instalar + limpiar, descargar + descomprimir + borrar el archivo
  • No fusionar: pasos sin relación lógica o pasos que cambian a menudo (rompen la caché de build)

Por ejemplo:

# Buen reparto de capas
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
RUN npm install
RUN npm run build

Dependencias del sistema en una capa; npm install en otra (package.json cambia seguido); build en otra. Si cambias package.json, la capa de dependencias del sistema puede reutilizar caché.

En un proyecto mío: de 12 instrucciones RUN a 4 tras optimizar; imagen de 520 MB a 320 MB.

Truco 3: build multietapa — solo lo imprescindible

El build multietapa (Multi-stage Build) es la herramienta más efectiva para adelgazar imágenes Docker. Sin discusión.

La idea es simple: separar build y ejecución.

Compilar un programa Go requiere todo el toolchain (cientos de MB), pero el binario final puede pesar 10 MB. Meter el toolchain en la imagen final es puro desperdicio.

El build multietapa lo resuelve: varias etapas en un Dockerfile — la primera compila, la segunda solo copia el artefacto.

Ejemplo con Node.js:

# === Etapa de build ===
FROM node:16-alpine AS builder
WORKDIR /app

# Copiar archivos de dependencias
COPY package*.json ./
RUN npm install

# Copiar código y compilar
COPY . .
RUN npm run build

# === Etapa de runtime ===
FROM node:16-alpine
WORKDIR /app

# Solo copiar lo necesario
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

EXPOSE 3000
CMD ["node", "dist/index.js"]

Fíjate en COPY --from=builder: copia desde la primera etapa (builder) a la segunda. La imagen final solo contiene la segunda etapa; los intermedios de la primera desaparecen.

Cuándo usar build multietapa

Escenarios típicos:

  1. Lenguajes compilados: Go, Rust, C++, etc.
  2. Frontend: TypeScript, empaquetado con Webpack
  3. Herramientas de build: por ejemplo Python con gcc para ciertas librerías

Mi proyecto Node.js era TypeScript → JavaScript. Código fuente 400 MB (incluidos @types en node_modules); dist compilado 2 MB. Con multietapa: de 400 MB a 220 MB.

Trampa común

Algunos hacen npm install también en runtime «por si acaso». Mal: entran devDependencies y se desperdicia espacio.

Lo correcto: en build npm install (con dev, para compilar); luego copiar node_modules a runtime. O más fino:

# Etapa de build
RUN npm install

# Etapa de runtime
RUN npm install --production

Solo dependencias de producción: 30-40% menos.

Al principio el multietapa puede marear, pero luego parece elegante. Como hacer la maleta: en casa (build) desparramas todo; en el avión (runtime) solo llevas lo indispensable.

Truco 4: excluir archivos inútiles con .dockerignore

.dockerignore funciona como .gitignore, pero muchos lo olvidan.

Con COPY . . en el Dockerfile, Docker envía todo el directorio al daemon como contexto de build. Si hay cientos de MB en node_modules, historial .git, tests o logs, todo viaja.

Aunque no entren en la imagen final, el build se ralentiza y es fácil empaquetar cosas que no deben (por ejemplo claves en .env).

Solución: .dockerignore en la raíz del proyecto.

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
README.md
.vscode
.idea
*.md
.DS_Store
coverage/
.pytest_cache/
__pycache__/
*.pyc
dist-local/

Principios clave

Incluye:

  1. Directorios de dependencias ya presentes: node_modules, vendor, target (se reinstalan en el build)
  2. Config de herramientas de desarrollo: .vscode, .idea, .editorconfig
  3. Git: .git, .gitignore (.git suele pesar decenas de MB)
  4. Documentación: README, CHANGELOG, docs/
  5. Datos sensibles: .env, credentials.json, *.pem

En un proyecto olvidé excluir .git y cada build transfería 500 MB de historial. Con .dockerignore, el build pasó de 2 minutos a 30 segundos y la imagen también bajó.

Truco práctico

Si no sabes qué se copia, construye una vez y entra al contenedor:

docker run --rm -it your-image sh
ls -lah

Si ves archivos que no deberían estar, añádelos a .dockerignore.

Parece trivial, pero el efecto es inmediato. En frontend, dist + node_modules + .cache pueden superar fácilmente 1 GB.

Truco 5: limpiar la caché de los gestores de paquetes

npm, pip, apt, apk dejan caché tras instalar. En desarrollo local acelera; en la imagen Docker solo ocupa espacio.

Muchos saben que hay que limpiar, pero lo hacen mal.

Hay que limpiar en la misma instrucción RUN

Repito el punto: limpieza e instalación en la misma capa.

# ❌ Limpieza inútil
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*  # esto no sirve

# ✅ Limpieza efectiva
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

Por gestor:

Node.js (npm/yarn)

# npm - forma clásica
RUN npm install && \
    npm cache clean --force

# npm - más simple (sin caché)
RUN npm install --no-cache

# yarn
RUN yarn install && \
    yarn cache clean

Python (pip)

# Directo: sin generar caché
RUN pip install --no-cache-dir -r requirements.txt

# O limpiar después
RUN pip install -r requirements.txt && \
    rm -rf ~/.cache/pip

Alpine (apk)

# Opción cómoda de apk
RUN apk add --no-cache package-name

# O limpieza manual
RUN apk add package-name && \
    rm -rf /var/cache/apk/*

Debian/Ubuntu (apt)

RUN apt-get update && \
    apt-get install -y package-name && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Datos reales

Probé un proyecto Python con las mismas dependencias:

  • Sin limpiar caché: 450 MB
  • Con limpieza: 320 MB
  • Con --no-cache-dir: 310 MB (lo más limpio)

¡140 MB de diferencia por un parámetro!

Desarrollo vs producción

En producción usa --production o --no-dev para instalar solo lo necesario. Las devDependencies suelen ser el 30-50% del volumen.

# Node.js: solo producción
RUN npm install --production

# Python: requirements separados
RUN pip install --no-cache-dir -r requirements-prod.txt

Estos 5 trucos combinados se multiplican, no se suman. Mi proyecto de 3,2 GB llegó a 180 MB aplicando todos.

Caso completo: optimización de una app Node.js

Teoría aparte, un caso real: la evolución del Dockerfile de un API Express.

Antes (1,2 GB)

FROM node:16
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]

Directo, pero enorme.

Paso 1: Alpine (→ 400 MB, -67%)

FROM node:16-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]

Una línea. 800 MB menos.

Paso 2: .dockerignore (→ 380 MB, -5%)

Crear .dockerignore:

node_modules
.git
*.md
.env
coverage

Parece poco en tamaño, pero el build fue mucho más rápido.

Paso 3: build multietapa (→ 220 MB, -42%)

# Etapa de build
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Etapa de runtime
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

El salto más grande: fuera todos los intermedios del build.

Paso 4: solo producción + limpiar caché (→ 180 MB, -18%)

# Etapa de build
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Etapa de runtime
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production --no-cache && \
    npm cache clean --force
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]

Versión final: 180 MB, 85% menos que los 1,2 GB iniciales.

Resumen del camino de optimización

1,2 GB  (node:16 original)
  ↓ Alpine
400 MB  (-67%)
  ↓ .dockerignore
380 MB  (-5%)
  ↓ build multietapa
220 MB  (-42%)
  ↓ producción + limpieza
180 MB  (-18%)
────────────────
Total: 85% menos

¿Qué trucos rinden más?

De este caso:

  1. Alpine: efecto inmediato, fácil de aplicar
  2. Build multietapa: el mayor impacto, con algo de curva de aprendizaje
  3. Limpieza y deps de producción: detalle que suma

Si el tiempo apremia, prioriza los dos primeros.

Conclusión

Repaso de los 5 trucos:

  1. Imagen Alpine — reduce desde el origen
  2. Fusionar RUN — instalar y limpiar en la misma capa
  3. Build multietapa — solo lo necesario en runtime
  4. .dockerignore — fuera archivos inútiles y datos sensibles
  5. Limpiar caché — parámetros tipo --no-cache

No son trucos aislados: juntos rinden más. Alpine + multietapa cubre el 80% del volumen; el 20% restante es limpieza y exclusión.

Acción inmediata

No esperes al problema. En un proyecto existente prueba estos 5 pasos:

  1. ¿Puedes pasar a Alpine? (probablemente sí)
  2. ¿Hay instalación y limpieza en RUN separados? (fusiónalos)
  3. Añade build multietapa (imprescindible en proyectos compilados)
  4. Crea .dockerignore
  5. Añade --no-cache a los gestores de paquetes

Tras el build, docker images y compara cuánto ahorras.

Siguiente nivel

Si quieres profundizar:

  • Caché montada de Docker BuildKit
  • Imágenes Distroless (Google; aún más pequeñas que Alpine)
  • Escaneo de seguridad (Trivy, Grype)

Optimizar el Dockerfile es un hábito, no un evento único. Mira el tamaño en cada build y el volumen se mantendrá bajo control.

Que tus imágenes pesen cada vez menos.

Flujo completo de optimización de Dockerfile

5 trucos para reducir el tamaño de la imagen un 80%, de 3,2 GB a 180 MB (94% menos)

⏱️ Estimated time: 1 hr

  1. 1

    Step 1: Truco 1: usar imagen base Alpine

    Ventajas de Alpine:
    • Tamaño mínimo (solo 5 MB; Ubuntu tiene 200 MB)
    • Mayor seguridad (superficie de ataque reducida)
    • Gestor de paquetes simple (apk)
    • Adecuada para producción

    Cómo usar Alpine:
    • Cambia FROM ubuntu:20.04 por FROM alpine:latest
    • Usa apk en lugar de apt-get: apk add --no-cache nodejs npm
    • La imagen pasa de 200 MB a 5 MB (95% menos)
  2. 2

    Step 2: Trucos 2-3: fusionar RUN y build multietapa

    Fusionar instrucciones RUN:
    • Combina varias instrucciones RUN en una para reducir capas
    • Usa && para encadenar comandos y \ para saltos de línea
    • Limpia la caché al final:
    RUN apt-get update && \
    apt-get install -y nodejs npm && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

    Build multietapa:
    • Primera etapa: imagen completa de build para compilar
    • Segunda etapa: imagen mínima de runtime
    • Solo conserva los archivos necesarios en runtime; las herramientas de compilación no van al paquete final
    • El volumen de la imagen se reduce drásticamente
  3. 3

    Step 3: Trucos 4-5: configurar .dockerignore y limpiar caché

    Configurar .dockerignore:
    • Excluye node_modules, .git, .env, dist y otros archivos innecesarios
    • Reduce el contexto de build y acelera la construcción

    Limpiar caché:
    • Usa --no-cache: apk add --no-cache
    • Limpia caché de apt: apt-get clean && rm -rf /var/lib/apt/lists/*
    • Limpia caché de npm: npm cache clean --force
    • Reduce el tamaño de la imagen

FAQ

¿Cuáles son los 5 trucos de optimización de Dockerfile?
5 trucos de optimización:
1) Usar imagen base Alpine (de Ubuntu 200 MB a Alpine 5 MB, 95% menos)
2) Fusionar instrucciones RUN (menos capas, imagen más pequeña)
3) Build multietapa (solo archivos de runtime; herramientas de compilación fuera)
4) Configurar .dockerignore (excluir node_modules, .git, etc.)
5) Limpiar caché (apt-get clean, npm cache clean, etc.)

Resultados:
• Imagen Node.js de 3,2 GB a 180 MB (94% menos)
• De 1,2 GB a 180 MB (85% menos)
• Tiempo de despliegue de 30 minutos a unos minutos
• Volumen de imagen drásticamente reducido
¿Por qué Alpine es mejor como imagen base?
Ventajas de Alpine:
• Tamaño mínimo (solo 5 MB; Ubuntu tiene 200 MB)
• Mayor seguridad (superficie de ataque reducida)
• Gestor de paquetes simple (apk)
• Adecuada para producción

Cómo usar Alpine:
• Cambia FROM ubuntu:20.04 por FROM alpine:latest
• Usa apk en lugar de apt-get: apk add --no-cache nodejs npm
• La imagen pasa de 200 MB a 5 MB (95% menos)
¿Cómo fusionar instrucciones RUN?
Fusionar instrucciones RUN:
• Combina varias instrucciones RUN en una para reducir capas
• Usa && para encadenar comandos
• Usa \ para saltos de línea y legibilidad
• Limpia la caché al final:
RUN apt-get update && \
apt-get install -y nodejs npm && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

Así reduces capas, bajas el tamaño de la imagen y mejoras la eficiencia del build.
¿Cuáles son las mejores prácticas de optimización de Dockerfile?
Mejores prácticas:
• Usar imagen base Alpine
• Fusionar instrucciones RUN
• Usar build multietapa
• Configurar .dockerignore
• Limpiar caché
• Actualizar periódicamente la versión de la imagen base

Estos trucos no son aislados: combinados funcionan mejor. En mi experiencia, Alpine + build multietapa resuelve el 80% del problema de volumen; el 20% restante depende de la limpieza y la exclusión de archivos.

Optimizar el Dockerfile no es algo de una sola vez: es un proceso continuo. Revisa el tamaño de la imagen en cada build y conviértelo en hábito; el volumen se mantendrá bajo control.

11 min de lectura · Publicado el: 17 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog