← maurobernal.com.ar

Etiqueta: layers

  • Mi guía para escribir Dockerfiles que no me den vergüenza

    Mi primer Dockerfile ocupaba 2.1 GB. No exagero. Usaba una imagen base de Ubuntu completa, instalaba todo lo que se me ocurría por las dudas, y copiaba el código sin pensar en las capas. Hoy mis imágenes de producción pesan entre 60 y 120 MB. Este artículo es ese camino condensado.

    El Dockerfile: instrucciones para construir una imagen

    Un Dockerfile es un archivo de texto con instrucciones que Docker ejecuta en orden para construir una imagen. Cada instrucción crea una nueva capa. Las capas se cachean, lo que hace que los builds sucesivos sean rápidos — si sabés ordenarlas bien.

    Las instrucciones esenciales

    # Instrucciones que uso en casi todos mis Dockerfiles:
    
    FROM        # imagen base (siempre es la primera instrucción)
    WORKDIR     # directorio de trabajo dentro del contenedor
    COPY        # copia archivos del host al contenedor
    RUN         # ejecuta un comando durante el build
    ENV         # variables de entorno
    EXPOSE      # documenta el puerto que usa el contenedor
    ENTRYPOINT  # comando principal del contenedor
    CMD         # argumentos por defecto del entrypoint

    Mi primer Dockerfile real: una API .NET 8

    Cuando tuve que dockerizar mi primera API .NET en el trabajo, empecé con algo que funcionaba pero era terrible. Luego aprendí a hacerlo bien.

    La versión ingenua (no hagas esto)

    # ❌ Dockerfile ingenuo - imagen de 2GB+
    FROM ubuntu:22.04
    
    RUN apt-get update && apt-get install -y     wget curl git vim     dotnet-sdk-8.0
    
    WORKDIR /app
    COPY . .
    RUN dotnet build -c Release
    
    EXPOSE 80
    CMD ["dotnet", "run", "--project", "MiApi"]

    Problemas de esta versión: imagen base enorme, herramientas de desarrollo en producción, SDK completo en runtime, dotnet run en lugar de un ejecutable publicado, y sin usuario no-root.

    La versión con multi-stage build (la forma correcta)

    # ✅ Dockerfile con multi-stage build - imagen de ~80MB
    # STAGE 1: Build
    FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
    WORKDIR /src
    
    # Copiar solo el .csproj primero (aprovecha cache de capas)
    COPY ["src/MiApi/MiApi.csproj", "src/MiApi/"]
    RUN dotnet restore "src/MiApi/MiApi.csproj"
    
    # Ahora copiar el resto del código
    COPY . .
    WORKDIR "/src/src/MiApi"
    RUN dotnet build "MiApi.csproj" -c Release -o /app/build
    
    # STAGE 2: Publish
    FROM build AS publish
    RUN dotnet publish "MiApi.csproj" -c Release -o /app/publish     /p:UseAppHost=false
    
    # STAGE 3: Runtime (imagen final - solo lo necesario)
    FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
    WORKDIR /app
    
    # Usuario no-root
    RUN adduser --disabled-password --gecos "" appuser
    USER appuser
    
    COPY --from=publish /app/publish .
    EXPOSE 80
    ENTRYPOINT ["dotnet", "MiApi.dll"]

    La magia del multi-stage: la imagen final solo contiene el runtime de ASP.NET y el binario publicado. Todo el SDK, las herramientas de build y el código fuente quedan afuera. El resultado: de 2.1 GB a 82 MB.

    Cómo funcionan las capas (y por qué importa el orden)

    Cada instrucción RUN, COPY o ADD crea una capa nueva. Docker cachea cada capa y solo reconstruye las que cambiaron y todas las que vienen después. Esta regla cambia completamente cómo ordenás las instrucciones.

    # ❌ Orden incorrecto: cualquier cambio en el código invalida el restore
    COPY . .                              # capa 1 - se invalida con cada cambio
    RUN dotnet restore                    # capa 2 - se repite innecesariamente
    
    # ✅ Orden correcto: restore se cachea hasta que cambie el .csproj
    COPY ["MiApi.csproj", "."]           # capa 1 - solo cambia si el proyecto cambia
    RUN dotnet restore                    # capa 2 - se cachea
    COPY . .                              # capa 3 - cambia con el código
    RUN dotnet build -c Release           # capa 4 - solo se repite si hay cambios

    Buenas prácticas que aplico en todos mis Dockerfiles

    1. Siempre usar imágenes base específicas, nunca :latest

    # ❌ Impredecible en producción
    FROM node:latest
    FROM python:latest
    
    # ✅ Reproducible y auditable
    FROM node:20.11.0-alpine3.19
    FROM python:3.12.2-slim-bookworm

    2. Preferir imágenes Alpine o Slim

    # node:20-alpine:   ~55 MB
    # node:20-slim:    ~180 MB
    # node:20:         ~1.1 GB
    FROM node:20-alpine

    3. Combinar comandos RUN para reducir capas

    # ❌ Tres capas separadas
    RUN apt-get update
    RUN apt-get install -y curl
    RUN rm -rf /var/lib/apt/lists/*
    
    # ✅ Una sola capa, sin cache de apt
    RUN apt-get update && apt-get install -y     curl     && rm -rf /var/lib/apt/lists/*

    4. El .dockerignore es tan importante como el .gitignore

    # .dockerignore
    **/bin/
    **/obj/
    **/.git/
    **/node_modules/
    **/*.log
    .env
    .env.local
    docker-compose*.yml
    README.md

    5. Nunca hardcodear secretos en el Dockerfile

    # ❌ NUNCA - queda grabado en la capa para siempre
    ENV DB_PASSWORD=mipassword123
    RUN curl -H "Authorization: Bearer TOKEN_SECRETO" https://api.ejemplo.com
    
    # ✅ Pasar en runtime como variables de entorno
    docker run -e DB_PASSWORD=$DB_PASSWORD mi-imagen

    Comandos esenciales para construir y gestionar imágenes

    # Construir imagen
    docker build -t mi-api:1.0.0 .
    docker build -t mi-api:1.0.0 -f Dockerfile.prod .  # Dockerfile específico
    docker build --no-cache -t mi-api:latest .           # Forzar rebuild completo
    
    # Etiquetar
    docker tag mi-api:1.0.0 mi-api:latest
    docker tag mi-api:1.0.0 miregistry.local/mi-api:1.0.0
    
    # Ver capas y tamaño
    docker history mi-api:1.0.0
    docker images mi-api
    
    # Limpiar imágenes no usadas
    docker image prune -a  # elimina todas las imágenes sin contenedor activo

    El resultado: de 2.1 GB a 82 MB

    Con el multi-stage build, una imagen base correcta, el orden de capas optimizado y el .dockerignore configurado, pasé de imágenes que tardaban 8 minutos en buildearse y ocupaban gigabytes, a imágenes que buildean en 90 segundos (o 20 segundos con cache) y pesan menos de 100 MB. En producción, eso se traduce en deploys más rápidos, menos espacio en el registry y menor superficie de ataque.


    Artículo anterior: Arquitectura interna de Docker | Serie Docker Completo | Próximo: Ciclo de vida de contenedores →


    Artículo anterior: Por dentro del motor: entendiendo la arquitectura de Docker | Serie Docker Completo | Próximo: docker run y todo lo que nadie te explica del ciclo de vida de un contenedor →

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)