paint-brush
Optimizar las imágenes de Docker es más que una tarea que se realiza una sola vezpor@aleksandrov
Nueva Historia

Optimizar las imágenes de Docker es más que una tarea que se realiza una sola vez

por Igor Alexandrov17m2025/01/30
Read on Terminal Reader

Demasiado Largo; Para Leer

Este artículo es parte de una serie de publicaciones en las que repasaré cada línea del Dockerfile predeterminado y explicaré las mejores prácticas y optimizaciones. El primer artículo solo abordará la optimización de la reducción del tamaño de las imágenes.
featured image - Optimizar las imágenes de Docker es más que una tarea que se realiza una sola vez
Igor Alexandrov HackerNoon profile picture
0-item


Este artículo es parte de una serie de publicaciones en las que recorreré cada línea del Dockerfile predeterminado de Rails y explicaré las mejores prácticas y optimizaciones.


Las imágenes de Docker se pueden optimizar de diferentes maneras, entre ellas, la reducción del tamaño de la imagen, la optimización del rendimiento de la compilación, las mejores prácticas de seguridad y mantenimiento, y las optimizaciones específicas de la aplicación. En el primer artículo, solo abordaré la optimización de la reducción del tamaño de la imagen y explicaré por qué es importante.

¿Por qué optimizar el tamaño de la imagen?

Como en cualquier otro proceso de desarrollo de software, cada desarrollador enumerará las razones por las que desea que sus compilaciones de Docker sean más rápidas. Enumeraré las razones que para mí son las más importantes.

Compilaciones e implementaciones más rápidas

Las imágenes más pequeñas se crean más rápido porque se necesitan procesar menos archivos y capas. Esto mejora la productividad del desarrollador, especialmente durante los ciclos de desarrollo iterativos. Las imágenes más pequeñas tardan menos en enviarse a un registro y extraerse de él durante las implementaciones. Esto es especialmente crítico en los procesos de CI/CD donde los contenedores se crean e implementan con frecuencia.

Reducción de costes de almacenamiento y uso de ancho de banda de red

Las imágenes más pequeñas consumen menos almacenamiento en los registros de contenedores, las máquinas de desarrollo locales y los servidores de producción. Esto reduce los costos de infraestructura, especialmente para implementaciones a gran escala. Las imágenes más pequeñas utilizan menos ancho de banda cuando se transfieren entre servidores, lo que es especialmente importante cuando se crean imágenes localmente o en procesos de CI/CD y se envían a un registro.


“Gastamos 3,2 millones de dólares en la nube en 2022... Ahorraremos unos 7 millones de dólares en gastos de servidores en cinco años desde que abandonemos la nube”. David Heinemeier Hansson — HEY World

Rendimiento y seguridad mejorados

Las imágenes más pequeñas requieren menos recursos (por ejemplo, CPU, RAM) para cargarse y ejecutarse, lo que mejora el rendimiento general de las aplicaciones en contenedores. Los tiempos de inicio más rápidos significan que sus servicios están listos más rápidamente, lo que es crucial para los sistemas de escalabilidad y alta disponibilidad. Las imágenes base mínimas como alpine o debian-slim contienen menos paquetes preinstalados, lo que reduce el riesgo de que se explote software innecesario o sin parches.


Además de todo lo mencionado anteriormente, eliminar archivos y herramientas innecesarios minimiza las distracciones al diagnosticar problemas y conduce a una mejor capacidad de mantenimiento y una menor deuda técnica.

Inspección de imágenes de Docker

Para obtener diferentes parámetros de la imagen, incluido el tamaño, puede mirar Docker Desktop o ejecutar el comando docker images en la terminal.


 ➜ docker images REPOSITORY TAG IMAGE ID CREATED SIZE kamal-dashboard latest 673737b771cd 2 days ago 619MB kamal-proxy latest 5f6cd8983746 6 weeks ago 115MB docs-server latest a810244e3d88 6 weeks ago 1.18GB busybox latest 63cd0d5fb10d 3 months ago 4.04MB postgres latest 6c9aa6ecd71d 3 months ago 456MB postgres 16.4 ced3ad69d60c 3 months ago 453MB


Conocer el tamaño de la imagen no le permite tener una visión completa. No sabe qué hay dentro de la imagen, cuántas capas tiene ni qué tamaño tiene cada capa. Una capa de imagen de Docker es una capa de sistema de archivos inmutable y de solo lectura que es un componente de una imagen de Docker. Cada capa representa un conjunto de cambios realizados en el sistema de archivos de la imagen, como agregar archivos, modificar configuraciones o instalar software.


