paint-brush
Otimizar imagens do Docker é mais do que algo feito uma vez e prontopor@aleksandrov
Novo histórico

Otimizar imagens do Docker é mais do que algo feito uma vez e pronto

por Igor Alexandrov17m2025/01/30
Read on Terminal Reader

Muito longo; Para ler

Este artigo é parte de uma série de posts onde eu vou percorrer cada linha do Dockerfile padrão e explicar as melhores práticas e otimizações. O primeiro artigo vai tocar apenas na otimização de redução de tamanho de imagem.
featured image - Otimizar imagens do Docker é mais do que algo feito uma vez e pronto
Igor Alexandrov HackerNoon profile picture
0-item


Este artigo faz parte de uma série de postagens onde abordarei cada linha do Dockerfile padrão do Rails e explicarei as melhores práticas e otimizações.


As imagens do Docker podem ser otimizadas de diferentes maneiras que incluem, mas não estão limitadas a, redução do tamanho da imagem, otimização do desempenho da compilação, práticas recomendadas de segurança e manutenibilidade e otimizações específicas do aplicativo. No primeiro artigo, abordarei apenas a otimização da redução do tamanho da imagem e explicarei por que elas são importantes.

Por que otimizar o tamanho da imagem?

Como em qualquer outro processo de desenvolvimento de software, cada desenvolvedor listará seus motivos pelos quais ele quer tornar suas compilações Docker mais rápidas. Listarei os motivos que são mais importantes para mim.

Construções e implantações mais rápidas

Imagens menores são mais rápidas de construir porque menos arquivos e camadas precisam ser processados. Isso melhora a produtividade do desenvolvedor, especialmente durante ciclos de desenvolvimento iterativos. Imagens menores levam menos tempo para enviar para um registro e extrair dele durante as implantações. Isso é especialmente crítico em pipelines de CI/CD, onde os contêineres são construídos e implantados com frequência.

Custos de armazenamento reduzidos e uso de largura de banda de rede

Imagens menores consomem menos armazenamento em registros de contêineres, máquinas de desenvolvimento local e servidores de produção. Isso reduz os custos de infraestrutura, especialmente para implantações em larga escala. Imagens menores usam menos largura de banda quando transferidas entre servidores, especialmente importante quando você está criando imagens localmente ou em pipelines de CI/CD e enviando-as para um registro.


“Gastamos US$ 3,2 milhões na nuvem em 2022... Podemos economizar cerca de US$ 7 milhões em despesas com servidores ao longo de cinco anos com nossa saída da nuvem.” David Heinemeier Hansson — HEY World

Desempenho e segurança aprimorados

Imagens menores exigem menos recursos (por exemplo, CPU, RAM) para carregar e executar, melhorando o desempenho geral de aplicativos em contêineres. Tempos de inicialização mais rápidos significam que seus serviços estão prontos mais rapidamente, o que é crucial para sistemas de escalonamento e alta disponibilidade. Imagens de base mínimas como alpine ou debian-slim contêm menos pacotes pré-instalados, diminuindo o risco de software sem patch ou desnecessário ser explorado.


Além de tudo o que foi mencionado acima, remover arquivos e ferramentas desnecessários minimiza distrações ao diagnosticar problemas e leva a uma melhor manutenibilidade e redução da dívida técnica.

Inspecionando imagens do Docker

Para obter diferentes parâmetros da imagem, incluindo o tamanho, você pode consultar o Docker Desktop ou executar o comando docker images no 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


Saber o tamanho da imagem não lhe dá a imagem completa. Você não sabe o que está dentro da imagem, quantas camadas ela tem ou quão grande é cada camada. Uma camada de imagem Docker é uma camada de sistema de arquivos imutável e somente leitura que é um componente de uma imagem Docker. Cada camada representa um conjunto de alterações feitas no sistema de arquivos da imagem, como adicionar arquivos, modificar configurações ou instalar software.


As imagens do Docker são construídas incrementalmente, camada por camada, e cada camada corresponde a uma instrução no Dockerfile . Para obter as camadas da imagem, você pode executar o 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


Como já forneci teoria sobre imagens e camadas, é hora de explorar o Dockerfile . A partir do Rails 7.1, o Dockerfile é gerado com o novo aplicativo Rails. Abaixo está um exemplo de como ele pode se parecer.


 # 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"]


Abaixo, fornecerei uma lista de abordagens e regras que foram aplicadas ao Dockerfile acima para tornar o tamanho da imagem final eficiente.

