Crear imágenes eficientes con Docker empieza por un Dockerfile mínimo y evoluciona hacia multi-stage builds cuando el proyecto crece. Aquí se explica, paso a paso y sin complicaciones, cómo pasar de una imagen simple a una imagen final de .NET optimizada, tal como se ve al trabajar con Visual Studio, VS Code y Docker Desktop.
¿Por qué iniciar con un Dockerfile mínimo?
Empezar con pocas líneas reduce errores y acelera la creación de imágenes. En un backend de Python, basta con una imagen base, un directorio de trabajo e instalar dependencias. Funciona y es simple. Sin embargo, en proyectos complejos, la imagen puede crecer de forma importante.
¿Qué problema resuelve multi-stage?
Al crear una API con ASP.NET desde Visual Studio, el Dockerfile generado tenía varias sentencias FROM repetidas. Esto indica una técnica multi-stage: varias etapas encadenadas para compilar, publicar y ensamblar solo lo necesario.
Imagen base: queda lista, sin cargar código aún.
Etapa de compilación: usa SDK para tomar el proyecto de C# y ejecutar un dotnet build.
Etapa de publicación: ejecuta dotnet publish y deja únicamente los ensamblados.
Etapa final: copia los archivos publicados a la imagen base con runtime y entrega una imagen más pequeña y enfocada.
El resultado: se usa solo lo necesario, no todo el árbol de código, y la imagen se vuelve más ligera y segura de manejar.
¿Cómo funciona el flujo multi-stage en .NET?
El Dockerfile de Visual Studio puede tener unas 25 líneas y tres etapas bien visibles: compilación, publicación y final. Durante la construcción, el log muestra claramente “etapa 1/3, 2/3, 3/3”, lo que ayuda a verificar que cada paso se ejecuta correctamente.
¿Qué diferencia hay entre Visual Studio y la documentación de Microsoft?
La documentación de Microsoft simplifica el proceso a dos etapas: usar FROM para restaurar y publicar, y luego una segunda etapa para empaquetar en la imagen de ASP.NET. Es la misma idea, pero más compacta: menos pasos, mismo objetivo.
Visual Studio: tres etapas separadas y explícitas.
Documentación de Microsoft: dos etapas, restaurar/publicar y final.
Ambas rutas dejan una imagen optimizada con solo los archivos necesarios de la aplicación.
¿Qué comandos clave se usan?
Para construir la imagen multi-stage del proyecto “multi-stage” en VS Code:
Cambiar a la carpeta del proyecto: cd multi-stage.
Verificar el Dockerfile en la ruta actual.
Construir con etiqueta específica.
docker build -t multi-stage .
Durante la compilación se observan mensajes que indican el paso entre etapas. Así confirmas que el encadenamiento funciona y que cada fase hace su parte.
¿Qué resultados y optimización se observaron?
En Docker Desktop, la imagen “multi-stage” aparece con un peso aproximado de 843 MB para una aplicación estándar de .NET. Es un avance, pero aún hay margen para reducir más el tamaño en iteraciones futuras.
¿Qué habilidades y conceptos se refuerzan?
Diseño de Dockerfile minimalista: menos líneas, menos errores.
Uso de multi-stage: FROM repetidos que representan etapas.
Diferencia entre SDK y runtime: compilar vs ejecutar.
Comandos dotnet build y dotnet publish: separar compilación y publicación.
Lectura del log de construcción: identificar “1/3, 2/3, 3/3”.
Trabajo en VS Code sin tener .NET instalado: se ejecuta todo dentro de Docker.
Validación en Docker Desktop: revisión de la imagen final y su tamaño.
¿Te gustaría que mostremos más tácticas para recortar el peso de la imagen y automatizar el flujo? Deja tus dudas y propuestas en los comentarios.
Técnica para la construcción de imágenes optimizadas y eficientes.
Es util cuando tenemos herramientas pesadas que no son necesarias en la imagen final que se ejecutará en producción.
Beneficios
Reducción del tamaño de la imagen final: Solo incluye extrictamente lo necesario.
<!---->
Eficiencia en el proceso de construcción: Reutiliza etapas previas de compilación y reduce redundancias.
No sé si alguien más llegó a esta conclusión y no estoy seguro si el profe lo explica. Pero quiero decir que la construcción multi-stage resulta muy útil cuando trabajamos con un lenguaje compilado.
Pero si el lenguaje es interpretado me temo que no hay mucho que hacer mas que tratar de utilizar las versiones alpine de dichos lenguajes, porque a la final necesitan de todas esas dependencias para funcionar. Puede rascarse un poco pero no es tan significativo como en el primer caso.
Impresionante, las diferencias entre los lenguajes cambian todo sustancialmente
Encontré un dockerfile que usa multi-stage con python 🐍:
# Primera imagen para compilar FROM python:3.8.4-slim-buster ascompile-image
# Se define una variable opcionalRUN python3 -m venv /opt/venv
# Se sobreescribe la variable path para que tenga prioridad los comandos del ambienteENV PATH="/opt/venv/bin:$PATH"# Se copia unicamente el archivo de dependencias COPY requirements.txt /requirements.txt
# Se instalan las dependencias.RUN pip install -r requirements.txt
# Listo, inicia el segundo contenedor FROM python:3.8.4-alpine3.12 AS build-image
# Se copia la carpeta venv que contiene todas las dependencias en el segundo contenedorCOPY --from=compile-image /opt/venv /opt/venv
# Se copia la aplicaciónCOPY . usr/src/app
# Se establece por defecto el directorio WORKDIR /usr/src/app
# Se agrega el directorio a las variables de ambiente.ENV PATH="/opt/venv/bin:$PATH"# Arranca la aplicaciónENTRYPOINT python3 main.py
``````  aquí te cuento algunas cosas que me sorprendieron:**1 etapa o primer contendor**\- En esta etapa solo compila la imágen de python: instala dependencias y demás: El docker quedará sucio y pesado.\- `FROM python:3.8.4-slim-buster ascompile-image` la versión slim-buster te da la imagen que compila con C. Por lo que es adecuada para correr numpy, keras y todo lo relacionado con machine learning.\- la línea `RUN python3 -m venv /opt/venv` asigna una carpeta para el entorno virtual.\- la línea `ENV PATH="/opt/venv/bin:$PATH"` asigna el entorno virtual a las variables de entorno de linux 🤯 (así todo corre en el entorno virtual y no en el sistema operativo del docker)\- las dependencias se instalarán en el entorno virtual: `RUN pip install -r requirements.txt`  **2 etapa o segundo contendor** \- La primera etapa deja el docker lleno de caché y basura que solo hace el docker más pesado. Por lo que en esta segunda etapa, se crea un nuevo docker que sí va a ser usado.\- `FROM python:3.8.4-alpine3.12 AS build-image` descarga la imágen de python que funciona con alpine.*¡Alpine es la distro de linux más liviana que existe! 🪶*\- la línea `COPY --from=compile-image /opt/venv /opt/venv` copia las dependencias del contenedor sucio y las pega en el directorio opt/venv (ya sabes el entorno virtual) \- aquí **SÍ** copias el proyecto de tu vsc local `COPY . usr/src/app`, en un contenedor limpio. Para que todo dev siempre sepa dónde buscar el proyecto, déjalo en usr/src/app.<u>Así millones de devs te lo agradecerán</u> \- Creas el Workdir, nuevamente asignas el path para que todas las dependencias se instalen allí y ENTRYPOINT arranca el archivo que necesites. Espero te haya servido ~ @camilocsoto
Que buena explicación
Chicos, por si alguien se perdio... En este dockerfile hacemos varios pasos digamoslo asi, para generar contenedores que para nosotros es 1 solo.
Primera fase que el le dice "build" miren el as build.
Le decimos WORKDIR es como el cd de la consola de docker.
Luego agarramos el .csproj, que es como el package.json de js con las dependencias de ese proyecto de net. Y lo copiamos a dentro de la carpeta de docker . en este caso /src.
Luego hacemos un dotnet restore, que es un comando de .net para que se instalen las dependencias, luego hacemos el copy de siempre, osea todo el proyecto de nosotros local, hacia el docker. Luego, corremos 2 comandos con RUN, donent build blabla.csproj -c Release -o /app/build, que en resumen, son comandos de .NET que es para que la app sea productiva y se guarde en /app/build.
el 2do comando es dotnet publish -c Release -o /app que lo que hace es que de ese codigo crea un minificado (por que hace los 2? no hacen lo mismo?) Pues si, pero el comando anterior deja una cache y este comando, minifica ese cache y lo hace mucho mejor para que la aplicacion quede mejor productiva y esto se guarda en la carpeta /app.
BIEN, ahora entramos en fase 2 o 2do contenedor no tiene nombre, pero digamosle que es final final final...
Ahora, el 2do contendor saca el sdk de nuevo, se va a /app y luego hace un COPY, ojo, ahi coloca:
COPY --from=build, este comando es para que desde nuestro contendor de fase 1 que le pusimos build EL TOME DESDE ESE CONTENEDOR esto lo hace para que no se confunda y tome nuestro local... Ya que nuestor local no tiene nada, todo eso, lo toma de /app (from) y lo guarda en .
BIEN, ahora corremos ENTRYPOINT, que basicamente es LUEGO que montaste el contendor final final, ahora corre este comando, que para resumir es la forma de correr la app en linux de .net
Espero que entedieras! No te rindas
Pensé que al trabajar con Python no valdría la pena crear un Dockerfile multi-stages, pero consultando un poco encontré un uso similar y bastante práctico: Instalación de dependencias.
Para ello, creé una API con FastAPI sencilla:
from fastapi import FastAPI
app = FastAPI()@app.get("/health")asyncdefhealth_check():return{"status":"ok"}@app.get("/items/{item_id}")asyncdefread_item(item_id:int):return{"item\_id": item\_id,"message":"Item retrieved"}
Y un archivo de dependencias pyproject.toml (opté por usar uv como gestor de dependencias, pero se puede usar pip igualmente, declarando fastapi y uvicorn\[standard] como dependencias del requirement.txt:
# Single-stage build
FROM python:3.13-slim-bookworm AS runtime
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
COPY . .
RUN uv venv -p python3.13 .venv
RUN uv pip install --no-cache-dir .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
El tamaño de la imagen resultante fue de alrededor de 200 megas:
fastapi-single:latest 85ecaba9a6da 208MB 0B
Luego, declaré el siguiente Dockerfile, con una etapa de instalación de dependencias y otra donde se ejecuta la API:
# Multi-stage build
# Builder stage
FROM python:3.13-bookworm AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
COPY . .
RUN uv venv -p python3.13 .venv
RUN . .venv/bin/activate && uv pip install --no-cache-dir .
# Final stage
FROM python:3.13-slim-bookworm AS runtime
WORKDIR /app
COPY --from=builder /app/.venv .venv
COPY main.py .
EXPOSE 8000
CMD [".venv/bin/uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Tras construir la imagen, obtuve que la imagen final era de 153 megas:
fastapi-multi:latest ab0f446b33ef 153MB 0B
Por lo que sí, pueden beneficiarse proyectos de lenguajes interpretados en ciertos aspectos como instalación de dependencias.
Yendo más allá de esto, en proyectos multi-lenguaje donde hay un componente de frontend y uno de backend, donde el backend es compilado, y se opta por un monolito o algo similar, el proceso puede verse aún más beneficiado al no copiar toolkits y dependencias no necesarias en la imagen final.
Que interesante tu análisis, gracias por tu aporte.
Este Fede lobo es muy bueno, gracias profe
La técnica de multi-stage es una estrategia fundamental para reducir el tamaño de las imágenes Docker y optimizar su gestión. Este enfoque consiste en dividir el proceso de construcción en múltiples etapas dentro de un mismo Dockerfile, donde cada etapa puede utilizar una imagen base diferente y cumplir un propósito específico.
¿Cómo funciona?
El proceso se estructura en etapas secuenciales: las primeras etapas se encargan de compilar, construir dependencias y preparar los artefactos necesarios, mientras que la etapa final solo incluye los componentes esenciales para ejecutar la aplicación. Todo lo demás, como herramientas de compilación, archivos fuente y dependencias de desarrollo, se descarta automáticamente, resultando en una imagen final significativamente más ligera.
Beneficios clave
Reducción de tamaño: Solo se incluyen los artefactos necesarios para ejecutar la aplicación
Despliegues más rápidos: Menor tiempo de descarga y distribución de imágenes
Optimización de recursos: Menor consumo de espacio en disco y almacenamiento en la nube
Reducción de costos: Menos uso de recursos se traduce en menores gastos operativos
Mayor seguridad: Superficie de ataque reducida al eliminar herramientas y dependencias innecesarias
CI/CD mejorado: Pipelines más eficientes con tiempos de construcción y despliegue optimizados
Consideraciones según el tipo de lenguaje
Lenguajes Compilados
Los multi-stage builds brillan especialmente con lenguajes compilados (Go, Rust, C++, Java). En estos casos, la primera etapa compila el código fuente generando un binario o artefacto ejecutable, y la etapa final solo copia este artefacto a una imagen mínima, descartando completamente el compilador y todas las dependencias de desarrollo. La reducción de tamaño puede ser dramática: de cientos de MB a apenas unos pocos MB.
Lenguajes Interpretados
Con lenguajes interpretados (Python, Node.js, Ruby), el impacto es más limitado. Estos lenguajes requieren su runtime completo y todas las dependencias para funcionar, por lo que no hay muchos componentes que eliminar. La optimización se centra en:
Utilizar imágenes base Alpine Linux (versiones ultraligeras)
Limpiar cachés de gestores de paquetes
Eliminar archivos de prueba y documentación
Usar dependencias de producción únicamente.
Con esta clase pasé de tener una imagen en 800 MB a menos de 200 M, increible!
Lo que entendí es que multi-stage sirve para llevar los proyectos a producción y no montar allá las imágenes pesadas que tenemos en local. Por otro lado entiendo que por regla Docker solo guarda únicamente la última imagen descartando todas las anteriores
FROM ubuntu AS etapa1 # ← Se descarta
FROM alpine AS etapa2 # ← Se descarta
FROM debian AS etapa3 # ← Se descarta
FROM nginx AS final # ← Esta se GUARDA(ya que es la última)
Un archivo Dockerfile es un script de texto que contiene una serie de instrucciones para automatizar la creación de una imagen de Docker. Estas instrucciones definen cómo se debe configurar el entorno, qué aplicaciones se deben instalar y cómo se deben ejecutar. Las líneas en un Dockerfile pueden incluir comandos como FROM para especificar la imagen base, RUN para ejecutar comandos en la construcción de la imagen, y COPY para añadir archivos desde el sistema local al contenedor. Utilizar Dockerfiles permite crear imágenes optimizadas y reproducibles, facilitando el despliegue de aplicaciones en contenedores.
De nuevo un proyecto con dotnet...
La clase se centra en la optimización de Dockerfiles mediante el uso de imágenes multi-stage. Se destaca la importancia de crear Dockerfiles simples para evitar errores. Se explica cómo Visual Studio genera un Dockerfile más complejo que utiliza múltiples etapas para reducir el tamaño de la imagen final, pasando por compilación y publicación. Este enfoque permite incluir solo los archivos necesarios en la imagen final, optimizando espacio y eficiencia. La técnica multi-stage es clave para proyectos complejos, permitiendo un manejo eficaz de recursos en Docker.
Reducir el tamaño de las imágenes en Docker, como se menciona en el contexto de imágenes multi-stage, trae varios beneficios que afectan positivamente el rendimiento. Un menor tamaño significa menos tiempo de descarga y despliegue, lo que se traduce en un inicio más rápido de las aplicaciones. Además, las imágenes más pequeñas consumen menos espacio en disco y recursos en la nube, lo que puede resultar en costos operativos más bajos. Esto también mejora la portabilidad y facilita la gestión de dependencias, optimizando así el flujo de trabajo en CI/CD.
La reducción del tamaño de las imágenes en Docker y la gestión de actualizaciones se logra principalmente a través de la técnica de multi-stage builds. En esta técnica, se crean varias etapas en el Dockerfile, donde cada etapa puede usar imágenes base diferentes. Esto permite compilar y optimizar solo lo necesario para la imagen final, dejando atrás archivos innecesarios.
Al optimizar el Dockerfile, puedes reducir el tamaño de la imagen resultante, ya que solo se incluyen los artefactos necesarios después del proceso de compilación y publicación. Para actualizaciones, puedes modificar las etapas necesarias y reconstruir la imagen, lo que resulta en una imagen más pequeña que solo incluye las últimas versiones de tu aplicación y sus dependencias.
Por si alguien se encuentra desarrollando un proyecto con Anaconda3, se pueden construir contenedores a partir de estas imágenes:
FROM continuumio/miniconda3
FROM condaforge/miniforge3
Además les dejo este link en el que me encontré un ejemplo de construcción de imágenes multi-stage usando Anaconda3
Explorando más el funcionamiento del Multi-Stage el ahorro de recursos y los beneficios es acontecido de la siguiente manera:
1.- Etapas: Segmentar el proceso en capas permite una lectura más fluida. De manera predeterminada cada etapa es inicializada con un identificador numérico empezando en 0, para nombrarlo es empleado "AS" "nombre".
Ejemplo:
FROM golang:1.23 // Primera Etapa "0"
// Serie de Instrucciones
FROM scratch // Segunda Etapa
COPY --from=0 // Transferir los elementos de la primera etapa.
Asignando Nombre
FROM golang:1.23 AS build // Primera Etapa "Build"
// Serie de Instrucciones
FROM scratch // Segunda Etapa
COPY --from=build // Transferir los elementos de la etapa build.
// Serie de Instrucciones
2.- Eliminación Herramientas Redundantes: Mediante la segmentación de la imagen en etapas permite construir todos los archivos necesarios llamados "artefactos", los cuales pueden ser .exe, ,jar, archivos compilados de C/C++. Para posteriormente transferirlos sin los compiladores o herramientas en caso de no ser necesitados para el despliegue del proyecto aconteciendo de esta manera el ahorro de espacio.
Un ejemplo empleado en la documentación oficial de Docker es la creación de un archivo en C donde es compilado y al crear el contendor solamente es ejecutado, por ende es descartado el compilador en la segunda fase al no necesitar compilar más archivos en ejecución.
En resumen el Multi-Stage te ayuda a brindar una estructura más sólida de fácil lectura a tu archivo de Docker y descartar las herramientas de construcción que no vas a volver emplear. Para un correcto uso del Multi-Stage se necesita que analices que herramientas sigues necesitando mientras el contenedor sigue en ejecución y descartar aquellas que solo son empleadas para construir archivos esenciales en el tiempo de creación del contenedor.
El ejemplo mencionado anteriormente lo puedes recurrir en la documentación oficial de Docker "Multi-stage builds".