Las imágenes de Docker se crean de forma incremental, capa por capa, y cada capa corresponde a una instrucción en el Dockerfile . Para obtener las capas de la imagen, puedes ejecutar el comando docker history .


 ➜ docker history kamal-dashboard:latest IMAGE CREATED CREATED BY SIZE COMMENT 673737b771cd 4 days ago CMD ["./bin/thrust" "./bin/rails" "server"] 0B buildkit.dockerfile.v0 <missing> 4 days ago EXPOSE map[80/tcp:{}] 0B buildkit.dockerfile.v0 <missing> 4 days ago ENTRYPOINT ["/rails/bin/docker-entrypoint"] 0B buildkit.dockerfile.v0 <missing> 4 days ago USER 1000:1000 0B buildkit.dockerfile.v0 <missing> 4 days ago RUN /bin/sh -c groupadd --system --gid 1000 … 54MB buildkit.dockerfile.v0 <missing> 4 days ago COPY /rails /rails # buildkit 56.2MB buildkit.dockerfile.v0 <missing> 4 days ago COPY /usr/local/bundle /usr/local/bundle # b… 153MB buildkit.dockerfile.v0 <missing> 4 days ago ENV RAILS_ENV=production BUNDLE_DEPLOYMENT=1… 0B buildkit.dockerfile.v0 <missing> 4 days ago RUN /bin/sh -c apt-get update -qq && apt… 137MB buildkit.dockerfile.v0 <missing> 4 days ago WORKDIR /rails 0B buildkit.dockerfile.v0 <missing> 3 weeks ago CMD ["irb"] 0B buildkit.dockerfile.v0 <missing> 3 weeks ago RUN /bin/sh -c set -eux; mkdir "$GEM_HOME";… 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV PATH=/usr/local/bundle/bin:/usr/local/sb… 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV BUNDLE_SILENCE_ROOT_WARNING=1 BUNDLE_APP… 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV GEM_HOME=/usr/local/bundle 0B buildkit.dockerfile.v0 <missing> 3 weeks ago RUN /bin/sh -c set -eux; savedAptMark="$(a… 78.1MB buildkit.dockerfile.v0 <missing> 3 weeks ago ENV RUBY_DOWNLOAD_SHA256=018d59ffb52be3c0a6d… 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV RUBY_DOWNLOAD_URL=https://cache.ruby-lan… 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV RUBY_VERSION=3.4.1 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV LANG=C.UTF-8 0B buildkit.dockerfile.v0 <missing> 3 weeks ago RUN /bin/sh -c set -eux; mkdir -p /usr/loca… 19B buildkit.dockerfile.v0 <missing> 3 weeks ago RUN /bin/sh -c set -eux; apt-get update; a… 43.9MB buildkit.dockerfile.v0 <missing> 3 weeks ago # debian.sh --arch 'arm64' out/ 'bookworm' '… 97.2MB debuerreotype 0.15


Dado que ya he proporcionado la teoría sobre las imágenes y las capas, es hora de explorar el Dockerfile . A partir de Rails 7.1, el Dockerfile se genera con la nueva aplicación Rails. A continuación, se muestra un ejemplo de cómo podría verse.


 # syntax=docker/dockerfile:1 # check=error=true # Make sure RUBY_VERSION matches the Ruby version in .ruby-version ARG RUBY_VERSION=3.4.1 FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here WORKDIR /rails # Install base packages # Replace libpq-dev with sqlite3 if using SQLite, or libmysqlclient-dev if using MySQL RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Set production environment ENV RAILS_ENV="production" \ BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" \ BUNDLE_WITHOUT="development" # Throw-away build stage to reduce size of final image FROM base AS build # Install packages needed to build gems RUN apt-get update -qq && \ apt-get install --no-install-recommends -y build-essential curl git pkg-config libyaml-dev && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Install application gems COPY Gemfile Gemfile.lock ./ RUN bundle install && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ bundle exec bootsnap precompile --gemfile # Copy application code COPY . . # Precompile bootsnap code for faster boot times RUN bundle exec bootsnap precompile app/ lib/ # Precompiling assets for production without requiring secret RAILS_MASTER_KEY RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile # Final stage for app image FROM base # Copy built artifacts: gems, application COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" COPY --from=build /rails /rails # Run and own only the runtime files as a non-root user for security RUN groupadd --system --gid 1000 rails && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ chown -R rails:rails db log storage tmp USER 1000:1000 # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] # Start server via Thruster by default, this can be overwritten at runtime EXPOSE 80 CMD ["./bin/thrust", "./bin/rails", "server"]


