Cómo optimizar un Dockerfile: buenas prácticas y errores frecuentes
Llevas semanas trabajando en tu app. La lógica funciona, los tests pasan, el código está limpio.
Pero el Dockerfile... ese lo escribiste en diez minutos copiando un ejemplo de Stack Overflow y nunca más lo tocaste.
El resultado: imágenes de 1.8 GB, builds que tardan una eternidad, secretos expuestos en el historial de capas y procesos corriendo como root sin que nadie lo haya decidido conscientemente.
No es un problema de conocimiento, es un problema de hábitos. Estos son los seis errores más comunes y cómo evitarlos de una vez.
📚 Índice
- Caché de capas en Dockerfile: el error que arruina tus builds
- Imágenes base en Docker: por qué el tamaño importa
- .dockerignore: el archivo que todo Dockerfile necesita
- Seguridad en Docker: nunca ejecutes procesos como root
- Secretos en Dockerfile: el fallo de seguridad más peligroso
- Multi-stage builds: reduce tu imagen Docker de 800 MB a 15 MB
- Checklist: Dockerfile optimizado antes del push
1.Caché de capas en Dockerfile: el error que arruina tus builds
Docker construye imágenes capa a capa. Si una capa cambia, todas las posteriores se reconstruyen desde cero.
Cada vez que modificas un archivo, Docker invalida la caché de npm install y vuelve a descargar todo. En proyectos grandes, eso son minutos perdidos en cada build.
❌ Mal — invalida la caché en cada cambio de código
COPY . .
RUN npm install✅ Bien — la caché de dependencias se reutiliza
COPY package*.json ./
RUN npm install
COPY . .Lo que cambia menos va arriba. Lo que cambia más, abajo. Las dependencias cambian mucho menos que el código fuente.
2.Imágenes base en Docker: por qué el tamaño importa
Usar ubuntu:latest o python:3.11 para una app sencilla es traer un camión de mudanzas para llevar una mochila. Funciona, pero el coste es innecesario.
❌ Mal — imagen completa sin necesidad
FROM python:3.11
# Imagen base: ~900 MB✅ Bien — variantes slim o alpine
FROM python:3.11-slim # ~60 MB
FROM python:3.11-alpine # ~20 MBAlpine usa musl libc en vez de glibc. Algunas librerías nativas pueden fallar o necesitar compilación adicional. Prueba siempre antes de ir a producción.
Fija siempre la versión.FROM python:3.11-slim es reproducible.FROM python:latest es una bomba de relojería.

3. .dockerignore: el archivo que todo Dockerfile necesita
Sin un .dockerignore, Docker envía todo el directorio al daemon en el momento del build: nodemodules, .git, archivos .env, logs, cachés... Todo eso engrosa el contexto y puede filtrarse en la imagen final.
✅ .dockerignore básico para Node.js
node_modules
.git
.env
*.log
dist
coverage
.DS_Store
.vscodeAñadir un .dockerignore es literalmente un archivo de texto con diez líneas. No hay excusa para no tenerlo.
4.Seguridad en Docker: nunca ejecutes procesos como root
Por defecto, los contenedores Docker corren como root. Si alguien explota una vulnerabilidad en tu aplicación tiene acceso root al contenedor. No es el fin del mundo si el aislamiento del host funciona bien, pero es una superficie de ataque que puedes eliminar en dos líneas.
✅ Crea y usa un usuario sin privilegios
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
5.Secretos en Dockerfile: el fallo de seguridad más peligroso
Cada instrucción ENV o ARG queda grabada permanentemente en el historial de capas. Aunque hagas RUN unset SECRET después, el valor sigue siendo visible con docker history. Si la imagen se sube a un registro, es información pública.
❌ Mal — secreto grabado en el historial
ENV DATABASE_URL=postgres://user:password@host/db
ENV API_KEY=sk-supersecret123✅ Bien — BuildKit secrets o inyección en runtime
# En tiempo de build (BuildKit)
RUN --mount=type=secret,id=api_key \
cat /run/secrets/api_key | setup-command
# En runtime: pásalos con -e o desde un .env externo
# Nunca en el Dockerfile.
6.Multi-stage builds: reduce tu imagen Docker de 800 MB a 15 MB
Si compilas código (Go, Rust, TypeScript, Java...) y metes el compilador en la imagen final, estás enviando a producción herramientas que no necesitas y que amplían la superficie de ataque. Los multi-stage builds resuelven esto: compilas en una etapa y copias solo el artefacto final a otra.
✅ Multi-stage build para una app Go
# Stage 1: compilación
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app/server .
# Stage 2: imagen final — sin compilador, sin código fuente
FROM alpine:3.19
COPY --from=builder /app/server /usr/local/bin/server
USER nobody
ENTRYPOINT ["server"]La imagen final puede pasar de 800 MB a menos de 15 MB. No es exageración, es aritmética.

7.Checklist: Dockerfile optimizado antes del push
- ✓ Las dependencias se instalan antes de copiar el código fuente
-
✓
Usas una imagen base
-slimo-alpinedonde es posible -
✓
La versión de la imagen base está fijada (sin
:latest) -
✓
Tienes un
.dockerignoreen el proyecto - ✓ El proceso corre con un usuario sin privilegios
- ✓ No hay secretos hardcodeados en el Dockerfile
- ✓ Si compilas, usas multi-stage builds
-
✓
Tienes
HEALTHCHECKdefinido para producción
Usa hadolint para analizar tu Dockerfile automáticamente. Detecta problemas de seguridad y mejores prácticas antes del build. Se integra fácilmente en cualquier pipeline de CI.

