
Docker Best Practices: 16 Consejos para Contenedores Listos para Produccion
Guia completa de mejores practicas Docker para optimizar tus Dockerfiles, asegurar tus contenedores y reducir el tamano de las imagenes. Ejemplos practicos y consejos de produccion.
Docker Best Practices: 16 Consejos para Contenedores Listos para Produccion
La mayoria de los Dockerfiles que reviso comparten los mismos problemas: imagenes de 2 GB cuando 200 MB serian suficientes, builds de 15 minutos que podrian tomar 30 segundos, contenedores ejecutandose como root sin ninguna razon valida.
El contenedor funciona localmente, las pruebas pasan, pero una vez en produccion, los problemas se acumulan: tiempos de despliegue excesivos, vulnerabilidades de seguridad, consumo de memoria explosivo.
La diferencia entre un Dockerfile que "funciona" y un Dockerfile listo para produccion a menudo se reduce a una docena de mejores practicas. Estas practicas no son complicadas, pero requieren entender como funciona Docker internamente: sistema de capas, cache de build, aislamiento de procesos.
En este articulo, descubrira las 16 mejores practicas esenciales de Docker para pasar de imagenes amateurs a contenedores profesionales. Cubriremos optimizacion de Dockerfiles, seguridad, rendimiento y patrones de produccion.
Este articulo presenta cada practica de forma concisa. Para profundizar y practicar en entornos reales, Train With Docker ofrece escenarios "Docker Best Practices" que lo guian paso a paso en la implementacion de cada concepto.
Optimizacion de Dockerfiles#
1. Use multi-stage builds#
Los multi-stage builds son probablemente la tecnica mas impactante para reducir el tamano de sus imagenes. La idea: separar el entorno de build del entorno de ejecucion.
# Etapa 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Etapa 2: Produccion
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]La primera etapa contiene todo lo necesario para compilar (TypeScript, devDependencies, fuentes). La segunda etapa conserva solo lo estrictamente necesario: codigo compilado y dependencias de produccion.
Ganancia tipica
Un proyecto Node.js TypeScript a menudo pasa de 1.2 GB a 150 MB con un multi-stage build bien configurado.
2. Ordene sus instrucciones para maximizar el cache#
Docker cachea cada capa. Cuando una instruccion cambia, todas las instrucciones siguientes se reconstruyen. El orden de sus instrucciones impacta directamente la velocidad de sus builds.
# Malo: el cache se invalida con cada cambio de codigo
COPY . .
RUN npm ci
RUN npm run build
# Bueno: las dependencias se cachean por separado
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run buildLa regla: coloque los elementos que raramente cambian primero (dependencias del sistema, luego paquetes, luego codigo fuente).
3. Minimice el numero de capas#
Cada instruccion RUN, COPY y ADD crea una nueva capa. Combine los comandos relacionados para reducir el numero de capas.
# Malo: 3 capas
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# Bueno: 1 capa
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*4. Use un archivo .dockerignore#
El archivo .dockerignore funciona como .gitignore: excluye archivos del contexto de build. Sin el, Docker copia todo, incluyendo node_modules, .git y sus archivos de prueba.
node_modules
.git
.gitignore
*.md
.env*
coverage
.nyc_output
distAtencion
Un contexto de build voluminoso ralentiza cada build, incluso si no copia estos archivos explicitamente. Docker aun debe enviarlos al daemon.
5. Elija la imagen base correcta#
La eleccion de la imagen base impacta el tamano final, la seguridad y las dependencias disponibles.
| Imagen | Tamano | Caso de uso |
|---|---|---|
node:20 | ~400 MB | Nunca en produccion |
node:20-slim | ~75 MB | Cuando necesita paquetes del sistema |
node:20-alpine | ~50 MB | Eleccion por defecto |
gcr.io/distroless/nodejs20 | ~188 MB | Seguridad maxima |
Alpine usa musl libc en lugar de glibc. En el 95% de los casos, funciona perfectamente. Para el 5% restante (ciertos binarios nativos), use -slim.
Las imagenes distroless de Google son mas pesadas que Alpine, pero no contienen shell, gestor de paquetes ni herramientas del sistema. Solo el runtime (aqui Node.js) y sus dependencias estan presentes. Resultado: menos superficie de ataque y ninguna forma para que un atacante ejecute comandos en el contenedor.
Seguridad de Contenedores#
6. Nunca ejecute como root#
Por defecto, los contenedores Docker se ejecutan como root. Este es un problema de seguridad importante: si un atacante compromete su aplicacion, tiene privilegios de root en el contenedor.
FROM node:20-alpine
# Crear un usuario no-root
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
WORKDIR /app
COPY --chown=nextjs:nodejs . .
# Cambiar de usuario antes de CMD
USER nextjs
CMD ["node", "index.js"]Verificacion
Puede verificar el usuario actual con docker exec <container> whoami. Si devuelve root, tiene un problema.
7. Limite las capabilities de Linux#
Por defecto, Docker otorga a sus contenedores un conjunto de capabilities de Linux (permisos del sistema). La mayoria son innecesarias para una aplicacion estandar y representan un riesgo de seguridad.
# Elimine todas las capabilities y agregue solo las necesarias
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myappCapabilities mas comunes:
| Capability | Uso |
|---|---|
NET_BIND_SERVICE | Escuchar en un puerto < 1024 |
CHOWN | Cambiar propietario de archivos |
SETUID / SETGID | Cambiar usuario/grupo |
SYS_PTRACE | Depurar procesos (evitar en prod) |
Principio de minimo privilegio
Siempre comience con --cap-drop=ALL, luego agregue solo las capabilities que su aplicacion realmente necesita. Si su app falla, los logs le indicaran que capability falta.
Para Docker Compose:
services:
app:
image: myapp
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE8. Use imagenes base minimas#
Cuantos mas paquetes contiene una imagen, mayor es su superficie de ataque. Las imagenes distroless de Google contienen solo su aplicacion y sus dependencias de runtime, sin shell ni herramientas del sistema.
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app/dist /app
WORKDIR /app
CMD ["index.js"]La desventaja: imposible hacer docker exec para depurar. Esto es intencional. En produccion, no deberia necesitar un shell en sus contenedores.
9. Escanee sus imagenes en busca de vulnerabilidades#
Las imagenes Docker pueden contener CVEs (vulnerabilidades conocidas) en sus paquetes del sistema o dependencias.
# Con Docker Scout (integrado en Docker Desktop)
docker scout cves <image>
# Con Trivy (open source)
trivy image <image>
# Con Snyk
snyk container test <image>Integre estos escaneos en su CI/CD. Un escaneo que descubre una vulnerabilidad critica deberia bloquear el despliegue.
10. Maneje sus secretos correctamente#
Nunca ponga secretos en su Dockerfile o imagen. Permaneceran en el historial de capas.
# NUNCA: el secreto permanece en la imagen
ENV DATABASE_URL=postgres://user:password@host/db
# MEJOR: use secretos de Docker (build-time)
# El secreto se monta temporalmente en /run/secrets/ durante el build
# Nunca se escribe en una capa de la imagen
RUN --mount=type=secret,id=db_url \
export DATABASE_URL=$(cat /run/secrets/db_url) && \
npm run migrate
# Para construir con este secreto:
# docker build --secret id=db_url,src=.env .
# EN PRODUCCION: pase los secretos en runtime
docker run -e DATABASE_URL=$DATABASE_URL myappPara Docker Swarm y Kubernetes, use sus mecanismos nativos de secretos.
Rendimiento y Tamano de Imagenes#
11. Limpie los caches en la misma capa#
Los gestores de paquetes dejan caches. Si los limpia en una capa separada, el cache permanece en las capas anteriores.
# Python
RUN pip install --no-cache-dir -r requirements.txt
# Node.js
RUN npm ci --only=production && npm cache clean --force
# Debian/Ubuntu
RUN apt-get update && \
apt-get install -y --no-install-recommends package && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*12. Use COPY en lugar de ADD#
COPY hace exactamente lo que su nombre indica: copiar archivos. ADD hace lo mismo, pero extrae automaticamente archivos (.tar, .gz) y puede descargar archivos desde una URL. Estos comportamientos implicitos hacen el Dockerfile menos legible y pueden introducir vulnerabilidades de seguridad si una URL esta comprometida o si un archivo contiene archivos inesperados. Prefiera COPY por su previsibilidad.
# Prefiera
COPY ./src /app/src
# Evite (a menos que necesite extraer un archivo)
ADD ./src /app/srcMejores Practicas de Build#
13. Agregue labels para metadatos#
Los labels ayudan a identificar y documentar sus imagenes.
LABEL org.opencontainers.image.title="Mi Aplicacion"
LABEL org.opencontainers.image.description="API Backend"
LABEL org.opencontainers.image.version="1.2.3"
LABEL org.opencontainers.image.authors="[email protected]"
LABEL org.opencontainers.image.source="https://github.com/org/repo"Estos labels siguen la especificacion OCI y son reconocidos por Docker Hub, GitHub Container Registry y otros registros.
14. Implemente health checks#
Un health check permite a Docker (y a los orquestadores) verificar que su aplicacion esta realmente funcionando.
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1Sin un health check, Docker considera un contenedor "healthy" tan pronto como inicia, incluso si la aplicacion falla despues de 5 segundos.
15. Entienda la diferencia entre ENTRYPOINT y CMD#
- ENTRYPOINT: define el comando principal (raramente sobrescrito)
- CMD: define los argumentos por defecto (facilmente sobrescritos)
# Patron recomendado
ENTRYPOINT ["node"]
CMD ["index.js"]
# Permite hacer:
docker run myapp # Ejecuta node index.js
docker run myapp other-script.js # Ejecuta node other-script.jsUse la forma exec ["cmd", "arg"] en lugar de la forma shell cmd arg. La forma exec permite un mejor manejo de senales.
Logging y Observabilidad#
16. Escriba sus logs en stdout/stderr#
Docker captura automaticamente todo lo que sale por stdout y stderr. No escriba en archivos de log.
// Bueno: stdout
console.log(JSON.stringify({ level: 'info', message: 'User logged in', userId: 123 }));
// Malo: archivo
fs.appendFileSync('/var/log/app.log', 'User logged in\n');El formato JSON estructura sus logs para herramientas de agregacion (ELK, Datadog, CloudWatch).
# No redirija logs a archivos
CMD ["node", "index.js"]
# No esto:
CMD ["node", "index.js", ">", "/var/log/app.log"]Orquestacion y Produccion#
Una vez que sus imagenes estan optimizadas, algunas practicas adicionales se aplican en runtime.
Limite los recursos para evitar que un contenedor monopolice el servidor:
docker run --memory="512m" --cpus="0.5" myappConfigure las politicas de reinicio para resiliencia:
docker run --restart=unless-stopped myappUse rolling updates con Docker Swarm o Kubernetes para desplegar sin interrupcion del servicio.
Docker Swarm
Si esta preparando la certificacion DCA o quiere profundizar en Docker Swarm, Train With Docker ofrece escenarios practicos con clusters preconfigurados.
Conclusion#
Estas 16 mejores practicas de Docker cubren los aspectos esenciales: optimizacion de builds, seguridad, rendimiento y preparacion para produccion. Aplicadas sistematicamente, transforman Dockerfiles amateurs en configuraciones profesionales.
Lo mas importante: entender por que existe cada practica. Los multi-stage builds reducen el tamano porque separan build y runtime. El cache funciona por capa, por lo que el orden importa. Los contenedores no-root limitan el impacto de un compromiso.
Este articulo pasa por cada practica sin entrar en detalles de implementacion. Para ensuciarse las manos, Train With Docker ofrece escenarios "Docker Best Practices" que le permiten practicar cada concepto en entornos preconfigurados, sin instalar nada localmente.