Otimizando suas imagens Docker com Multi Stage Build

Pery Lemke
Test After Deploy
Published in
5 min readJan 3, 2018
Mascote do Docker

Tenho a honra de fazer o primeiro post do blog do Test After Deploy e para tal, irei contar o primeiro desafio que tive na Nuveo: Otimizar as nossas imagens Docker.

Mas antes de começarmos, o que é Docker?

De tantas referências que eu li a melhor descrição sobre Docker foi dada pelo meu amigo e grande mentor Gomex no seu livro Docker para Desenvolvedores:

O Docker é uma plataforma aberta, criada com o objetivo de facilitar o desenvolvimento, a implantação e a execução de aplicações em ambientes isolados.

Desafio

Quando cheguei ao time da Nuveo, me deparei com as nossas imagens “core” com o seguinte tamanho:

Como ilustrei no início: Imagens gigantes…

Imagens muito pesadas, que estavam trazendo problemas na hora da realização do build e também para os membros do time que tem pouco espaço em disco.

O processo de otimização das imagens foi iniciado pelos senhores Cássio Botaro e Cesar Gimenes, porém devido a outras prioridades e falta de tempo da equipe não foi possível terminá-lo.

Scratch

Como missão dada, é missão cumprida, corri atrás de terminar este processo. A primeira sugestão que recebi, seria a utilização da imagem Scratch.

Para quem não sabe, Scratch é uma imagem especial do Docker que vem completamente vazia.

Realizando testes com Scratch

Para iniciarmos esta tarefa iremos utilizar uma funcionalidade empregada na versão 17.05 do Docker, o Multi Stage Build que permite que o mesmo build seja utilizado em diversas etapas da criação da imagem Docker, permitindo Dockerfiles mais limpos e de fácil manutenção.

Processo de Multi Stage Build

Neste artigo iremos usar uma aplicação de exemplo da Nuveo que é o Auth, então abaixo segue o Dockerfile sem o processo de Multi Stage Build:

Dockerfile sem Multi Stage Build

Mesmo com poucas linhas e bem fácil de manter esse Dockerfile baseado numa imagem customizada pela Nuveo (inspirada na imagem oficial do Go), buildava uma imagem com 673MB, ou seja, bem pesada.

Realizado o processo de Multi Stage Build com uma imagem Scratch, atualizando nosso Dockerfile:

Dockerfile do Auth utilizando Multi Stage Build com Scratch

Dissecando o Dockerfile acima:

  • A primeira etapa do processo está compilando nossa aplicação para um executável binário e percebam que foi inserida na primeira linha o complemento as builder, builder nada mais é que o nome deste primeiro processo, caso você não coloque nada, o Docker entende que ele tem o nome de 0.
  • Na segunda etapa, utilizando uma imagem Scratch, estamos definindo o diretório da nossa aplicação, através da instrução WORKDIR e após isto vamos copiar nosso executável para o segundo processo.

Isto é feito graças ao comando COPY --from=builder, que pega da primeira etapa e insere na imagem a ser montada na segunda etapa.

Feito o processo de build, a imagem foi criada com sucesso e ela ficou muito mais leve que a imagem anterior, como mostra o print abaixo comparando:

Um ganho gigantesco…

Porém como nem tudo na vida são flores, temos dois problemas utilizando imagens Scratch:

  1. Nosso executável possui algumas dependências. Então, para resolver devemos compilar nossa aplicação com a variável CGO_ENABLED=0, conforme a linha 8 do Dockerfile apresentado.
  2. Principal problema: o Docker não consegue ajustar as variáveis de ambiente dentro do contêiner, caso o mesmo seja do tipo Scratch e como todas as nossas configurações estão em variáveis de ambiente, se torna inviável utilizar imagens Scratch para resolver este problema.

Por isso vamos para a nossa segunda opção, usar a imagem Alpine.

Alpine

Alpine é uma distribuição Linux baseada em musl libc e BusyBox que combina versões minúsculas de vários utilitários comuns no UNIX em um pequeno executável, tendo como características principais: leveza, simplicidade e segurança.

Realizando testes com alpine

Com as lições aprendidas no processo com as imagens baseadas em Scratch, realizei os testes com o Alpine.

Nosso Dockerfile teve algumas modificações:

Dockerfile utilizando uma imagem do Alpine 3.6

Ao invés de uma imagem Scratch, estamos utilizando uma imagem Alpine na versão 3.6, conforme estamos vendo na linha 10 e também realizando a instalação do pacote ca-certificates, conforme a linha 11 do Dockerfile acima.

Definimos o diretório da aplicação, copiamos nosso executável do builder e executamos nossa aplicação.

Continua leve…

Percebemos que a imagem está um pouco mais pesada que a imagem baseada em Scratch, porém ainda muito leve em comparação a imagem sem o processo de Multi Stage Build e por sua vez sem os problemas mencionados quando realizamos o build anterior.

Resultado

Após os testes realizados, aplicamos este mesmo processo nas nossas outras imagens e com isso tivemos um ganho incrível em tamanho, conforme imagem abaixo:

Resultado final: Imagens mais leves e performáticas

Os ganhos foram incríveis, o nosso build ficou muito mais rápido e o nosso processo de integração e entrega contínuas ganhou mais performance.

Dicas

Duas dicas finais, quando você realiza o build ou baixa as imagens do Docker Hub, o Docker cria imagens intermediárias, as famosas imagens com a tag <none>, por isso após este processo é interessante que você execute o comando docker image prune -f para limpar estas imagens.

E uma dica final, se você desenvolve aplicações Go e utiliza Docker, recomendo fortemente estudar e aplicar o Multi Stage Build para otimizar as imagens de suas aplicações.

Futuro

Após conseguirmos realizar a otimização das imagens das nossas aplicações em Go, não paramos um minuto de ver formas para diminuir ainda mais o tamanho das mesmas. Além disso, estamos fazendo os testes para realizar a otimização usando Multi Stage Build também nas dezenas de imagens Python da Nuveo.

Então em breve teremos um novo artigo sobre este case aqui no Medium do TAD. \o/

Links úteis

--

--

Pery Lemke
Test After Deploy

SysAdmin/SRE at Nuveo, Pythonist, Gopher Newbie, Sudocaster, Test After Deploy member, DevOps Evangelist and nice guy…