Un Dockerfile tradicional puede terminar incluyendo el SDK, compiladores, código fuente, cachés y herramientas de diagnóstico que solo se necesitan durante la compilación.

Pronbando el flujodock

Los builds multi-stage separan esas responsabilidades:

Etapa build                     Imagen final
SDK + código + compilador  →    binario ejecutable

Cada instrucción FROM comienza una etapa. COPY --from permite trasladar únicamente los artefactos necesarios a una imagen final limpia, dejando fuera las herramientas de compilación. Docker Docs.

Qué se va a construir

Se compararán dos imágenes de una aplicación Go:

  • app:single: incluye Go, código fuente, cachés y binario.
  • app:multi: contiene únicamente el binario estático.

La imagen multi-stage utilizará scratch, no tendrá shell ni gestor de paquetes y ejecutará el proceso con UID 65532.

Multi-stage reduce tamaño y superficie de ataque, pero no elimina automáticamente todas las vulnerabilidades. El binario, sus dependencias y cualquier base utilizada en runtime deben seguir analizándose y actualizándose.

Requisitos previos

  • Docker Engine actualizado.
  • Docker Buildx con BuildKit.
  • Acceso a las imágenes base.
  • Un proyecto de prueba aislado.
  • Registro donde publicar imágenes inmutables.
  • Herramientas para probar la aplicación, como curl.

Comprueba las versiones:

docker version
docker buildx version
docker buildx ls

Los checks mediante docker build --check requieren Docker Buildx 0.15 o posterior. Build checks.

Paso 1: crear una aplicación de ejemplo

Crea go.mod:

module ejemplo.com/multistage

go 1.26