A continuación, proporcionaré una lista de enfoques y reglas que se aplicaron al Dockerfile anterior para hacer que el tamaño de la imagen final sea eficiente.

Optimizar instalaciones de paquetes

Estoy seguro de que solo mantienes el software necesario en tu máquina de desarrollo local. Lo mismo debería aplicarse a las imágenes de Docker. En los ejemplos a continuación, empeoraré constantemente el Dockerfile extraído del Dockerfile de Rails anterior. Lo mencionaré como una versión original Dockerfile .

Regla n.° 1: use imágenes base mínimas

 FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base


La imagen base es el punto de partida del Dockerfile . Es la imagen que se utiliza para crear el contenedor. La imagen base es la primera capa del Dockerfile y es la única capa que no crea el propio Dockerfile .


La imagen base se especifica con el comando FROM , seguido del nombre de la imagen y la etiqueta. La etiqueta es opcional y, si no se especifica, se utiliza la etiqueta latest . La imagen base puede ser cualquier imagen disponible en Docker Hub o en cualquier otro registro.


En el Dockerfile , usamos la imagen ruby con la etiqueta 3.4.1-slim . La imagen ruby es la imagen Ruby oficial disponible en Docker Hub. La etiqueta 3.4.1-slim es una versión reducida de la imagen Ruby que se basa en la imagen debian-slim . Mientras que la imagen debian-slim es una versión mínima de la imagen Debian Linux que está optimizada para el tamaño. Observa la tabla a continuación para tener una idea de qué tan pequeña es la imagen slim .


 ➜ docker images --filter "reference=ruby" REPOSITORY TAG IMAGE ID CREATED SIZE ruby 3.4.1-slim 0bf957e453fd 5 days ago 219MB ruby 3.4.1-alpine cf9b1b8d4a0c 5 days ago 99.1MB ruby 3.4.1-bookworm 1e77081540c0 5 days ago 1.01GB


A partir de enero de 2024, la versión actual de Debian se llama bookworm y la anterior es bullseye .


219 MB en lugar de 1 GB: una diferencia enorme. Pero, ¿qué pasa si la imagen alpine es incluso más pequeña? La imagen alpine se basa en la distribución de Linux Alpine, que es una distribución de Linux superligera que está optimizada para el tamaño y la seguridad. Alpine utiliza la biblioteca musl (en lugar de glibc ) y busybox (un conjunto compacto de utilidades de Unix) en lugar de sus contrapartes GNU. Si bien es técnicamente posible utilizar la imagen alpine para ejecutar Rails, no lo abordaré en este artículo.

Regla n.° 2: minimizar las capas

 RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives


Cada instrucción RUN , COPY y FROM en Dockerfile crea una nueva capa. Cuantas más capas tenga, mayor será el tamaño de la imagen. Por eso, la mejor práctica es combinar varios comandos en una sola instrucción RUN . Para ilustrar este punto, veamos el siguiente ejemplo.


 # syntax=docker/dockerfile:1 # check=error=true # Make sure RUBY_VERSION matches the Ruby version in .ruby-version ARG RUBY_VERSION=3.4.1 FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base RUN apt-get update -qq RUN apt-get install --no-install-recommends -y curl RUN apt-get install --no-install-recommends -y libjemalloc2 RUN apt-get install --no-install-recommends -y libvips RUN apt-get install --no-install-recommends -y libpq-dev RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives CMD ["echo", "Whalecome!"]


He dividido la instrucción RUN en varias líneas, lo que obviamente las hace más legibles para los humanos . Pero, ¿cómo afectará esto al tamaño de la imagen? Construyamos la imagen y veámosla.


 ➜ time docker build -t no-minimize-layers --no-cache -f no-minimize-layers.dockerfile . 0.31s user 0.28s system 2% cpu 28.577 total


Se necesitaron 28 segundos para construir la imagen, mientras que construir la versión original con capas minimizadas toma solo 19 segundos ( casi un 33% más rápido ).


 ➜ time docker build -t original --no-cache -f original.dockerfile . 0.25s user 0.28s system 2% cpu 19.909 total