Otimizar instalações de pacotes

Tenho certeza de que você mantém apenas o software necessário em sua máquina de desenvolvimento local. O mesmo deve ser aplicado às imagens do Docker. Nos exemplos abaixo, vou consistentemente piorar o Dockerfile extraído do Rails Dockerfile acima. Vou referenciá-lo como uma versão original Dockerfile .

Regra nº 1: Use o mínimo de imagens de base

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


A imagem base é o ponto de partida para o Dockerfile . É a imagem que é usada para criar o contêiner. A imagem base é a primeira camada no Dockerfile e é a única camada que não é criada pelo próprio Dockerfile .


A imagem base é especificada com o comando FROM , seguido pelo nome da imagem e tag. A tag é opcional e, se não for especificada, a tag latest será usada. A imagem base pode ser qualquer imagem disponível no Docker Hub ou qualquer outro registro.


No Dockerfile about, estamos usando a imagem ruby com a tag 3.4.1-slim . A imagem ruby é a imagem oficial do Ruby disponível no Docker Hub. A tag 3.4.1-slim é uma versão slim da imagem Ruby que é baseada na imagem debian-slim . Enquanto a imagem debian-slim é uma versão mínima da imagem do Debian Linux que é otimizada para tamanho. Veja a tabela abaixo para ter uma ideia de quão menor é a imagem 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


Em janeiro de 2024, a versão atual do Debian é chamada de bookworm e a anterior é bullseye .


219 MB em vez de 1 GB — uma diferença enorme. Mas e se a imagem alpine for ainda menor? A imagem alpine é baseada na distribuição Alpine Linux, que é uma distribuição Linux superleve que é otimizada para tamanho e segurança. Alpine usa a biblioteca musl (em vez de glibc ) e busybox (um conjunto compacto de utilitários Unix) em vez de equivalentes GNU. Embora seja tecnicamente possível usar a imagem alpine para executar Rails, não vou abordá-la neste artigo.

Regra nº 2: Minimize as camadas

 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 instrução RUN , COPY e FROM no Dockerfile cria uma nova camada. Quanto mais camadas você tiver, maior será o tamanho da imagem. É por isso que a melhor prática é combinar vários comandos em uma única instrução RUN . Para ilustrar esse ponto, vamos dar uma olhada no exemplo abaixo.


 # 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!"]


Eu dividi a instrução RUN em várias linhas, o que obviamente as torna mais legíveis para humanos . Mas como isso afetará o tamanho da imagem? Vamos construir a imagem e dar uma olhada.


 ➜ 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


Foram necessários 28 segundos para construir a imagem, enquanto construir a versão original com camadas minimizadas leva apenas 19 segundos ( quase 33% mais rápido ).


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


Vamos verificar o tamanho das imagens.


 ➜ 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


A imagem com camadas minimizadas é 23 MB menor do que aquela sem camadas minimizadas. Isso é uma redução de 6% no tamanho . Embora pareça uma pequena diferença neste exemplo, a diferença será muito maior se você dividir todas as instruções RUN em várias linhas.

Regra nº 3: Instale apenas o necessário

Por padrão, apt-get install instala os pacotes recomendados, assim como os pacotes que você pediu para ele instalar. A opção --no-install-recommends diz apt-get para instalar somente os pacotes que são explicitamente especificados e não os 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 você pode ver, a imagem sem --no-install-recommends é 70 MB maior que a original . Isso é um aumento de 16% no tamanho .


Use o utilitário dive para ver quais arquivos foram adicionados à imagem – leia mais sobre isso no final do artigo.

Regra nº 4: Limpe após as instalações

