Reduzindo o tamanho de suas imagens Docker (exemplo c/ Ruby)

Carlos Ribeiro
Opensanca
Published in
6 min readFeb 15, 2019

--

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/app
ENV PATH="$PATH:/opt/yarn/bin" BUNDLE_PATH="/gems" BUNDLE_JOBS=2 RAILS_ENV=${rails_env} BUNDLE_WITHOUT=${bundle_without}COPY . /var/app
WORKDIR /var/app
RUN 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-builder
ARG 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/app
ENV PATH="$PATH:/opt/yarn/bin" BUNDLE_PATH="/gems" BUNDLE_JOBS=2 RAILS_ENV=${rails_env} BUNDLE_WITHOUT=${bundle_without}COPY . /var/app
WORKDIR /var/app
RUN 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-client
COPY --from=pre-builder /gems/ /gems/
COPY --from=pre-builder /var/app /var/app
ENV 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-builder
ARG 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/app
ENV PATH="$PATH:/opt/yarn/bin" BUNDLE_PATH="/gems" BUNDLE_JOBS=4 RAILS_ENV=${rails_env} BUNDLE_WITHOUT=${bundle_without}COPY . /var/app
WORKDIR /var/app
RUN 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-client
COPY --from=pre-builder /gems/ /gems/
COPY --from=pre-builder /var/app /var/app
ENV 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 🍻

--

--