Uma imagem do docker é na verdade uma lista vinculada de camadas do sistema de arquivos. Cada instrução em um Dockerfile cria uma camada do sistema de arquivos que descreve as diferenças no sistema de arquivos antes e após a execução da instrução correspondente. O docker inspect
subcomando pode ser usado em uma imagem de janela de encaixe para revelar sua natureza de ser uma lista vinculada de camadas do sistema de arquivos.
O número de camadas usadas em uma imagem é importante
- ao empurrar ou puxar imagens, pois afeta o número de uploads ou downloads simultâneos que ocorrem.
- ao iniciar um contêiner, como as camadas são combinadas para produzir o sistema de arquivos usado no contêiner; quanto mais camadas estiverem envolvidas, pior será o desempenho, mas os diferentes back-ends do sistema de arquivos são afetados de maneira diferente por isso.
Isso tem várias conseqüências sobre como as imagens devem ser construídas. O primeiro e mais importante conselho que posso dar é:
Conselho # 1 Verifique se as etapas de compilação em que seu código-fonte está envolvido chegam o mais tarde possível no Dockerfile e não estão vinculadas aos comandos anteriores usando a &&
ou a ;
.
A razão para isso é que todas as etapas anteriores serão armazenadas em cache e as camadas correspondentes não precisarão ser baixadas repetidamente. Isso significa compilações e lançamentos mais rápidos, provavelmente o que você deseja. Curiosamente, é surpreendentemente difícil fazer um uso otimizado do cache do docker.
Meu segundo conselho é menos importante, mas acho muito útil do ponto de vista da manutenção:
Conselho nº 2 Não escreva comandos complexos no Dockerfile, mas use scripts que devem ser copiados e executados.
Um Dockerfile seguindo este conselho pareceria
COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh
COPY install_pacakges.sh /root/
RUN sh -x /root/install_packages.sh
e assim por diante. O conselho de vincular vários comandos com &&
apenas um escopo limitado. É muito mais fácil escrever com scripts, onde você pode usar funções etc. para evitar redundância ou para fins de documentação.
As pessoas interessadas pelos pré-processadores e dispostas a evitar a pequena sobrecarga causada pelas COPY
etapas e na verdade estão gerando on-the-fly um Dockerfile em que o
COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh
sequências são substituídas por
RUN base64 --decode … | sh -x
onde …
é a versão codificada em base64 de apt_setup.sh
.
Meu terceiro conselho é para pessoas que desejam limitar o tamanho e o número de camadas ao possível custo de construções mais longas.
Conselho nº 3 Use o with
idiom para evitar arquivos presentes nas camadas intermediárias, mas não no sistema de arquivos resultante.
Um arquivo adicionado por alguma instrução do docker e removido por alguma instrução posterior não está presente no sistema de arquivos resultante, mas é mencionado duas vezes nas camadas do docker que constituem a imagem do docker em construção. Uma vez, com nome e conteúdo completo na camada resultante da adição da instrução e uma vez como aviso de exclusão na camada resultante da remoção da instrução.
Por exemplo, suponha que precisamos temporariamente de um compilador C e alguma imagem e considere o
# !!! THIS DISPLAYS SOME PROBLEM --- DO NOT USE !!!
RUN apt-get install -y gcc
RUN gcc --version
RUN apt-get --purge autoremove -y gcc
(Um exemplo mais realista criaria algum software com o compilador, em vez de apenas afirmar sua presença com o --version
sinalizador.)
O snippet do Dockerfile cria três camadas, a primeira contém o pacote gcc completo, de modo que, mesmo que não esteja presente no sistema de arquivos final, os dados correspondentes ainda fazem parte da imagem da mesma maneira e precisam ser baixados, carregados e descompactados sempre que o arquivo imagem final é.
O with
idiom é uma forma comum na programação funcional para isolar a propriedade e a liberação de recursos da lógica que o utiliza. É fácil transpor esse idioma para shell-script, e podemos reformular os comandos anteriores como o script a seguir, para ser usado COPY & RUN
como no Conselho # 2.
# with_c_compiler SIMPLE-COMMAND
# Execute SIMPLE-COMMAND in a sub-shell with gcc being available.
with_c_compiler()
(
set -e
apt-get install -y gcc
"$@"
trap 'apt-get --purge autoremove -y gcc' EXIT
)
with_c_compiler\
gcc --version
Comandos complexos podem ser transformados em função para que possam ser alimentados ao with_c_compiler
. Também é possível encadear chamadas de várias with_whatever
funções, mas talvez não seja muito desejável. (Usando recursos mais esotéricos do shell, certamente é possível tornar os with_c_compiler
comandos complexos aceitos, mas em todos os aspectos é preferível agrupar esses comandos complexos em funções.)
Se quisermos ignorar o Conselho nº 2, o snippet do Dockerfile resultante será
RUN apt-get install -y gcc\
&& gcc --version\
&& apt-get --purge autoremove -y gcc
o que não é tão fácil de ler e manter por causa da ofuscação. Veja como a variante do shell script enfatiza a parte importante, gcc --version
enquanto a &&
variante encadeada enterra a parte no meio do ruído.