De 442 MB a 56 MB: así se adelgaza una imagen Docker sin romperla
Un desarrollador lleva una imagen de Python y Flask de 442 MB a 56,6 MB: base slim, capas ordenadas, Gunicorn y un usuario sin privilegios.

Una imagen Docker de 442 MB puede parecer normal hasta que se mira qué lleva dentro. En un ejercicio publicado estos días, un desarrollador parte de una aplicación Flask de dos rutas y un Dockerfile de manual —base python:3.12, instalación de curl, git y vim, y un COPY . . colocado antes de resolver dependencias— y la deja en 56,6 MB. El disco sin comprimir baja de 1,75 GB a 256 MB. Son reducciones del 87,19 % y del 85,37 %, según las cifras que da el propio autor, que no las ha contrastado con nadie.
Los dos recortes que más pesan
El primer movimiento es cambiar de base. python:3.12 arrastra una Debian completa con compiladores, cabeceras y utilidades que una app web no va a llamar nunca. La alternativa es python:3.12-slim, y el autor descarta Alpine a propósito: usa musl libc en lugar de glibc, y los paquetes de Python con extensiones en C —numpy, cryptography y compañía— suelen llegar sin wheels precompilados para musl, lo que obliga a compilar durante el build o deja bugs de comportamiento en tiempo de ejecución. Para una base Debian slim el ahorro es menor, pero la compatibilidad está garantizada.
El segundo es borrar paquetes y unir capas. Un docker history de la imagen inicial muestra dos capas sospechosas: unos 21,3 MB del apt-get update y unos 53,1 MB de curl, git y vim. Un editor de texto y un cliente de control de versiones no pintan nada dentro de un contenedor en producción; si hace falta depurar, el autor recomienda sidecars efímeros o volúmenes montados. Al eliminar los apt-get, además, desaparecen capas que guardaban la caché de APT para siempre.
El orden de las capas y el resto del recorte
Docker invalida una capa en cuanto cambia algo de lo que depende. Con COPY . . antes de pip install, tocar una línea de app.py tiraba la caché y reinstalaba todo. Invirtiendo el orden —primero requirements.txt y el pip install, después el código, con COPY --chown— reconstruir tras editar la aplicación tarda menos de un segundo, porque la capa pesada sale de caché. A eso se suma un .dockerignore que deja fuera .git, .venv, .pytest_cache, tests y los propios Dockerfile, recortando el contexto de build que se envía al demonio y evitando que archivos sensibles acaben dentro de la imagen.
El penúltimo cambio es sustituir el servidor de desarrollo de Flask por Gunicorn (gunicorn --bind 0.0.0.0:5000 app:app), que aporta concurrencia por workers y gestión de procesos sin engordar la imagen. Flask ya avisa en sus propios logs de que su servidor no es para producción. El último paso es dejar de ejecutar como root, algo que el texto original corta justo cuando empieza a explicarlo, así que del cambio a usuario de sistema solo consta el resultado.
Nada de esto es nuevo para quien lleva años escribiendo Dockerfiles, y el interés está más en el orden de las decisiones que en cada truco por separado. Lo que sí conviene recordar es el efecto acumulado: menos paquetes significa menos superficie de ataque, una imagen que viaja antes por el registro y un pipeline de CI que no recompila lo que no ha cambiado. Cualquiera puede reproducir el ejercicio en una tarde y comprobar con docker history por dónde se le escapa el espacio.
