# Otimizando o `docker build` Esse tutorial tem como objetivo ajudar a evitar um erro comum na criação de imagens de Docker, em especial em stack onde contamos com arquivos de especificação de dependências e onde for possível fazer uso imagem de cache, anteriormente criadas, com as quais podemos otimizar o processo de build de uma nova imagem. Essa dica pode ser utilizada em diferentes tipos de imagem, mas ela é especialmente importante em stack onde temos arquivos de configuração de dependência (manifestos de dependências), como: - Elixir: `mix.exs` e `mix.lock` - Ruby: `Gemfile` e `Gemfile.lock` - Python: `requirements.txt` - Node: `package.json`, `package-lock.json` e `yarn.lock` - Rust: `Cargo.toml` e `Cargo.lock` Em stacks assim devemos tomar bastante cuidado na ordem com que escrevemos as instruções do Dockerfile, ou vamos fazer com que o Docker sempre execute todos os passos e vamos perdendo bastante tempo de build. ## `docker build` e o cache Já observou que é comum que ao executar um `docker build`, ele nem sempre executa todos os steps novamente? Vamos ver um exemplo especialmente preparado para esse parte do tutor (depois de explicar o conceito voltaremos em um exemplo mais real). Nele estamos fazendo o build de imagem docker de quatro steps, três vezes, veja: [![asciicast](https://asciinema.org/a/346518.svg)](https://asciinema.org/a/346518) Para facilitar o entendimento vou transcrever o `Dockerfile` aqui: ```Dockerfile # step 1 FROM alpine:3.10 # step 2 ENV FILE "Dockerfile" # step 3 COPY $FILE ./ # step 4 RUN cat $FILE ``` Análisando rápidamente os resultados das três chamadas de `docker build -t docker_example .`, temos: - **primeira vez:** Cada step é executado completamente, inclusive o step onde o conteúdo do Dockerfile é exibido na tela com um comando `cat`; - **segunda vez:** Nenhum dos steps é de fato executado, e a mensagem `Using cache` aparece três vezes na saída mas o conteúdo do nosso Dockerfile não é exibido; - **terceira vez:** O segundo step não é executado (a mensagem de `Using cache` só aparece uma vez), mas os dois últimos são e novamente o conteúdo (agora modificado com o `echo "# end of file" >> Dockerfile`) do `Dockerfile` é exibido; Isso acontece porque o docker utiliza um sistema de validação com hashs para cada layer da imagem que esta sendo gerada (um step == uma layer), e assim ele sabe que aquele layer não precisa ser gerado novamente. Quando o arquivo é modificado o step 3 passa ter uma nova assinatura e dali em diante o docker não consegue mais achar uma imagem com a mesma assinatura dos steps e novas layers são geradas. ## Entendedo as assinaturas (Ids) Vamos analisar cada step e entender como funciona validação/assinatura por hash: ```Dockerfile # step 1 FROM alpine:3.10 ``` Nesse caso não há nenhum step anterior, logo o `Id` da imagem é o identificador do primeiro layer da nossa nova imagem, neste caso: `sha256:be4e4bea2c2e`. Observe ainda que o download da imagem só acontece da primeira vez, dai em diante o docker não precisa mais baixar a imagem. ```Dockerfile # step 2 ENV FILE "Dockerfile" ``` Esse é um step de configuração, o que quer dizer que ele não gera um layer em si mas apenas salva meta informações na imagem. De qualquer forma um `Id` é calculado para ele, os seguintes dados são considerados para o calculo de hash: - `Id` da layer anterior, nesse caso o `Id` da última layer da imagem `alpine:3.10` - As configurações de `ENV` já existente na imagem, nesse caso: `"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"` - Os argumentos do próprio comando, aqui: `FILE "Dockerfile"` ```Dockerfile # step 3 COPY $FILE ./ ``` Neste step estamos instruindo para que o docker faça uma copia do arquivo `Dockerfile` para o diretório corrente da imagem (governado pela instrução `WORKDIR`). O `Id` deste layer é gerado usando um calculo de hash que envolve: - `Id` da layer anteior, nesse caso o `Id` da layer `ENV FILE "Dockerfile"` - As configurações de `ENV` - Os argumentos do próprio comando `$FILE ./` - E por fim todo o conteúdo que esta sendo copiado nesse layer Uma vez com o `Id` do step calculado o docker verifica se já existe um layer com a quele mesmo `Id` dentro daquela instância de docker. Na primeira vez isso não é verdade e um layer com o conteúdo do arquivo é criado. Já na segunda vez o arquivo não tem qualquer diferença, e os steps anteriores são os mesmos, então uma nova layer não precisa ser criada. Por fim na última execução o arquivo passa a ter uma diferença, logo seu conteúdo vai interferir no calculo do hash e um novo `Id` sera gerado, como o docker não tem uma layer para esse `Id` ele é obrigado a gerar uma nova layer. ```Dockerfile # step 4 RUN cat $FILE ``` Por fim temos um comando que exibi o conteúdo do arquivo. O `Id` deste layer é gerado usando um calculo de hash que envolve: - `Id` da layer anterior, nesse caso o `Id` da layer `COPY $FILE ./` - As configurações de `ENV` - Os argumentos do próprio comando `cat $FILE` Na primeira execução não temos uma layer com esse `Id`, assim como acontece no step anterior, logo o comando é executado e o resultado da sua execução é exibido na tela. Já na segunda execução o docker utiliza o Layer já gerado, já que não temos nenhuma mudança que altere o `Id` da layer anterior, assim o comando `cat` não é executado e não temos nenhuma saída. Por fim na última execução voltamos a ter uma saída com o novo conteúdo do Dockerfile, já que agora seu conteúdo foi modificado e esse step recebeu um novo `Id` e precisa ser executado novamente. ## De volta a otimização Uma vez que tenhamos entendido o processo de calculo dos `Ids` e cash de layers, podemos otimizar nossos Dockerfiles: ```Dockerfile # Dockerfile for elixir env # step 1 FROM elixir # step 2 WORKDIR /app # step 3 COPY . /app # step 4 RUN mix deps.get ``` O problema desse Dockerfile é que no terceiro step estamos copiando nossa aplicação inteira para dentro da pasta `/app`. Isso inclui o `mix.exs` e `mix.lock`, mas também inclui todos os outros arquivos da aplicação. Nossas rotinas de desenvolvimento em geral inclui modificar os arquivos de lógica muito mais vezes do que modificamos os manifestos de dependência. Dado que esses arquivos modificam o `Id` do step, se o processo for feito em três partes, no lugar de duas, é possível aproveitar melhor os caches de layer: ```Dockerfile # Dockerfile for elixir env # step 1 FROM elixir # step 2 WORKDIR /app # step 3 COPY mix.exs mix.lock /app # step 4 RUN mix deps.get # step 5 COPY . /app ``` Dessa forma apenas mudanças nos arquivos `mix.exs` e `mix.lock` vão fazer com que os steps 3 e 4 gerem layers diferentes daqueles anteriormente executados. E apenas o step 5 seria executado com mais frequência. **Importante:** É sempre importante utilizar o `.dockerignore` em seus projetos, muitos arquivos usados em tempo de desenvolvimento não são requeridos na execução da aplicação, logo não precisam ser adicionados e evitam um nova layer no step 5.