Vamos a comprobar el tamaño de las imágenes.


 ➜ docker images --filter "reference=*original*" --filter "reference=*no-minimize*" REPOSITORY TAG IMAGE ID CREATED SIZE original latest f1363df79c8a 8 seconds ago 356MB no-minimize-layers latest ad3945c8a8ee 43 seconds ago 379MB


La imagen con capas minimizadas es 23 MB más pequeña que la que no tiene capas minimizadas. Esto supone una reducción del tamaño del 6 % . Aunque parece una diferencia pequeña en este ejemplo, la diferencia será mucho mayor si divide todas las instrucciones RUN en varias líneas.

Regla n.° 3: Instale solo lo necesario

De forma predeterminada, apt-get install instala los paquetes recomendados y los paquetes que le solicitaste que instalara. La opción --no-install-recommends le indica apt-get que instale solo los paquetes que se especifican explícitamente y no los recomendados.


 ➜ time docker build -t without-no-install-recommends --no-cache -f without-no-install-recommends.dockerfile . 0.33s user 0.30s system 2% cpu 29.786 total ➜ docker images --filter "reference=*original*" --filter "reference=*recommends*" REPOSITORY TAG IMAGE ID CREATED SIZE without-no-install-recommends latest 41e6e37f1e2b 3 minutes ago 426MB minimize-layers latest dff22c85d84c 17 minutes ago 356MB


Como puede ver, la imagen sin --no-install-recommends es 70 MB más grande que la original . Esto representa un aumento del 16 % en el tamaño .


Utilice la utilidad Dive para ver qué archivos se agregaron a la imagen; lea más sobre ello al final del artículo.

Regla n.° 4: Limpiar después de las instalaciones

