Dockerfile: De 1.2GB a 80MB
Un Dockerfile ingenuo para una app Node.js produce una imagen de 1.2GB. Uno bien escrito produce 80MB. La diferencia es entender cómo funcionan las capas de Docker y aplicar unos patrones consistentemente.
El Dockerfile Ingenuo
# NO hagas esto
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]Problemas: imagen Node.js completa (1GB+), copia node_modules si existen localmente, instala devDependencies, sin caché de capas, corre como root.
El Dockerfile de Producción
# Stage 1: Instalar dependencias
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Imagen de producción
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# No corras como root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]Optimizaciones Clave Explicadas
| Técnica | Impacto | Por Qué |
|---|---|---|
node:20-alpine | ~900MB más pequeño | Alpine Linux tiene ~5MB vs ~120MB de Debian |
| Multi-stage build | Sin herramientas de build en imagen final | Solo artefactos de producción se copian al stage final |
npm ci vs npm install | Builds determinísticos | Usa lockfile exactamente, más rápido, sin sorpresas |
| Copiar package.json primero | Mejor caché de capas | Dependencias solo se reinstalan cuando package.json cambia |
USER appuser | Seguridad | Container no corre como root |
--only=production | node_modules más pequeño | Sin devDependencies en producción |
Caché de Capas: El Orden Importa
Docker cachea cada capa. Cuando una capa cambia, todas las subsecuentes se reconstruyen. Así que pon cosas que cambian raramente arriba:
# Raramente cambia → cache hit
FROM node:20-alpine
WORKDIR /app
# Cambia cuando cambian las dependencias
COPY package.json package-lock.json ./
RUN npm ci
# Cambia con cada cambio de código → invalida solo esta capa
COPY . ..dockerignore
Tan importante como .gitignore:
node_modules
.git
.env
*.md
Dockerfile
docker-compose.yml
.next
coverage
tests
.vscode
.DS_StoreSin esto, COPY . . envía todo tu node_modules (e historial git) al daemon Docker, haciendo builds lentos antes siquiera de empezar.
Dockerfile Python
# Build multi-stage Python
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]Dockerfile Go (Imagen Final Diminuta)
# Go compila a binario estático — imagen final puede ser scratch
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# Imagen final: ~10-20MBChecklist de Seguridad
- Nunca corras como root — siempre crea y usa un usuario no-root
- No almacenes secretos en la imagen — usa variables de entorno o secret mounts
- Fija versiones de imagen —
node:20.11.1-alpinenonode:latest - Escanea vulnerabilidades —
docker scout quickviewotrivy image myapp - Usa
.dockerignorepara excluir.env,.gity archivos sensibles
Referencia Docker: Cheatsheet Docker — todos los comandos que necesitas, categorizados y buscables.