O Dockerfile original inclui o comando rm -rf /var/lib/apt/lists/* /var/cache/apt/archives após o comando apt-get install . Este comando remove as listas de pacotes e arquivos que não são mais necessários após a instalação. Vamos ver como isso afeta o tamanho da imagem, para conseguir isso, criarei um novo Dockerfile sem o comando cleaning .


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


A criação das imagens leva quase o mesmo tempo que a original, o que faz 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 verificar o tamanho das imagens.


 ➜ 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


A imagem sem limpeza é 19 MB maior que a com limpeza, o que representa um aumento de 5% no tamanho .

O pior cenário

E se todas as quatro otimizações mencionadas acima não forem aplicadas? Vamos criar um novo Dockerfile sem nenhuma otimização e construir a imagem.


 # 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


Uau, demorou mais de um minuto para construir a imagem.


 ➜ 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


A imagem sem otimizações é 714 MB maior que a original, o que é um aumento de 200% no tamanho . Isso mostra claramente o quão importante é otimizar o Dockerfile , imagens maiores levam mais tempo para serem construídas e consomem mais espaço em disco.

Sempre use .dockerignore

O arquivo .dockerignore é semelhante ao arquivo .gitignore usado pelo Git. Ele é usado para excluir arquivos e diretórios do contexto da compilação. O contexto é o conjunto de arquivos e diretórios que são enviados ao daemon do Docker ao compilar uma imagem. O contexto é enviado ao daemon do Docker como um tarball, então é importante mantê-lo o menor possível.


Se, por algum motivo, você não tiver o arquivo .dockerignore no seu projeto, você pode criá-lo manualmente. Eu sugiro que você use o modelo de arquivo oficial do Rails .dockerignore como ponto de partida. Abaixo está um exemplo de como ele pode parecer.


 # 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*


Ter um arquivo .dockerfile no projeto não só permite excluir arquivos e diretórios desnecessários (por exemplo, fluxos de trabalho do GitHub da pasta .github ou dependências JavaScript do node_modules ) do contexto. Também ajuda a evitar adicionar acidentalmente informações confidenciais à imagem. Por exemplo, o arquivo .env que contém as variáveis de ambiente ou o arquivo master.key que é usado para descriptografar as credenciais.

Usar mergulho

Todas as otimizações mencionadas acima podem parecer óbvias quando explicadas. O que fazer se você já tem uma imagem enorme e não sabe por onde começar?


Minha ferramenta favorita e mais útil é o Dive . O Dive é uma ferramenta TUI para explorar uma imagem Docker, conteúdo de camadas e descobrir maneiras de reduzir o tamanho da imagem. O Dive pode ser instalado com o gerenciador de pacotes do seu sistema, ou você pode usar sua imagem oficial do Docker para executá-lo. Vamos usar a imagem do nosso pior cenário.


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


Ferramenta de inspeção de camadas do Dive Docker

Na captura de tela acima, você pode ver a inspeção da nossa imagem menos otimizada. O Dive mostra o tamanho de cada camada, o tamanho total da imagem e os arquivos que foram alterados (adicionados, modificados ou excluídos) em cada camada. Para mim, esse é o recurso mais útil do Dive. Ao listar os arquivos no painel direito, você pode identificar facilmente os arquivos que não são necessários e remover comandos que os adicionam à imagem.


Uma coisa que eu realmente amo sobre o Dive é que, além de ter uma UI de terminal, ele também pode fornecer uma saída amigável ao CI, o que pode ser eficaz em um desenvolvimento local também. Para usá-lo, execute o Dive com a variável de ambiente CI definida como true , a saída do comando está na captura de tela abaixo.


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


Saída compatível com CI

Minha preferência pessoal é usar o Dive em uma base programada, por exemplo, uma vez por semana, para garantir que suas imagens ainda estejam em boas condições. Nos próximos artigos, abordarei fluxos de trabalho automatizados que uso para verificar meu Dockerfile, incluindo Dive e Hadolint .

Não esmague as camadas

Uma abordagem para minimizar o tamanho da imagem que eu vi é tentar comprimir as camadas. A ideia era combinar várias camadas em uma única camada para reduzir o tamanho da imagem. O Docker tinha uma opção experimental --squash , além disso, havia ferramentas de terceiros como docker-squash .


Embora essa abordagem tenha funcionado no passado, atualmente ela está obsoleta e não é recomendada para uso. O esmagamento de camadas destruiu o recurso fundamental do Docker de cache de camadas. Além disso, ao usar --squash você pode incluir involuntariamente arquivos sensíveis ou temporários de camadas anteriores na imagem final. Essa é uma abordagem do tipo tudo ou nada que carece de controle refinado.


Em vez de esmagar camadas, é recomendado usar builds multi-stage. O Rails Dockerfile já usa builds multi-stage, explicarei como funciona no próximo artigo.

Conclusões

Otimizar imagens do Docker, assim como qualquer outra otimização, não pode ser feito uma vez e esquecido . É um processo contínuo que requer verificações e melhorias regulares. Tentei cobrir o básico, mas é essencial conhecê-lo e entendê-lo. Nos próximos artigos, abordarei técnicas e ferramentas mais avançadas que podem ajudar a tornar suas compilações do Docker mais rápidas e eficientes.