RUN múltiplo x RUN encadeado único no Dockerfile, o que é melhor?

132

Dockerfile.1executa vários RUN:

FROM busybox
RUN echo This is the A > a
RUN echo This is the B > b
RUN echo This is the C > c

Dockerfile.2 junta-se a eles:

FROM busybox
RUN echo This is the A > a &&\
    echo This is the B > b &&\
    echo This is the C > c

Cada RUNum cria uma camada, então eu sempre assumi que menos camadas são melhores e, portanto, Dockerfile.2são melhores.

Isso é obviamente verdade quando um RUNremove algo adicionado por um anterior RUN(ie yum install nano && yum clean all), mas nos casos em que todos RUNadiciona algo, há alguns pontos que precisamos considerar:

  1. As camadas devem adicionar apenas um diff acima do anterior, portanto, se a camada posterior não remover algo adicionado na anterior, não haverá muito espaço em disco economizando vantagem entre os dois métodos ...

  2. As camadas são puxadas em paralelo no Docker Hub, portanto Dockerfile.1, embora provavelmente sejam um pouco maiores, teoricamente seriam baixadas mais rapidamente.

  3. Se adicionar uma quarta frase (ie echo This is the D > d) e reconstruir localmente, Dockerfile.1criaria mais rápido graças ao cache, mas Dockerfile.2teria que executar todos os 4 comandos novamente.

Então, a pergunta: qual é a melhor maneira de criar um Dockerfile?

Yajo
fonte
1
Não pode ser respondida, em geral, uma vez que depende da situação e sobre o uso da imagem (Otimizar para o tamanho, velocidade de download, ou a construção de velocidade)
Henry

Respostas:

99

Sempre que possível, sempre mesclo comandos que criam arquivos com comandos que excluem esses mesmos arquivos em uma única RUNlinha. Isso ocorre porque cada RUNlinha adiciona uma camada à imagem, a saída é literalmente as alterações no sistema de arquivos que você pode visualizar docker diffno contêiner temporário criado. Se você excluir um arquivo criado em uma camada diferente, tudo o que o sistema de arquivos da união fará é registrar a alteração do sistema de arquivos em uma nova camada, o arquivo ainda existe na camada anterior e é enviado pela rede e armazenado no disco. Portanto, se você baixar o código-fonte, extraí-lo, compilá-lo em um binário e excluir os arquivos tgz e de origem no final, você realmente deseja que tudo seja feito em uma única camada para reduzir o tamanho da imagem.

Em seguida, eu pessoalmente divido as camadas com base em seu potencial de reutilização em outras imagens e no uso esperado de armazenamento em cache. Se eu tiver 4 imagens, todas com a mesma imagem base (por exemplo, debian), posso puxar uma coleção de utilitários comuns para a maioria dessas imagens no primeiro comando de execução, para que as outras imagens se beneficiem do cache.

A ordem no Dockerfile é importante ao analisar a reutilização do cache de imagem. Examino todos os componentes que serão atualizados muito raramente, possivelmente apenas quando a imagem base é atualizada e os coloca no alto do Dockerfile. No final do Dockerfile, incluo todos os comandos que serão executados rapidamente e podem mudar com frequência, por exemplo, adicionando um usuário com um UID específico do host ou criando pastas e alterando permissões. Se o contêiner incluir código interpretado (por exemplo, JavaScript) que está sendo desenvolvido ativamente, isso será adicionado o mais tarde possível, para que uma reconstrução execute apenas essa única alteração.

Em cada um desses grupos de alterações, consolido o melhor possível para minimizar as camadas. Portanto, se houver quatro pastas de código-fonte diferentes, elas serão colocadas em uma única pasta para que possam ser adicionadas com um único comando. Qualquer instalação de pacote de algo como o apt-get é mesclada em um único RUN, quando possível, para minimizar a quantidade de sobrecarga do gerenciador de pacotes (atualização e limpeza).


Atualização para compilações de vários estágios:

Preocupo-me muito menos em reduzir o tamanho da imagem nos estágios não finais de uma compilação de vários estágios. Quando esses estágios não são marcados e enviados para outros nós, você pode maximizar a probabilidade de uma reutilização de cache dividindo cada comando em uma RUNlinha separada .

No entanto, essa não é uma solução perfeita para esmagar camadas, pois tudo o que você copia entre os estágios são os arquivos e não o restante dos metadados da imagem, como configurações de variáveis ​​de ambiente, ponto de entrada e comando. E quando você instala pacotes em uma distribuição linux, as bibliotecas e outras dependências podem estar espalhadas pelo sistema de arquivos, dificultando a cópia de todas as dependências.

Por esse motivo, utilizo compilações de vários estágios como substituto para a construção de binários em um servidor de CI / CD, para que meu servidor de CI / CD precise apenas ter as ferramentas para executar docker builde não ter um jdk, nodejs, go e quaisquer outras ferramentas de compilação instaladas.

BMitch
fonte
30

Resposta oficial listada em suas melhores práticas (as imagens oficiais DEVEM aderir a elas)

Minimize o número de camadas

Você precisa encontrar o equilíbrio entre legibilidade (e, portanto, manutenção de longo prazo) do Dockerfile e minimizar o número de camadas que ele usa. Seja estratégico e cauteloso quanto ao número de camadas que você usa.

