Docker en producción: errores que nadie te cuenta
⏱️ Tiempo de lectura: 7 minutos
Docker en producción: errores que nadie te cuenta
Recuerdo perfectamente el día que pusimos nuestro primer contenedor en producción. El equipo estaba emocionado, el pipeline verde, todo funcionando en local. Tres horas después, la app estaba caída y yo revisando logs a las 11 de la noche sin entender absolutamente nada. El contenedor 'funcionaba'. El problema era todo lo demás que nadie nos había dicho.
Dockerizar una aplicación es la parte fácil. Hay tutoriales por todos lados, la documentación oficial es decente, y en una tarde tienes tu Dockerfile listo. El problema real aparece cuando ese contenedor tiene que vivir en un servidor real, con tráfico real, y con usuarios que no tienen ninguna paciencia. Eso es lo que voy a contarte: no el ABC de Docker, sino los golpes que te esperan si nadie te advierte a tiempo.
El Dockerfile que parece correcto pero no lo es
Empecemos por algo que casi todo el mundo hace mal al principio: el orden de las instrucciones en el Dockerfile.
La mayoría escribe algo así:
FROM node:18
COPY . .
RUN npm install
CMD ["node", "server.js"]Funciona. Pero cada vez que cambias una línea de código, Docker invalida el caché desde el COPY y reinstala todas las dependencias. En un proyecto con 200 paquetes, eso son minutos de build que se acumulan en cada deploy.
Lo que sí funciona:
FROM node:18
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]Primero copias solo los archivos de dependencias, instalas, y luego copias el resto del código. Así Docker reutiliza el caché de npm install mientras package.json no cambie. Algo tan simple puede reducir tus tiempos de build a la mitad.
El problema de correr como root
Otro clásico: por defecto, tu contenedor corre como root. Eso en local no te importa, pero en producción es una superficie de ataque innecesaria. Si alguien explota una vulnerabilidad en tu app, tiene acceso de root al contenedor.
Agrega esto antes del CMD:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuserDos líneas. Sin drama.
Los límites de recursos que olvidaste configurar
Aquí viene uno que me costó caro. Teníamos un microservicio que procesaba imágenes. Sin límites configurados, cuando llegaba un pico de tráfico, el contenedor consumía toda la RAM disponible del host y empezaba a afectar a otros servicios. El clásico problema del vecino ruidoso.
Cuando usas Docker Compose o Kubernetes, los límites de recursos no son opcionales en producción, son obligatorios:
# docker-compose.yml
services:
api:
image: mi-api:latest
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
memory: 256MEl punto es: sin límites, un contenedor hambriento puede tumbar todo el host. Con límites, al menos el daño está contenido y el resto del sistema sigue respirando.
Health checks: no son decorativos
Docker tiene un mecanismo de health check que mucha gente ignora o configura mal. Sin él, Docker considera que tu contenedor está saludable simplemente porque el proceso no ha muerto. Pero un proceso puede estar vivo y tu app completamente rota por dentro.
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1Y en tu app, ese endpoint /health debe verificar cosas reales: conexión a base de datos, acceso a servicios externos críticos. No solo devolver un 200 con { status: 'ok' } que siempre pasa.
Logs: el caos que nadie planea
En local haces console.log y listo. En producción con diez contenedores corriendo, eso se convierte en un río de texto imposible de analizar.
Lo primero: configura el log driver de Docker. Por defecto usa json-file sin límite de tamaño, lo que significa que tus logs pueden comerse el disco del servidor. Sí, eso pasa. Lo he visto.
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}Eso va en /etc/docker/daemon.json. Simple, pero salva discos.
Ahora bien, si ya tienes varios contenedores, lo que realmente necesitas es centralizar los logs. Herramientas como Loki + Grafana, o el stack ELK, te permiten buscar entre todos los contenedores desde un solo lugar. Cuando estás debuggeando un problema a las 2am, agradecer tener todo en un solo panel no tiene precio.
Variables de entorno: el secreto que no debería ser un secreto
Por favor, no hagas esto:
ENV DB_PASSWORD=miPasswordSuperSecreta123Esa contraseña queda en el historial de la imagen, visible para cualquiera con acceso al registro. Usa Docker Secrets si estás en Swarm, o variables inyectadas en runtime si usas Compose:
environment:
- DB_PASSWORD=${DB_PASSWORD}Y el valor real vive en un .env que nunca, jamás, entra al repositorio.
El deploy que se convierte en pesadilla sin estrategia de rollback
Esto lo aprendí viendo a un colega sudar frío. Hicieron un deploy, algo salió mal, y revertir significaba reconstruir la imagen desde cero porque no tenían tags versionados.
Etiqueta siempre tus imágenes con algo significativo:
docker build -t mi-api:1.4.2 -t mi-api:latest .Así, si el deploy de 1.4.2 falla, volver a 1.4.1 es cuestión de segundos:
docker service update --image mi-api:1.4.1 mi-servicioEn Kubernetes esto es todavía más elegante con kubectl rollout undo, pero el principio es el mismo: necesitas saber exactamente a qué versión vas a volver antes de que algo se rompa, no después.
Redes internas: no expongas lo que no debes
Último punto, y uno que se pasa por alto constantemente. Cuando defines servicios en Docker Compose, por defecto todos pueden comunicarse entre sí en la misma red. Pero si expones puertos innecesariamente al host, estás abriendo puertas que no necesitas abrir.
Tu base de datos no necesita ser accesible desde fuera del contenedor. Solo tu API necesita hablar con ella:
services:
db:
image: postgres:15
# Sin 'ports' aquí — solo accesible internamente
api:
image: mi-api:latest
ports:
- "3000:3000"Sin el mapeo de puertos en db, Postgres solo es accesible para los contenedores en la misma red de Compose. Afuera, invisible.
Antes de irte
Si sientes que cada vez que algo falla en producción no sabes por dónde empezar a buscar, tranquilo. No es que no sepas Docker — es que nadie te mostró la parte que viene después del tutorial. El ecosistema de contenedores tiene una curva de aprendizaje real, y los errores que duelen más son exactamente los que no están en la documentación oficial.
La próxima vez que hagas un deploy, revisa solo una de estas cosas que mencioné. Con el tiempo, todas se vuelven hábito y dejan de ser problemas.
💬 Comentarios