Reduzindo o tamanho de suas imagens Docker (exemplo c/ Ruby)
Um grande problema de fazer deploy em produção utilizando docker é tamanho que as imagens geradas podem ocupar. Imagens muito grandes demoram para ser baixadas, consomem parte da sua cota de tráfego de rede no seu provedor de nuvem, custam para ser guardadas no seu repositório e não trazer nenhuma valor adicional.
Na maioria das situações, quando criamos uma imagem docker, colocamos alguns passos e dependências que as vezes não precisamos na imagem final que iremos rodar em produção
Vou usar como exemplo a seguinte aplicação: https://github.com/opensanca/opensanca_jobs
e esse é o Dockerfile que nos gera a imagem.
FROM ruby:2.5.0-alpineLABEL maintainer="contato@opensanca.com.br"ARG rails_env="development"
ARG build_without=""ENV SECRET_KEY_BASE=dumbRUN apk update \
&& apk add \
openssl \
tar \
build-base \
tzdata \
postgresql-dev \
postgresql-client \
nodejs \
&& wget https://yarnpkg.com/latest.tar.gz \
&& mkdir -p /opt/yarn \
&& tar -xf latest.tar.gz -C /opt/yarn --strip 1 \
&& mkdir -p /var/appENV PATH="$PATH:/opt/yarn/bin" BUNDLE_PATH="/gems" BUNDLE_JOBS=2 RAILS_ENV=${rails_env} BUNDLE_WITHOUT=${bundle_without}COPY . /var/app
WORKDIR /var/appRUN bundle install && yarn && bundle exec rake assets:precompile
CMD rails s -b 0.0.0.0
E o comando utilizado para realizar o build:
docker build -t openjobs:latest --build-arg build_without="development test" --build-arg rails_env="production" .
Esse build nos gerou uma imagem de quase 1GB!!! 😱.
Essa imagem tem algumas coisas desnecessárias, como o node e o yarn (só precisamos deles para precompilar os assets e não para a execução da aplicação).
Multi-Stage build
Na versão 17.05 o docker introduziu o conceito de Multi-stage Builds. Essa técnica de build nos permite dividir o nosso Dockerfile em várias declarações FROM
. Cada declaração pode usar imagens base diferentes e você pode copiar artefatos de um estágio para outro, sem trazer as coisas que você não quer na imagem final. Nossa imagem final só contará com o build escrito no ultimo estágio.
Nosso Dockerfile separado em dois estágios: Pré Build e Final Build
# pre-build stage
FROM ruby:2.5.0-alpine AS pre-builderARG rails_env="development"
ARG build_without=""ENV SECRET_KEY_BASE=dumbRUN apk add --update --no-cache \
openssl \
tar \
build-base \
tzdata \
postgresql-dev \
postgresql-client \
nodejs \
&& wget https://yarnpkg.com/latest.tar.gz \
&& mkdir -p /opt/yarn \
&& tar -xf latest.tar.gz -C /opt/yarn --strip 1 \
&& mkdir -p /var/appENV PATH="$PATH:/opt/yarn/bin" BUNDLE_PATH="/gems" BUNDLE_JOBS=2 RAILS_ENV=${rails_env} BUNDLE_WITHOUT=${bundle_without}COPY . /var/app
WORKDIR /var/appRUN bundle install && yarn && bundle exec rake assets:precompile# final build stage
FROM ruby:2.5.0-alpine
LABEL maintainer="contato@opensanca.com.br"RUN apk add --update --no-cache \
openssl \
tzdata \
postgresql-dev \
postgresql-clientCOPY --from=pre-builder /gems/ /gems/
COPY --from=pre-builder /var/app /var/appENV RAILS_LOG_TO_STDOUT trueWORKDIR /var/appEXPOSE 3000CMD rails s -b 0.0.0.0
No estágio de build, instalamos o node e o yarn, instalamos as dependências e precompilamos os assets. No estágio final, utilizamos uma imagem alpine (que é bem leve) com ruby, instalamos apenas dependências necessárias para rodar a aplicação e copiamos as bibliotecas e assets gerados no estágio anterior com o seguinte comando:
COPY --from=pre-builder /gems/ /gems/
COPY --from=pre-builder /var/app /var/app
Fazendo o build desse Dockerfile, agora temos uma imagem de 562mb.
Já diminuímos quase pela metade, mas será que ainda podemos reduzir ainda mais o tamanho da imagem?? 🤔
Sim. Podemos fazer algumas outras ações para reduzir ainda mais essa imagem.
Removendo arquivos desnecessários
Podemos apagar da imagem arquivos que não são necessários, como cache e os arquivos utilizados temporariamente das bibliotecas instaladas. E também podemos adicionar um arquivo .dockerignore
, dizendo para o build o que não enviar para a imagem.
# build stage
FROM ruby:2.5.0-alpine AS pre-builderARG rails_env="development"
ARG build_without=""ENV SECRET_KEY_BASE=dumbRUN apk add --update --no-cache \
openssl \
tar \
build-base \
tzdata \
postgresql-dev \
postgresql-client \
nodejs \
&& wget https://yarnpkg.com/latest.tar.gz \
&& mkdir -p /opt/yarn \
&& tar -xf latest.tar.gz -C /opt/yarn --strip 1 \
&& mkdir -p /var/appENV PATH="$PATH:/opt/yarn/bin" BUNDLE_PATH="/gems" BUNDLE_JOBS=4 RAILS_ENV=${rails_env} BUNDLE_WITHOUT=${bundle_without}COPY . /var/app
WORKDIR /var/appRUN bundle install && yarn && bundle exec rake assets:precompile \
&& rm -rf /gems/cache/*.gem \
&& find /gems/gems/ -name "*.c" -delete \
&& find /gems/gems/ -name "*.o" -delete# final stage
FROM ruby:2.5.0-alpine
LABEL maintainer="contato@opensanca.com.br"RUN apk add --update --no-cache \
openssl \
tzdata \
postgresql-dev \
postgresql-clientCOPY --from=pre-builder /gems/ /gems/
COPY --from=pre-builder /var/app /var/appENV RAILS_LOG_TO_STDOUT trueWORKDIR /var/appEXPOSE 3000CMD rails s -b 0.0.0.0
Nesse novo Dockerfile, adicionamos esse trecho que remove caches e arquivos C temporários utilizados para buildar as bibliotecas.:
&& rm -rf /gems/cache/*.gem \
&& find /gems/gems/ -name "*.c" -delete \
&& find /gems/gems/ -name "*.o" -delete
Além de incluir tambem o nosso .dockerignore
para informar ao processo de build os arquivos que não precisam ser enviados para a imagem:
.env*
.git
.gitignore
.codeclimate.yml
.dockerignore
.gitlab-ci.yml
.hound.yml
.travis.yml
LICENSE.md
README.md
docker-compose.*
Dockerfile
log/*
node_modules/*
public/assets/*
storage/*
public/packs/*
public/packs-test/*
tmp/*
Com esses dois passos, agora nossa imagem tem 272MB.
Podemos dar um passo a mais. Para produção, não precisamos das pastas de teste, dependências do npm (pois elas já foram incluídas no asset pipeline), assets não precompilados, e caches.
Pra remover esses arquivos, podemos incluir uma estratégia de passar um argumento para o build, que iremos chamar de to_remove
.
...ARG to_remove...RUN bundle install && yarn && bundle exec rake assets:precompile \
&& rm -rf /usr/local/bundle/cache/*.gem \
&& find /usr/local/bundle/gems/ -name "*.c" -delete \
&& find /usr/local/bundle/gems/ -name "*.o" -delete \
&& rm -rf $to_remove # Aqui removemos todos arquivos que passamos como argumento no build...
Nesse argumento, passaremos os arquivos que não desejamos para produção:
docker build -t openjobs:reduced --build-arg build_without="development test" --build-arg rails_env="production" . --build-arg to_remove="spec node_modules app/assets vendor/assets lib/assets tmp/cache"
Perceba o --build-arg to_remove="spec node_modules app/assets vendor/assets lib/assets tmp/cache"
Essas são as pastas que iremos remover do nosso processo de build, pois não necessitamos delas para rodar em produção.
Removendo esses arquivos, ficamos com uma imagem de 164MB, quase 6 vezes menor que a original.
Se você ainda não acreditou em mim e quer ver com seus próprios olhos, deixo aqui o pull request pra alteração que gerou essa redução em um projeto que estou trabalhando: https://github.com/opensanca/opensanca_jobs/pull/164
Cheers 🍻