Desde a janela de encaixe 1.10 COPY, as instruções ADDe RUNadicionam uma nova camada à sua imagem. Seja cauteloso ao usar essas instruções. Tente combinar comandos em uma única RUNinstrução. Separe isso apenas se for necessário para facilitar a leitura.

Mais informações: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/minimize-the-number-of-layers

Atualização: Estágio múltiplo na janela de encaixe> 17,05

Com compilações de vários estágios, você pode usar várias FROMinstruções no seu Dockerfile. Cada FROMdeclaração é um estágio e pode ter sua própria imagem de base. No estágio final, você usa uma imagem base mínima como alpine, copia os artefatos de construção dos estágios anteriores e instala os requisitos de tempo de execução. O resultado final desta fase é a sua imagem. Portanto, é aqui que você se preocupa com as camadas, conforme descrito anteriormente.

Como de costume, o docker possui ótimos documentos sobre compilações de vários estágios. Aqui está um trecho rápido:

Com compilações de vários estágios, você usa várias instruções FROM no Dockerfile. Cada instrução FROM pode usar uma base diferente e cada uma delas inicia um novo estágio da compilação. Você pode copiar artefatos seletivamente de um estágio para outro, deixando para trás tudo o que não deseja na imagem final.

Um ótimo post sobre isso pode ser encontrado aqui: https://blog.alexellis.io/mutli-stage-docker-builds/

Para responder seus pontos:

  1. Sim, as camadas são como diferenças. Eu não acho que há camadas adicionadas se houver absolutamente zero alterações. O problema é que, depois de instalar / baixar algo na camada 2, você não pode removê-lo na camada 3. Assim, quando algo é escrito em uma camada, o tamanho da imagem não pode mais ser diminuído removendo-o.

  2. Embora as camadas possam ser puxadas em paralelo, tornando-a potencialmente mais rápida, sem dúvida, cada camada aumenta o tamanho da imagem, mesmo que esteja removendo arquivos.

  3. Sim, o cache é útil se você estiver atualizando seu arquivo docker. Mas funciona em uma direção. Se você tiver 10 camadas e alterar a camada 6, ainda precisará reconstruir tudo da camada 6 a 10. Portanto, não é muito frequente que acelere o processo de criação, mas é garantido que aumenta desnecessariamente o tamanho da sua imagem.


Obrigado a @Mohan por me lembrar de atualizar esta resposta.

Menzo Wijmenga
fonte
1
Agora está desatualizado - veja a resposta abaixo.
Mohan
1
@ Mohan obrigado pelo lembrete! Atualizei a postagem para ajudar os usuários.
Menzo Wijmenga
19

Parece que as respostas acima estão desatualizadas. Os documentos observam:

Antes do Docker 17.05, e ainda mais, antes do Docker 1.10, era importante minimizar o número de camadas na sua imagem. As seguintes melhorias atenuaram essa necessidade:

[...]

O Docker 17.05 e superior adicionam suporte para compilações de vários estágios, o que permite copiar apenas os artefatos necessários na imagem final. Isso permite incluir ferramentas e informações de depuração nos estágios intermediários de construção sem aumentar o tamanho da imagem final.

https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#minimize-the-number-of-layers

e

Observe que este exemplo também compacta artificialmente dois comandos RUN usando o operador Bash &&, para evitar a criação de uma camada adicional na imagem. Isso é propenso a falhas e difícil de manter.

https://docs.docker.com/engine/userguide/eng-image/multistage-build/

A melhor prática parece ter mudado para o uso de compilações de vários estágios e para manter a Dockerfilelegibilidade.

Mohan
fonte
Embora as compilações de vários estágios pareçam uma boa opção para manter o equilíbrio, a correção real para essa pergunta ocorrerá quando a docker image build --squashopção for fora do experimental.
Yajo
2
@Yajo - Eu sou cético em relação ao teste squashexperimental. Ele tem muitos truques e só fazia sentido antes da construção em vários estágios. Com compilações de vários estágios, você só precisa otimizar o estágio final, o que é muito fácil.
Menzo Wijmenga
1
@Yajo Para expandir isso, apenas as camadas no último estágio fazem alguma diferença no tamanho da imagem final. Portanto, se você colocar todos os seus gubbins do construtor em estágios anteriores e tiver o estágio final apenas instalando pacotes e copiando arquivos dos estágios anteriores, tudo funcionará perfeitamente e o squash não será necessário.
Mohan
3

Depende do que você incluir nas camadas da imagem.

O ponto principal é compartilhar o maior número possível de camadas:

Mau exemplo:

Dockerfile.1

RUN yum install big-package && yum install package1

Dockerfile.2

RUN yum install big-package && yum install package2

Bom exemplo:

Dockerfile.1

RUN yum install big-package
RUN yum install package1

Dockerfile.2

RUN yum install big-package
RUN yum install package2

Outra sugestão é excluir não é tão útil apenas se ocorrer na mesma camada que a ação de adição / instalação.

dias
fonte
Esses 2 realmente compartilhariam o RUN yum install big-packagecache from?
Yajo 8/09/16
Sim, eles compartilhariam a mesma camada, desde que partissem da mesma base.
Ondra Žižka