BG-01 · ONLINE
[ DESCARGAR CV ]
REGISTRO
REG-003
SECCIÓN
S-OPS · OPERACIONES
ARCHIVADO
2026.05.28
CONSULTA
6 MIN
DEVOPSDOCKERGOLINUX

Contenedores de 12 MB: multi-stage para binarios Go

La primera imagen Docker del servicio pesaba 1.1 GB: golang:1.22 completo, código fuente, caché de módulos y el binario. Cada deploy movía un gigabyte por la red para ejecutar un archivo de 18 MB. La versión final pesa 12 MB y arranca en frío notablemente más rápido en ECS.

Multi-stage: compilar en un sitio, ejecutar en otro

Go compila a un binario estático: el contenedor de ejecución no necesita compilador, ni fuente, ni distribución Linux completa. El Dockerfile multi-stage usa la imagen pesada solo para compilar y copia el binario a una base mínima.

SEGMENTO DE CÓDIGO · DOCKERFILE
# ── Etapa 1: compilación ───────────────────────────────
FROM golang:1.22-alpine AS build
WORKDIR /src

# Cachear módulos: solo se reejecuta si cambian go.mod/go.sum
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux \
    go build -ldflags="-s -w" -o /out/api ./cmd/api

# ── Etapa 2: ejecución ─────────────────────────────────
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /out/api /api
USER 65534
ENTRYPOINT ["/api"]

Las tres trampas de scratch

scratch es literalmente nada: ni shell, ni libc, ni certificados. Las tres líneas extra del Dockerfile existen porque cada una me costó una tarde de depuración.

  • Certificados CA: sin ca-certificates.crt, toda llamada HTTPS saliente falla con "x509: certificate signed by unknown authority". El error aparece en runtime, nunca en el build.
  • Zona horaria: sin zoneinfo, time.LoadLocation devuelve error y cualquier lógica de cortes diarios se rompe en silencio hacia UTC.
  • Usuario: scratch no tiene /etc/passwd; USER 65534 (nobody) evita correr como root sin necesitar crear usuarios.

CGO_ENABLED=0 no es decorativo: con cgo activo el binario enlaza dinámicamente contra libc y simplemente no arranca en scratch ("no such file or directory" — el archivo que falta es el intérprete ELF, no tu binario). Y -ldflags="-s -w" recorta símbolos y DWARF: ~30% menos de binario a cambio de stack traces igual de útiles.

Resultado

  • Imagen: 1.1 GB → 12 MB (94× menos).
  • Superficie de ataque: cero paquetes del sistema, cero shell — el escáner de vulnerabilidades pasó de 40+ CVEs a 0.
  • Pull en cold start de ECS: de ~25 s a <2 s.

Si necesitas depurar dentro del contenedor alguna vez, gcr.io/distroless/static es el término medio: sigue sin shell, pero existe una variante :debug con busybox para emergencias. Para este servicio, scratch + buenas métricas externas ha sido suficiente.