Imágenes y capas
Todo el curso despliega tienda/api:2.1.0 como si viniera dada. No viene dada: alguien la construyó, y de cómo la construyó dependen cosas que aparecerán mucho más adelante, cuando ya no se vean venir.
Una imagen es un paquete con todo lo necesario para ejecutar una aplicación: la distribución base, las librerías, tu binario. Se construye a partir de un fichero de texto —el Dockerfile—, y cada instrucción produce una capa de solo lectura. La imagen final es esa pila.
En esta lección construyes la primera versión de la api, la abres para ver sus capas y compruebas con un cronómetro por qué el orden de las instrucciones no es cuestión de estilo. Aún no hay clúster: trabaja desde la pestaña dev-machine.
Paso 1: La primera imagen
En ~/projects/api tienes el código: un servidor HTTP en Go, solo librería estándar, que escucha en el 8080. Le falta el Dockerfile. Créalo:
cat << 'EOF' > ~/projects/api/Dockerfile
FROM golang:1.24-alpine
WORKDIR /src
COPY . .
RUN apk add --no-cache git
RUN go build -o /api .
CMD ["/api"]
EOF
El Dockerfile, explicado con preguntas y respuestas
¿Qué aporta cada instrucción?
FROM elige la imagen base sobre la que apilar; WORKDIR fija el directorio de trabajo; COPY mete ficheros dentro; RUN ejecuta un comando durante la construcción y guarda el resultado como una capa nueva; CMD declara qué proceso arranca cuando el contenedor se ejecuta. Solo CMD no produce capa: es metadato, no contenido.
¿Para qué el apk add git?
Para representar lo que en un proyecto real es la instalación de dependencias del sistema. Aquí no hace falta para compilar, y precisamente por eso sirve de ejemplo limpio: es una capa cara que no depende de tu código.
¿La imagen lleva un kernel dentro?
No. La imagen aporta el sistema de ficheros y las librerías; el kernel es siempre el del anfitrión. Por eso una imagen de Alpine corre sobre un host de Ubuntu sin problema: solo comparten arquitectura y familia de sistema operativo.
Constrúyela:
cd ~/projects/api
docker build -t tienda/api:1 .
docker images tienda/api
Paso 2: Abrir la pila
Ahora mira de qué está hecha:
docker history tienda/api:1
Cada línea es una capa, con la instrucción que la creó y lo que ocupa. Verás que unas pocas concentran casi todo el tamaño: la base de Go, el apk add y el go build.
Fíjate en el total. Son más de 350 MB para un servidor que escribe una línea de texto, y ahí dentro va el compilador de Go entero, su biblioteca estándar en código fuente y el gestor de paquetes de Alpine. Nada de eso hace falta para ejecutar nada. Es el problema que resuelve el challenge de la lección siguiente.
Un detalle que sorprende: borrar un fichero en una capa posterior no lo elimina de la imagen. Las capas se apilan, no se reescriben: la capa nueva lo tapa, y el contenido sigue ahí abajo, ocupando y siendo recuperable. Por eso un secreto copiado por error a una imagen no se arregla con un RUN rm.
Paso 3: La caché, y por qué el orden importa
Las capas se cachean. Al reconstruir, Docker reutiliza las que están por encima del primer cambio y rehace solo el resto. Compruébalo: toca el código y reconstruye midiendo el tiempo.
sed -i 's/2.1.0/2.1.1/' main.go
time docker build -t tienda/api:2 .
Mira la salida. En cuanto llega al COPY . . se acabó la caché, y a partir de ahí lo rehace todo: el apk add incluido, aunque no hayas tocado ninguna dependencia. Has pagado la instalación de paquetes por cambiar un literal.
La regla que se deduce es corta: de menos volátil a más volátil. Lo que casi nunca cambia, arriba; tu código, al final. Reordena el Dockerfile:
cat << 'EOF' > ~/projects/api/Dockerfile
FROM golang:1.24-alpine
WORKDIR /src
RUN apk add --no-cache git
COPY . .
RUN go build -o /api .
CMD ["/api"]
EOF
Paso 4: Comprobar que la caché ahora sí sirve
Construye dos veces con el Dockerfile nuevo, cambiando el código entre medias:
time docker build -t tienda/api:2 .
sed -i 's/2.1.1/2.1.2/' main.go
time docker build -t tienda/api:3 .
La segunda construcción tarda una fracción de la primera, y en la salida verás CACHED en las instrucciones de arriba. No es que Docker haya repetido el trabajo más rápido: es que no lo ha repetido. La capa del apk add de tienda/api:3 no se parece a la de tienda/api:2: es literalmente la misma, el mismo blob almacenado una sola vez.
Compruébalo comparando la cadena de capas de las dos imágenes:
docker image inspect tienda/api:2 --format '{{range .RootFS.Layers}}{{println .}}{{end}}'
docker image inspect tienda/api:3 --format '{{range .RootFS.Layers}}{{println .}}{{end}}'
Las primeras coinciden una a una —la base y el apk add— y solo divergen las dos últimas, las que dependen de tu código. Eso es exactamente lo que va a comprobar la tarea.
Aquí no sirve
docker history: con BuildKit, el constructor que Docker usa por defecto, las capas intermedias aparecen como<missing>y no hay identificadores que comparar. Sigue valiendo para ver instrucciones y tamaños, que es para lo que lo usaste en el paso 2.
Resumen
- Una imagen es una pila de capas de solo lectura; cada instrucción del Dockerfile que aporta contenido crea una.
- Las capas se apilan y no se reescriben: lo que borras en una capa posterior sigue dentro de la imagen.
- La caché reutiliza todo lo que está por encima del primer cambio, así que el orden de las instrucciones decide si reconstruir cuesta segundos o minutos.
- De menos volátil a más volátil: base y dependencias arriba, tu código al final.
- Y queda un problema sin resolver: lo que has construido pesa más de 350 MB y lleva dentro el compilador. Eso es la lección siguiente.
- Next lesson
- Challenge: construye la imagen de la tienda