Un Dockerfile básico funciona — uno bien escrito produce imágenes más pequeñas, seguras y reproducibles. Las builds multi-stage son el avance más importante en optimización de imágenes Docker. Esta guía va de principiante a experto.
Por qué importa el tamaño de la imagen
Una imagen de 1 GB tarda mucho en subir al registry, en descargarse en producción y aumenta la superficie de ataque. El objetivo es incluir solo lo estrictamente necesario para correr la aplicación — sin compiladores, sin herramientas de debug, sin archivos temporales.
| Imagen base | Tamaño aprox. | Cuándo usar |
|---|---|---|
| ubuntu | ~78 MB | Compatibilidad máxima, debugging |
| debian:slim | ~75 MB | Producción con apt disponible |
| alpine | ~5 MB | Tamaño mínimo (usa musl, no glibc) |
| scratch | 0 MB | Binarios estáticos Go/Rust |
Multi-stage builds: el cambio de paradigma
Antes de multi-stage, tenías que elegir entre tener todo en una imagen enorme o mantener scripts externos de build complejos. Multi-stage permite múltiples etapas FROM en el mismo Dockerfile — cada una puede ser una imagen diferente — y solo copias lo que necesitas al resultado final.
Ejemplo: aplicación PHP con assets compilados
# Etapa 1: compilar assets de Node.js
FROM node:20-alpine AS assets
WORKDIR /build
COPY package*.json ./
RUN npm ci --only=production
COPY resources/ ./resources/
RUN npm run build
# Etapa 2: instalar dependencias PHP
FROM composer:2 AS composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Etapa 3: imagen final (solo lo necesario)
FROM php:8.3-fpm-alpine AS production
# Solo las extensiones PHP que necesitas
RUN docker-php-ext-install pdo pdo_mysql opcache
WORKDIR /var/www/html
# Copiar solo los artefactos compilados de las etapas anteriores
COPY --from=composer /app/vendor ./vendor
COPY --from=assets /build/public/build ./public/build
COPY . .
# No root
RUN adduser -D -u 1000 appuser && chown -R appuser:appuser /var/www/html
USER appuser
EXPOSE 9000
CMD ["php-fpm"]
Buenas prácticas de Dockerfile
Ordenar instrucciones de menos a más cambiantes
# MAL: si cambias el código, Docker reconstruye desde la instalación de dependencias
FROM node:20-alpine
COPY . /app
RUN npm install
# BIEN: las dependencias se cachean, solo el código se vuelve a copiar
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci # ← cacheado mientras package.json no cambie
COPY . . # ← solo esto se vuelve a copiar en cada build
Un proceso por contenedor
# Cada CMD ejecuta un solo proceso principal — Docker lo monitorea
CMD ["php-fpm", "-F"]
# Si necesitas múltiples procesos, usa un process manager
# o divide en múltiples contenedores coordinados por Compose
.dockerignore: excluir lo innecesario
# .dockerignore
node_modules/
.git/
*.log
.env
storage/logs/*
tests/
*.md
Variables de build con ARG y ENV
# ARG: solo disponible durante el build
ARG APP_VERSION=1.0.0
LABEL version="${APP_VERSION}"
# ENV: disponible también en el contenedor en ejecución
ENV APP_ENV=production \
PHP_OPCACHE_ENABLE=1 \
PHP_MEMORY_LIMIT=256M
Verificar el resultado
# Ver tamaño de todas las imágenes
docker images
# Inspeccionar las capas y su tamaño
docker history mi-app:latest
# Análisis profundo con dive (instalar por separado)
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive mi-app:latest
"Una imagen Docker es tan pequeña como lo que decides no incluir en ella."