El Dockerfile original incluye el comando rm -rf /var/lib/apt/lists/* /var/cache/apt/archives después del comando apt-get install . Este comando elimina las listas de paquetes y los archivos que ya no son necesarios después de la instalación. Veamos cómo afecta al tamaño de la imagen. Para lograrlo, crearé un nuevo Dockerfile sin el comando de limpieza .


 RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev


Construir las imágenes lleva casi el mismo tiempo que la original, lo cual tiene sentido.


 ➜ time docker build -t without-cleaning --no-cache -f without-cleaning.dockerfile . 0.28s user 0.30s system 2% cpu 21.658 total


Vamos a comprobar el tamaño de las imágenes.


 ➜ docker images --filter "reference=*original*" --filter "reference=*cleaning*" REPOSITORY TAG IMAGE ID CREATED SIZE without-cleaning latest 52884fe50773 2 minutes ago 375MB original latest f1363df79c8a 16 minutes ago 356MB


La imagen sin limpieza es 19 MB más grande que la que tiene limpieza, esto es un aumento del 5% en el tamaño .

El peor escenario

¿Qué sucede si no se aplican las cuatro optimizaciones mencionadas anteriormente? Creemos un nuevo Dockerfile sin ninguna optimización y construyamos la imagen.


 # syntax=docker/dockerfile:1 # check=error=true ARG RUBY_VERSION=3.4.1 FROM docker.io/library/ruby:$RUBY_VERSION AS base RUN apt-get update -qq RUN apt-get install -y curl RUN apt-get install -y libjemalloc2 RUN apt-get install -y libvips RUN apt-get install -y libpq-dev CMD ["echo", "Whalecome!"]


 ➜ time docker build -t without-optimizations --no-cache -f without-optimizations.dockerfile . 0.46s user 0.45s system 1% cpu 1:02.21 total


Vaya, tardó más de un minuto construir la imagen.


 ➜ docker images --filter "reference=*original*" --filter "reference=*without-optimizations*" REPOSITORY TAG IMAGE ID CREATED SIZE without-optimizations latest 45671929c8e4 2 minutes ago 1.07GB original latest f1363df79c8a 27 hours ago 356MB


La imagen sin optimizaciones es 714 MB más grande que la original, esto es un aumento de tamaño del 200% . Esto demuestra claramente lo importante que es optimizar el Dockerfile , las imágenes más grandes tardan más en compilarse y consumen más espacio en disco.

Utilice siempre .dockerignore

El archivo .dockerignore es similar al archivo .gitignore que utiliza Git. Se utiliza para excluir archivos y directorios del contexto de la compilación. El contexto es el conjunto de archivos y directorios que se envían al demonio de Docker cuando se crea una imagen. El contexto se envía al demonio de Docker como un archivo tar, por lo que es importante mantenerlo lo más pequeño posible.


Si, por alguna razón, no tienes el archivo .dockerignore en tu proyecto, puedes crearlo manualmente. Te sugiero que uses la plantilla oficial de archivo .dockerignore de Rails como punto de partida. A continuación, se muestra un ejemplo de cómo podría verse.


 # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. # Ignore git directory. /.git/ /.gitignore # Ignore bundler config. /.bundle # Ignore all environment files. /.env* # Ignore all default key files. /config/master.key /config/credentials/*.key # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep # Ignore pidfiles, but keep the directory. /tmp/pids/* !/tmp/pids/.keep # Ignore storage (uploaded files in development and any SQLite databases). /storage/* !/storage/.keep /tmp/storage/* !/tmp/storage/.keep # Ignore assets. /node_modules/ /app/assets/builds/* !/app/assets/builds/.keep /public/assets # Ignore CI service files. /.github # Ignore development files /.devcontainer # Ignore Docker-related files /.dockerignore /Dockerfile*


Tener un archivo .dockerfile en el proyecto no solo permite excluir archivos y directorios innecesarios (por ejemplo, flujos de trabajo de GitHub de la carpeta .github o dependencias de JavaScript de node_modules ) del contexto. También ayuda a evitar agregar información confidencial accidentalmente a la imagen. Por ejemplo, el archivo .env que contiene las variables de entorno o el archivo master.key que se usa para descifrar las credenciales.

Utilice Dive

Todas las optimizaciones mencionadas anteriormente pueden parecer obvias cuando se explican. ¿Qué hacer si ya tienes una imagen enorme y no sabes por dónde empezar?


Mi herramienta favorita y más útil es Dive . Dive es una herramienta TUI para explorar una imagen de Docker, el contenido de las capas y descubrir formas de reducir el tamaño de la imagen. Dive se puede instalar con el administrador de paquetes del sistema o se puede usar su imagen oficial de Docker para ejecutarlo. Usemos la imagen de nuestro peor escenario.


 docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest without-optimizations 


Herramienta de inspección de capas de Dive Docker

En la captura de pantalla anterior, puede ver la inspección de nuestra imagen menos óptima. Dive muestra el tamaño de cada capa, el tamaño total de la imagen y los archivos que se cambiaron (agregaron, modificaron o eliminaron) en cada capa. Para mí, esta es la característica más útil de Dive. Al enumerar los archivos en el panel derecho, puede identificar fácilmente los archivos que no son necesarios y eliminar los comandos que los agregan a la imagen.


Una cosa que realmente me encanta de Dive es que, además de tener una interfaz de usuario de terminal, también puede proporcionar una salida compatible con CI, que también puede ser eficaz en un desarrollo local. Para usarlo, ejecute Dive con la variable de entorno CI establecida en true , la salida del comando se muestra en la captura de pantalla a continuación.


 docker run -e CI=true --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest without-optimizations 


Salida compatible con CI Dive

Mi preferencia personal es usar Dive de forma programada, por ejemplo, una vez a la semana, para asegurarme de que las imágenes sigan en buen estado. En los próximos artículos, hablaré sobre los flujos de trabajo automatizados que utilizo para verificar mi Dockerfile, incluidos Dive y Hadolint .

No aplastes las capas

Un enfoque que he visto para minimizar el tamaño de la imagen es intentar comprimir las capas. La idea era combinar varias capas en una sola para reducir el tamaño de la imagen. Docker tenía una opción experimental --squash , además de esto, había herramientas de terceros como docker-squash .


Si bien este enfoque funcionó en el pasado, actualmente está en desuso y no se recomienda su uso. La combinación de capas destruyó la característica fundamental de Docker de almacenamiento en caché de capas. Además de eso, al usar --squash podría incluir involuntariamente archivos confidenciales o temporales de capas anteriores en la imagen final. Este es un enfoque de todo o nada que carece de un control detallado.


En lugar de comprimir las capas, se recomienda utilizar compilaciones de varias etapas. Rails Dockerfile ya utiliza compilaciones de varias etapas. Explicaré cómo funciona en el próximo artículo.

Conclusiones

La optimización de imágenes de Docker, al igual que cualquier otra optimización, no se puede hacer una sola vez y olvidarse . Es un proceso continuo que requiere controles y mejoras regulares. Intenté cubrir los conceptos básicos, pero es fundamental conocerlos y comprenderlos. En los próximos artículos, cubriré técnicas y herramientas más avanzadas que pueden ayudar a que sus compilaciones de Docker sean más rápidas y eficientes.