Crea main.go:

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
		fmt.Fprintln(w, "aplicacion multi-stage operativa")
	})

	log.Println("escuchando en :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

La aplicación utiliza únicamente la biblioteca estándar y puede compilarse como binario estático.

Paso 2: reducir el contexto con .dockerignore

Crea .dockerignore:

.git
.gitignore
Dockerfile*
.env
.env.*
!.env.example
bin/
dist/
coverage/
tmp/
*.log

El contexto es el conjunto de archivos enviado al builder. Excluir repositorios, credenciales, logs y artefactos reduce transferencias y evita invalidaciones innecesarias de caché.

COPY . . no debe enviar automáticamente todo el directorio de trabajo. Optimización de caché.

Comprueba que .env.example no contenga credenciales reales antes de exceptuarlo.

Paso 3: crear un Dockerfile single-stage

Guarda como Dockerfile.single:

# syntax=docker/dockerfile:1

FROM golang:1.26

WORKDIR /src

COPY go.mod ./
RUN go mod download

COPY . .

RUN go test ./...
RUN CGO_ENABLED=0 GOOS=linux \
    go build \
    -trimpath \
    -ldflags="-s -w" \
    -o /usr/local/bin/app \
    .

EXPOSE 8080

ENTRYPOINT ["/usr/local/bin/app"]

Este Dockerfile funciona, pero su imagen final conserva:

  • La distribución base.
  • El SDK de Go.
  • El compilador.
  • El código fuente.
  • La caché de compilación.
  • El binario.

Eliminar archivos en una capa posterior no siempre recupera el espacio ocupado en capas anteriores.

Paso 4: crear el Dockerfile multi-stage

Guarda como Dockerfile.multi:

# syntax=docker/dockerfile:1

FROM golang:1.26 AS build

WORKDIR /src

COPY go.mod ./

RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download

COPY . .

RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go test ./...

RUN --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 GOOS=linux \
    go build \
    -trimpath \
    -ldflags="-s -w" \
    -o /out/app \
    .

FROM scratch AS runtime

COPY --from=build /out/app /app

USER 65532:65532

EXPOSE 8080

ENTRYPOINT ["/app"]

La primera etapa contiene todo lo necesario para compilar. La etapa runtime comienza desde cero y recibe únicamente /out/app.

Nombrar las etapas mediante AS build es más seguro que referenciarlas por índice. Si alguien reordena los bloques FROM, COPY --from=build continúa apuntando a la etapa correcta.

Qué hace cada optimización

Separar dependencias del código

COPY go.mod ./
RUN go mod download
COPY . .

Docker puede reutilizar la descarga de dependencias mientras go.mod no cambie. Si se ejecutara primero COPY . ., cualquier modificación del código invalidaría esa capa.

En proyectos con dependencias y archivo go.sum, copia ambos antes del código:

COPY go.mod go.sum ./

Utilizar cache mounts

RUN --mount=type=cache,target=/root/.cache/go-build ...

Los cache mounts persisten entre builds y aceleran nuevas compilaciones. No se incorporan a la imagen final.

La caché del builder puede seguir ocupando espacio local aunque la imagen distribuida sea pequeña. Revísala por separado:

docker builder du

Compilar un binario estático

CGO_ENABLED=0 GOOS=linux go build

Desactivar CGO evita depender de bibliotecas dinámicas que no existen en scratch. Esta decisión debe validarse si la aplicación utiliza paquetes que requieren CGO.

Eliminar rutas locales

-trimpath

Reduce información sobre rutas del sistema de compilación incluida en el binario.

Eliminar símbolos

-ldflags="-s -w"

Reduce el tamaño al retirar información de símbolos y depuración. Esto puede dificultar determinados análisis de fallos; conserva artefactos con símbolos cuando tu proceso operativo los necesite.

Ejecutar sin root

USER 65532:65532

Linux acepta UID y GID numéricos aunque scratch no tenga /etc/passwd. El proceso no necesita privilegios para escuchar en el puerto 8080.

Paso 5: validar el Dockerfile

docker build \
  --check \
  -f Dockerfile.multi \
  .

Los checks analizan el Dockerfile sin ejecutar el build completo. Detectan, entre otros problemas, nombres de etapas duplicados, casing inconsistente y plataformas base incorrectas. Referencia de build checks.

No ocultes los resultados con || true. Revisa cada advertencia y configura cuáles deben bloquear la integración continua.

Paso 6: construir ambas imágenes

Construye la versión single-stage:

docker build \
  --progress=plain \
  -f Dockerfile.single \
  -t app:single \
  .

Construye la multi-stage:

docker build \
  --progress=plain \
  -f Dockerfile.multi \
  -t app:multi \
  .

BuildKit procesa únicamente las etapas necesarias para el target solicitado.

Paso 7: comparar los tamaños

docker image ls app

Obtén el tamaño exacto en bytes:

docker image inspect \
  app:single \
  app:multi \
  --format '{{index .RepoTags 0}}: {{.Size}} bytes'

Revisa las capas:

docker history app:single
docker history app:multi

La diferencia concreta depende de la arquitectura, la versión de Go, el código y las imágenes almacenadas localmente. Registra las mediciones reales en lugar de prometer un porcentaje fijo.

Una reducción de tamaño no demuestra por sí sola que la aplicación funcione o que la imagen sea segura.

Paso 8: probar la imagen final

docker run -d \
  --rm \
  --name app-multi \
  -p 127.0.0.1:8080:8080 \
  app:multi

Comprueba el endpoint:

curl -fsS http://127.0.0.1:8080/

Resultado esperado:

aplicacion multi-stage operativa

Revisa los logs:

docker logs app-multi

Comprueba el usuario:

docker inspect app-multi \
  --format 'User={{.Config.User}}'

Resultado esperado:

User=65532:65532

Detén la prueba:

docker stop app-multi

Paso 9: inspeccionar una etapa intermedia

Puedes detener el build en la etapa build:

docker build \
  --target build \
  -f Dockerfile.multi \
  -t app-build:debug \
  .

Como esa etapa sí tiene shell y herramientas:

docker run --rm -it \
  --entrypoint /bin/sh \
  app-build:debug

Esto permite diagnosticar la compilación sin agregar herramientas a la imagen de producción.

No publiques accidentalmente app-build:debug como artefacto final.

Paso 10: comprender las limitaciones de scratch

scratch no contiene:

  • Shell.
  • Gestor de paquetes.
  • Certificados CA.
  • Base de usuarios.
  • Información de zona horaria.
  • Herramientas como curl, ps o ls.

Por eso, este comando debe fallar:

docker run --rm \
  --entrypoint /bin/sh \
  app:multi

Si la aplicación realiza conexiones HTTPS salientes, necesitará certificados CA. Puedes copiarlos desde una etapa preparada para ello o utilizar una imagen runtime mínima que ya los incluya.

Si necesita datos de zonas horarias, librerías dinámicas o herramientas operativas, scratch puede no ser la base adecuada. Considera una imagen distroless o una distribución mínima.

La imagen final debe ser tan pequeña como sea práctico, no tan pequeña como sea posible a cualquier costo.

Cómo manejar secretos durante el build

No pases tokens mediante ARG o ENV:

ARG TOKEN
ENV TOKEN=$TOKEN

Los argumentos y variables pueden quedar expuestos en metadatos, cachés o la imagen.

Utiliza un secret mount:

RUN --mount=type=secret,id=repo_token \
    TOKEN="$(cat /run/secrets/repo_token)" \
    comando-que-necesita-el-token

Entrega el secreto al builder:

docker build \
  --secret id=repo_token,src=/ruta/segura/token \
  -f Dockerfile.multi \
  -t app:multi \
  .

El secreto existe únicamente durante esa instrucción y no se copia a la imagen final. Docker build secrets.

No ejecutes comandos que impriman el secreto en la salida del build.

Fijar imágenes base por digest

Las etiquetas como golang:1.26 pueden cambiar. En producción, registra el digest aprobado:

FROM golang:1.26@sha256:DIGEST_VERIFICADO AS build

Consulta los manifiestos disponibles:

docker buildx imagetools inspect golang:1.26

No inventes ni reutilices un digest de otra arquitectura. Automatiza la revisión de actualizaciones para no quedar fijado indefinidamente a una imagen vulnerable.

Evitar copias excesivas entre etapas

Esta instrucción es precisa:

COPY --from=build /out/app /app

Estas variantes pueden reintroducir archivos innecesarios:

COPY --from=build / /
COPY --from=build /src /app

Copia artefactos concretos desde un directorio de salida conocido. Inspecciona qué produce el compilador antes de definir el COPY.

Problemas frecuentes

COPY --from no encuentra el archivo

Comprueba el nombre de la etapa y la ruta generada:

docker build \
  --target build \
  -f Dockerfile.multi \
  -t app-build:debug \
  .

Después inspecciona /out dentro de esa etapa.

La imagen multi-stage no es mucho más pequeña

Revisa:

  • Imagen base del runtime.
  • Archivos copiados desde la etapa build.
  • Binarios con símbolos.
  • Dependencias dinámicas.
  • Artefactos duplicados.
  • Uso accidental de la etapa build como etapa final.
docker history app:multi

Aparece exec format error

El binario fue compilado para otra arquitectura o sistema operativo. Comprueba:

docker image inspect app:multi \
  --format '{{.Os}}/{{.Architecture}}'

Para builds multiplataforma, utiliza --platform y los argumentos proporcionados por BuildKit.

Aparece x509: certificate signed by unknown authority

La imagen scratch no contiene certificados CA. Incorpora el bundle necesario o cambia a una imagen runtime mínima con certificados actualizables.

La compilación no reutiliza caché

Revisa el orden de COPY, el contenido de .dockerignore y qué archivos cambian antes de las instrucciones costosas.

Los cache mounts aceleran descargas y compilación, pero no corrigen una invalidación causada por copiar todo el proyecto demasiado pronto.

La aplicación requiere CGO

Un binario creado con CGO puede depender del cargador y de librerías que no existen en scratch. Utiliza una base compatible o copia conscientemente las dependencias dinámicas requeridas.

El contenedor no tiene shell

Es el comportamiento esperado de scratch. Usa una etapa debug, contenedores efímeros de depuración o las herramientas del orquestador, sin modificar la imagen de producción.

Se filtró un secreto durante el build

Revoca inmediatamente la credencial. Elimina su uso en ARG, ENV, COPY o comandos que la impriman, limpia los caches afectados según tu entorno y reconstruye desde una fuente segura.

Seguridad y operación

Multi-stage ayuda a:

  • Reducir paquetes distribuibles.
  • Eliminar compiladores y shells de producción.
  • Disminuir tiempo de transferencia.
  • Acelerar despliegues y escalado.
  • Reducir componentes que requieren parches.

No reemplaza:

  • Análisis de vulnerabilidades.
  • Firma y verificación de imágenes.
  • SBOM y procedencia.
  • Ejecución no root.
  • Filesystem de solo lectura.
  • Límites de recursos.
  • Seccomp, AppArmor o capabilities.
  • Actualización periódica de dependencias.

Analiza la imagen final, no solo la etapa build.

Cómo revertir un despliegue

Cambiar el Dockerfile no modifica contenedores que ya están ejecutándose. Publica la imagen multi-stage con una etiqueta o digest nuevo y conserva la referencia anterior.

Para revertir, despliega el digest previamente validado:

registro.ejemplo.com/app@sha256:DIGEST_ANTERIOR

Después verifica salud, logs y tráfico.

No utilices una etiqueta mutable como latest como único mecanismo de rollback. Tampoco uses docker image prune durante la validación: eliminar cachés o imágenes locales no restaura una versión anterior.

Buenas prácticas

  • Nombra todas las etapas.
  • Copia únicamente artefactos concretos.
  • Usa .dockerignore.
  • Ordena capas de estable a variable.
  • Ejecuta tests antes de generar el artefacto final.
  • Utiliza cache mounts para dependencias.
  • Usa secret mounts para credenciales.
  • Ejecuta runtime con un UID no root.
  • Fija bases por digest y actualízalas de forma controlada.
  • Mide tamaño, funcionalidad y vulnerabilidades por separado.
  • Conserva una etapa debug fuera de producción.
  • Despliega y revierte mediante digests inmutables.

Conclusión

Un Dockerfile multi-stage reduce una imagen porque separa el entorno de compilación del entorno de ejecución. La optimización no consiste en acumular comandos de limpieza, sino en comenzar la imagen final desde una nueva base y copiar únicamente el artefacto necesario.

La implementación queda validada cuando la imagen multi-stage es menor que la monolítica, ejecuta el mismo comportamiento, no contiene herramientas de build ni secretos y puede desplegarse y revertirse mediante referencias inmutables.

Cloud Serversby Donweb

Todo el poder de la nube a tus proyectos y aplicaciones.
Alojamiento ultra rápido, escalable y con alta disponibilidad.

  • Performance que te sorprenderá
  • Rápida escalabilidad y sin limitaciones
  • Arquitectura de alta disponibilidad
  • Soporte experto y ejecutivos de cuenta
  • Pagos en tu moneda y facturación local

Descubre la mejor solución de Cloud Hosting