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

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.
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 GBnode:16-slim→ 240 MBnode:16-alpine→ 174 MBalpine: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:
- Prueba primero la variante Alpine (sufijo
-alpine) - Si hay problemas de compatibilidad, usa
-slim(Debian recortado) - 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:
- Lenguajes compilados: Go, Rust, C++, etc.
- Frontend: TypeScript, empaquetado con Webpack
- 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:
- Directorios de dependencias ya presentes: node_modules, vendor, target (se reinstalan en el build)
- Config de herramientas de desarrollo: .vscode, .idea, .editorconfig
- Git: .git, .gitignore (
.gitsuele pesar decenas de MB) - Documentación: README, CHANGELOG, docs/
- 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:
- Alpine: efecto inmediato, fácil de aplicar
- Build multietapa: el mayor impacto, con algo de curva de aprendizaje
- Limpieza y deps de producción: detalle que suma
Si el tiempo apremia, prioriza los dos primeros.
Conclusión
Repaso de los 5 trucos:
- Imagen Alpine — reduce desde el origen
- Fusionar RUN — instalar y limpiar en la misma capa
- Build multietapa — solo lo necesario en runtime
- .dockerignore — fuera archivos inútiles y datos sensibles
- 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:
- ¿Puedes pasar a Alpine? (probablemente sí)
- ¿Hay instalación y limpieza en RUN separados? (fusiónalos)
- Añade build multietapa (imprescindible en proyectos compilados)
- Crea
.dockerignore - Añade
--no-cachea 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
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
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
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?
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?
• 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?
• 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?
• 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
Guía práctica de Docker
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Tutorial introductorio de Dockerfile: construye tu primera imagen Docker desde cero (con ejemplo)
Te enseñamos paso a paso a escribir un Dockerfile, con FROM, RUN, COPY y otras instrucciones clave, los errores típicos de principiantes y un caso práctico con Node.js. Al terminar, podrás escribir un Dockerfile para tu proyecto.
Parte 3 de 38
Siguiente
Builds multietapa de Docker en la práctica: imágenes Go/Java/Rust de GB a MB
Análisis profundo de los builds multietapa de Docker con casos reales: optimiza imágenes Go un 98%, Java un 86% y Rust un 99,4%. Incluye Dockerfiles completos y lecciones aprendidas.
Parte 5 de 38



Comentarios
Inicia sesión con GitHub para dejar un comentario