Optimizar un Dockerfile significa reducir el tamaño final de la imagen y acelerar las reconstrucciones, y ambas cosas dependen de las mismas decisiones: el orden de las instrucciones, la separación en varias etapas y la elección de la imagen base. Docker cachea cada instrucción como una capa, y si una capa se invalida, se invalidan también todas las que siguen; por eso conviene copiar primero los archivos de dependencias, instalarlas, y recién al final copiar el código fuente, que es lo que cambia en cada commit. Una construcción multietapa (multi-stage build) compila en una etapa descartable y copia a la imagen final solo el artefacto resultante. El tamaño no es un detalle estético: es superficie de ataque y velocidad de despliegue.
Ver índice de contenidos
- 01Por qué el tamaño importa de verdad
- 02Cómo funciona la caché de capas
- 03El orden de las instrucciones: antes y después
- 04.dockerignore: lo que no debería entrar
- 05Construcciones multietapa
- 06Elegir la imagen base
- 07Combinar apt-get update e install
- 08Cómo medir el tamaño real
- 09Errores frecuentes
- 10Preguntas frecuentes
- 11Fuentes
Por qué el tamaño importa de verdad
Es fácil tratar el tamaño de una imagen Docker como un detalle cosmético: «total, el disco es barato». Pero cuando la imagen crece, empeoran dos cosas concretas. La primera es la velocidad de despliegue: cada actualización implica subir la imagen a un registro y bajarla en cada servidor o nodo, y una imagen de 900 MB tarda sensiblemente más que una de 90 MB. La segunda, más importante, es la superficie de ataque: todo lo que entra a la imagen final —un compilador, un gestor de paquetes, una shell— existe dentro del contenedor y, si alguien logra ejecutar código en él, puede usarlo para moverse o persistir. Una imagen mínima no es infalible, pero reduce lo disponible para quien ya entró.
Es superficie de ataque y velocidad de despliegue. Esta guía desarrolla un punto que Docker desde cero menciona en una sola línea: partir de una imagen base chica y copiar las dependencias antes que el código.
Cómo funciona la caché de capas
Cada instrucción de un Dockerfile —cada FROM, COPY, RUN— genera una capa: una unidad de solo lectura que Docker guarda y reutiliza si nada cambió. Al reconstruir, Docker recorre el archivo de arriba hacia abajo y, para cada instrucción, compara si puede reutilizar la capa de la construcción anterior o si tiene que ejecutarla de nuevo.
Acá está el punto que ordena todo lo demás: si una capa se invalida, se invalidan también todas las capas siguientes, sin importar si esas capas cambiaron o no. Docker no analiza si el resultado sería el mismo: apenas detecta un cambio, descarta la caché de ahí en adelante y ejecuta el resto desde cero.
La consecuencia práctica es una regla simple: las instrucciones que cambian con más frecuencia van lo más abajo posible. El código fuente cambia en cada commit; las dependencias —lo declarado en package.json, requirements.txt o go.mod— cambian mucho menos. Copiar el código antes que las dependencias mezcla lo volátil con lo estable, y cada cambio de una línea fuerza a reinstalar todo de nuevo.
El orden de las instrucciones: antes y después
El efecto se ve mejor con un ejemplo: una aplicación Node.js empaquetada con el orden más habitual entre quien recién empieza con Docker.
# Antes: código primero
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]
El problema está en COPY . ., en la segunda capa: copia todo el proyecto, incluido el código que cambia en cada commit. Como esa capa se invalida en cada cambio, arrastra en cascada las siguientes: instalación de dependencias, compilación y arranque. Una sola línea de código dispara una reinstalación completa de node_modules, aunque ninguna dependencia haya cambiado.
Reordenando para separar lo estable de lo volátil:
# Después: orden correcto
FROM node:20
WORKDIR /app
# 1. Dependencias primero
COPY package.json package-lock.json ./
RUN npm ci
# 2. Código al final
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
npm ci ahora solo se reejecuta si cambia package.json o package-lock.json. Un cambio de código sigue invalidando las capas desde COPY . . en adelante, pero ya no toca la instalación de dependencias, que en un proyecto grande suele ser la parte más lenta del build.
.dockerignore: lo que no debería entrar
Antes de ejecutar la primera instrucción, Docker arma el contexto de construcción: copia el contenido del directorio donde corrés docker build, incluidos los subdirectorios. Si ese directorio tiene un node_modules de cientos de megabytes, un .git con todo el historial, o archivos con secretos, todo eso se envía igual, aunque el Dockerfile no lo use.
Un archivo .dockerignore, en la raíz del proyecto, evita ese envío. Usa el mismo formato de patrones que un .gitignore, sin necesidad de reorganizar el repositorio.
node_modules
.git
.env
*.log
dist
Dockerfile
.dockerignore
El efecto es doble: el build es más rápido, porque hay menos para enviar, y baja el riesgo de copiar por accidente un archivo con credenciales dentro de una capa.
Construcciones multietapa
Una construcción multietapa (multi-stage build) es un Dockerfile con más de una instrucción FROM. Cada FROM abre una etapa nueva, y se le puede poner nombre con AS <nombre>. Es un solo archivo, no varios Dockerfiles: creer que hacen falta archivos separados por etapa es un error común al empezar.
Una etapa puede usarse solo para producir algo —compilar código, generar artefactos— y después descartarse por completo. Lo único que pasa a la siguiente es lo que se copia explícitamente con COPY --from=<etapa>. Retomando el ejemplo de Node, con una imagen distroless como base final:
# Etapa build: compila todo
FROM node:20 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Etapa final: solo runtime
FROM gcr.io/distroless/nodejs22-debian13
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["dist/server.js"]
La etapa build, con el compilador, las herramientas de npm y el código fuente completo, nunca llega a la imagen final: se usó, produjo dist y se descartó. Lo mismo aplica a lenguajes compilados: en Go o Rust se compila en una etapa con el toolchain completo y se copia solo el binario a una imagen final que puede no tener ni shell, incluida la opción extrema de partir de FROM scratch, una imagen vacía.
Elegir la imagen base
La imagen base con la que arranca la etapa final determina buena parte del tamaño y de la superficie de ataque que quedan expuestos. Las opciones más habituales son, de más pesada a más liviana, la imagen completa de Debian o Ubuntu, sus variantes slim, las basadas en Alpine y las imágenes distroless.
| Imagen base | Qué incluye | Cuándo conviene |
|---|---|---|
| Debian/Ubuntu completa | Sistema completo: paquetes, shell, utilidades | Desarrollo y depuración; no para producción |
Variante slim | Debian recortada: conserva apt y shell | Punto medio, si hace falta instalar algo o depurar |
| Alpine | Minimalista, basada en musl, con apk y shell | Liviana, si la app no depende de glibc |
| Distroless | Solo la app y sus dependencias de ejecución; sin paquetes ni shell | Producción, superficie de ataque mínima |
Las cifras concretas ilustran la diferencia. Según el propio proyecto distroless de Google, consultado en julio de 2026, su variante más chica (gcr.io/distroless/static-debian13) pesa alrededor de 2 MiB, cerca de la mitad que Alpine (~5 MiB) y menos del 2 % de una Debian completa (~124 MB). Son cifras de una variante puntual que varían según la etiqueta, pero el orden de magnitud se mantiene.
Combinar apt-get update e install
Cuando la imagen base es Debian y hace falta instalar paquetes con apt-get, aparece un problema de caché distinto. La documentación oficial de Docker recomienda ejecutar apt-get update e install en la misma instrucción RUN: si update queda en una capa separada y se cachea, un build posterior puede reutilizarla sin actualizar el índice, e instalar una versión desactualizada —o rota— del paquete pedido. Docker llama a esto cache busting.
# Evitar: update cacheado
RUN apt-get update
RUN apt-get install -y curl
# Preferido: una sola instrucción
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
La bandera --no-install-recommends no es de Docker: es una opción de apt, documentada en el manual de apt-get de Debian, y evita instalar paquetes que el pedido solo «recomienda» sin necesitarlos. El rm -rf /var/lib/apt/lists/* al final borra el índice descargado por update, que ya no hace falta y de otro modo ocupa espacio en esa capa.
Cómo medir el tamaño real
Todo lo anterior se puede verificar con el propio Docker, sin herramientas adicionales:
- Construir la versión original con un tag identificable
docker build -t mi-app:antes . - Anotar su tamaño
docker images mi-appmuestra el tamaño en la columnaSIZE. - Aplicar las técnicasReordenar instrucciones, sumar
.dockerignore, pasar a multietapa y cambiar la imagen base. - Reconstruir con otro tag
docker build -t mi-app:despues . - Comparar ambos tamaños
docker images mi-applista las dos versiones, una junto a la otra. - Inspeccionar qué pesa cada capa
docker history mi-app:despuesdesglosa el tamaño por instrucción.
Medir antes y después de cada cambio ayuda a entender cuánto aportó cada técnica por separado.
Errores frecuentes
- Poner
COPY . .al principio. Invalida en cascada todas las capas siguientes, incluida la instalación de dependencias. - Tratar el tamaño como algo estético. Una imagen grande no es solo más lenta de mover: también es más superficie de ataque.
- Confundir multietapa con varios Dockerfiles. Es un único archivo con varios
FROM; lo que pasa de etapa en etapa es lo que se copia conCOPY --from. - No usar
.dockerignore. El build copianode_modules,.gity archivos de entorno sin que el Dockerfile los use. - Separar
apt-get updatedeinstallen capas distintas. Puede quedar cacheada una actualización vieja del índice de paquetes. - Elegir distroless sin plan para depurar. Sin shell ni gestor de paquetes, hace falta otra estrategia para investigar adentro.
Preguntas frecuentes
¿Qué es una construcción multietapa y cómo reduce el tamaño final?
Una construcción multietapa es un Dockerfile con más de una instrucción FROM, cada una abriendo una etapa distinta dentro del mismo archivo, no varios archivos separados. Una etapa temprana, habitualmente nombrada con AS build, puede tener un compilador, un kit de desarrollo, dependencias que solo hacen falta para construir y el código fuente completo. Esa etapa produce un resultado y después se descarta por completo: no deja rastro en la imagen publicada. La imagen final arranca desde otro FROM, normalmente una base mucho más chica pensada solo para ejecutar, y a ella se copia explícitamente, con COPY --from=<etapa>, únicamente lo necesario para correr. El compilador, el código fuente y las herramientas de construcción nunca llegan a la imagen final.
¿Por qué el orden de las instrucciones afecta la velocidad del build?
Docker ejecuta las instrucciones del Dockerfile en orden y guarda el resultado de cada una como una capa cacheada. En cada reconstrucción revisa cada instrucción: si detecta un cambio, descarta la caché de esa capa y de todas las siguientes, sin importar si habrían dado el mismo resultado. Por eso conviene ubicar cerca del FROM lo que casi nunca cambia —copiar e instalar dependencias— y dejar el código fuente, que cambia en cada commit, para el final. Invertir ese orden hace que cualquier modificación mínima dispare de nuevo pasos costosos, como reinstalar todas las dependencias, aunque ninguna haya cambiado.
¿Alpine o distroless?
Depende de si necesitás entrar al contenedor a diagnosticar problemas. Alpine es minimalista, más chica que Debian o Ubuntu, pero conserva gestor de paquetes y shell, así que se puede usar docker exec para investigar; su costado a vigilar es que usa musl en lugar de glibc. Distroless va más allá: sin gestor de paquetes ni shell, solo la app y sus dependencias mínimas, más chica pero más difícil de inspeccionar por dentro. Para producción, donde depurar adentro debería ser la excepción, distroless suele ser la opción más defendible; si hace falta esa flexibilidad, Alpine es buen intermedio.
¿Qué hace exactamente .dockerignore?
Antes de ejecutar la primera instrucción, Docker arma el contexto de construcción: envía el contenido del directorio donde se corre docker build, incluidos los subdirectorios. Un archivo .dockerignore, en la raíz del proyecto, indica qué rutas excluir de ese envío, con el mismo formato de patrones que un .gitignore, sin reorganizar el repositorio. Sirve para dos cosas: acelerar el build, con menos datos para transferir, y evitar que se copien por accidente carpetas pesadas como node_modules o .git, o archivos con secretos.
¿Por qué combinar apt-get update e install en una sola instrucción?
Porque cada RUN genera su propia capa cacheada, y si apt-get update queda separado de install, una reconstrucción posterior puede reutilizar esa capa vieja del índice sin actualizarlo, aunque install sí se ejecute de nuevo. El resultado es instalar una versión desactualizada del paquete, y a veces una ya retirada del repositorio que falla al descargarse. Ejecutar ambos comandos en la misma instrucción RUN, unidos con &&, hace que se cacheen o se invaliden siempre juntos: si algo fuerza la reconstrucción de esa capa, se actualiza el índice antes de instalar. La documentación oficial de Docker llama a esto cache busting.
¿Cómo mido el tamaño real de la imagen y comparo antes y después?
No hace falta ninguna herramienta externa: alcanza con Docker. Construí la versión sin optimizar con un tag identificable (docker build -t mi-app:antes .) y anotá el tamaño en la columna SIZE de docker images mi-app. Aplicá las técnicas —reordenar instrucciones, sumar un .dockerignore, pasar a multietapa, cambiar la imagen base final— y reconstruí con otro tag, como mi-app:despues. docker images mi-app va a listar ambas versiones con su tamaño real. Para ver qué instrucción pesa más dentro de una imagen, docker history mi-app:despues desglosa el tamaño capa por capa.
Fuentes
Documentación oficial y material de la comunidad consultados para esta guía. Fecha de consulta: 29 de julio de 2026.
Aportes de la comunidad Underc0de
- Underc0de, foro. Curso de Docker (Básico - Medio - Avanzado), por DarkDante, 12 de mayo de 2020, sección QA. No cubre multietapa ni tamaño.
Documentación oficial
- Docker. Multi-stage builds. Cómo declarar varias etapas y copiar artefactos entre ellas.
- Docker. Docker build cache. Cómo se cachea cada capa y cómo se invalida.
- Docker. Dockerfile best practices. Orden de instrucciones, .dockerignore y cache busting con apt-get.
- Docker. Base images. Criterios para elegir la imagen base.
- Docker. Dockerfile reference. Sintaxis de FROM, COPY --from y RUN.
- Google Container Tools. distroless. Tamaño de las variantes y comparación con Alpine y Debian.
- Debian. apt-get(8) — manual de referencia. Definición de --no-install-recommends, propia de apt, no de Docker.