Linux Docker Servidor linux Kubernetes

Dockerfile - Boas práticas!

Dockerfile - Boas práticas!

Primeiro vamos entender o que é uma imagem Docker?

Antes de entendermos o que é uma imagem Docker, vamos entender um pouco sobre a base de tudo, como exemplo iremos utilizar uma aplicação rodando em cima de um Bare Metal (Servidor Físico), Hypervisor (Máquinas Virtuais) e Containers.

Repare nas camadas do desenho abaixo ao ler cada tópico, que eu separei por letras A, B e C.

(A) Bare Metal: Temos um Servidor Físico (Hardware) > Kernel Linux > Bins e Libs e por último a nossa aplicação.

(B) Virtual Machine: Na Máquina Virtual, simulamos alguns sistemas operacionais utilizando um hypervisor (virtualizador) que por sua vez simula um sistema operacional com toda a sua arquitetura, ficando da seguinte forma, Servidor Físico (Hardware) > Hypervisor (Virtualizador) > Kernel Linux > Bins e Libs e por último a aplicação. (Isso lá para meados de 2010 em diante, foi quando as máquinas virtuais surgiram, que para a época ajudou muito, porque você não precisaria de um servidor para cada aplicação, já com as máquinas virtuais você conseguiria ter diversas aplicações em uma único servidor, otimizando com custos de hardware, energia e espaço no rack).

(C) Container: Agora o Container ao executar funciona com isolamento de recursos que são divididos em namespaces, repare que ele não emula o Kernel mas utiliza os recursos do kernel do host hospedeiro, podendo ser executado em cima de uma vm ou um bare metal, dessa forma otimizando os recursos de infraestrutura, separando novamente em camadas, ficaria da seguinte forma.. Servidor Físico (Hardware) > Hypervisor (Virtualizador) > Kernel Linux > Namespaces > Bins e Libs e Aplicação.

Dessa forma, como o container roda em qualquer ambiente, você não precisa configurar um servidor para suportar a sua aplicação direto nele, e sim um container com as libs necessárias e o seu código, podendo versionar o container e realizar rollbacks do código por versão, sem a necessidade de reverter commits e compilar novamente a sua aplicação.

image
A conclusão disso tudo é que poupamos os recursos de infraestrutura (Hardware) e podemos rodar nossos containers com nossas aplicações em cima de qualquer Sistema que tenha Kernel Linux, pois vai usar as primitivas do Linux para criar os containers, pra rodar em Windows precisaria de uma VM Linux com Docker instalado.
De forma granular podemos escalar de uma forma horizontal (mais containers) e não vertical (mais recursos de hardware), poupando recursos e acabando com as desculpas de que só funciona na minha máquina rsrs!

Agora voltando para as Imagens Docker

image
Imagens Docker são compostas por sistemas de arquivos de camadas que ficam uma sobre as outras. Ela é a nossa base para construção de uma aplicação, essa imagem pode ser baseada em diversos tipos de sistemas operacionais e nelas contém somente as libs e os pacotes necessárias para o nosso aplicativo funcionar, o container utiliza essa imagem como base para ser executado.

Entendendo as camadas de uma imagem

image
A primeira camada da imagem é RW (Read Write – Leitura e Escrita), ou seja, uma camada que você pode ler e escrever e camadas mais baixas são somente como leitura, (Read Only - Somente Leitura).

Legal, mas onde você quer chegar com isso? Se eu tiver um arquivo de 33MB, na camada mais baixa e você tentar remover este arquivo, ele será “removido” porém ele ainda estará lá, porque está na camada de Read Only, então irá ficar esse “lixo” nas camadas de RO da sua imagem, abaixo um exemplo prático, irei criar um arquivo e depois apagar ele no primeiro Dockerfile irei utilizar dois RUNs um para baixar o arquivo e o outro remover, e no segundo Dockerfile irei remover este arquivo no mesmo RUN, vamos acompanhar o resultado:
image
Conforme o teste acima, no primeiro Dockerfile que utilizamos os dois RUNs, o primeiro RUN para baixar o arquivo e o segundo para remover o aquivo (app:1.0), e no segundo Dockerfile utilizamos somente um RUN para baixar o arquivo e remover (app:1.1), repare que a imagem app:1.0 ficou com 182MB e a app:1.1 ficou com 149MB, isso porque ele ficou na camada de somente leitura.

Com o comando docker image inspect, inspecionamos as camadas das imagens, e a imagem abaixo mostra que o app:1.0 ficou com uma camada a mais que a imagem app:1.1.
image

Como construir “buildar” uma imagem?

Para realizar o build de uma imagem é possível utilizar algumas ferramentas além do Docker, algumas alternativas que eu conheço são o Podman, Buildah, Paketo e Pack CLI.

Mas iremos construir a nossa imagem utilizando o Docker mesmo, que tem a opção de build de imagens, no futuro eu faço um artigo explicando como utilizar as outras ferramentas e a diferença entre elas.
image
O Docker pode construir imagens automaticamente lendo as instruções de um Dockerfile. Mas o que é um Dockerfile? Dockerfile é um documento de texto que contém todos os comandos que um usuário pode chamar através de linhas de comando para montar uma imagem. Usando o docker image build, os usuários podem criar um build automatizado que executa várias instruções de linha de comando.

Para “buildar” uma imagem usando o comando docker image build, passe o parâmetro -t de tag seguido do nome da imagem com uma tag, que represente a sua versão e o local onde está o Dockerfile, o comando ficará da seguinte forma:

docker image build -t meuapp:1.0 .

Obs. O ponto no final do comando significa que estou referenciando o diretório em que me encontro e lá está o Dockerfile.

Vamos ao que interessa hehe

Agora irei ensinar algumas práticas que podemos adotar quando escrevemos um Dockerfile, para otimizar o tamanho da imagem, a quantidade de camadas, reaproveitar comandos e por aí vai…

1 - Então utilize imagens pequenas!

Sempre escolha imagens pequenas, as imagens pequenas irão te proporcionar o push e o pull mais rápido e irá otimizar na utilização de recursos de disco.

Abaixo irei listar algumas opções de imagens por tamanho.

Distribuição Tamanho Downloads Estrelas Ger. de pacotes Mantainer
Alpine 5.61 MB 10M+ 7.4k apk Natanael Copa (an Alpine Linux maintainer)
Ubuntu 72.7 MB 10M+ 10k apt-get Canonical and Tianon.
Debian 114 MB 10M+ 3.8k apt-get Debian Developers Tianon and Paultag.
Amazon Linux 163 MB 10M+ 1k yum Amazon Linux Team
Fedora 180 MB 10M+ 961 yum Fedora Release Engineering.
CentOS 209 MB 5M+ 6.5k yum The CentOS Project.

Imagens listadas por popularidade clique nesse link do dockerhub.

2 - Utilize imagens oficiais quando possível

As Imagens oficiais são mais seguras (isso não significa que irão vir sem vulnerabilidades rsrs), por isso eu recomendo que sempre procure imagens oficiais das empresas ou comunidades mantenedoras.

Para ver no Dockerhub tem uma etiqueta nas imagens que são oficiais.
image
Lembrando que existe imagens oficiais para ferramentas também!
image

3 - Use uma versão específica

Use sempre uma versão especifíca de uma imagem, pois usando sempre a versão mais recente(latest) pode baixar com alguns BUGs, com isso garantirá que você não faça nenhuma alteração acidentalmente e consequentemente quebrando a sua aplicação.
image

4 - Instale somente o necessário

Nada de instalar net-tools, vim, telnet, netcat e por aí vai… (Claro, na hora do troubleshooting vale tudo hehe) mas nada de buildar suas imagens com ferramentas desnecessárias

Por que devemos instalar somente os pacotes necessários?

↳ Otimiza o tamanho da imagem
↳ Administração
↳ Manter a imagem padrão

Por que não devemos instalar pacotes desnecessários?

↳ Aumenta vulnerabilidade da imagem
↳ A imagem fica muito grande

5 - Use o RUN com moderação

Lembra que eu comentei a pouco, que camadas inferiores são somente como leitura, pois é, a cada RUN que você coloca, cria uma camada a mais na sua imagem.

Olhe o Dockerfile de exemplo com diversos RUNs para executar um comando por vez.
image
Agora repare no desenho quantas camadas foram criadas ao executar todos esses RUNs, então utilize o RUN com moderação!
image

6 - Utilize MULTI-LINE, bora economizar camadas! o/

Utilize o MULTI-LINE para quebrar a linha e ficar mais clean a visualização do seu Dockerfile, o MULTI-LINE nada mais é que uma barra invertida que serve de quebra linhas.
image

7 - Qual a diferença entre o && e ; (ponto e virgula)

Quando decidimos executar uma sequência de comandos utilizando somente um RUN, é necessário quebrar a linha com o MULTI-LINE e executar o próximo comando utilizando dois “&” comercial ou ponto e virgula.

&& Usando a sequência de dois “E” comercial ao executar uma sequência de comandos se um dos comandos falhar ele não executa o próximo, por exemplo: apt-get update && apt-get install nginx, se o apt-get update falhar não será executado o próximo comando, e irá falhar o build.

; Usando ponto e vírgula, na sequência de comandos, apt-get update ; apt-get install nginx, se o primeiro comando falhar o segundo comando é executado mesmo assim.

8 - Ordene os Comandos

Por melhores práticas e a imagem ficar fácil de ler, ordene os comandos, ah mais qual é melhor usar? Eu iria na lógica, a versão da imagem que eu quero (FROM) sempre ficará como primeiro parâmetro, instalação dos pacotes (RUN), variáveis de ambiente (ENV), copiar o artefato para dentro da imagem (COPY/ADD) o comando para segurar o processo que vai executar o serviço (CMD/ENTRYPOINT) e por aí vai, é claro que muda de acordo com a sua necessidade, mas sempre que possível comece dessa forma.
image

9 - Sempre limpe o CACHE

Sempre que instalar algum pacote, limpe o cache! Você não quer uma imagem cheia de lixo e com um tamanho grande.
image
A mesma coisa serve para quando você for clonar um repositório.
image

10 - Use o .DOCKERIGNORE

Para não jogar arquivos desnecessários dentro da imagem, utilize o .dockerignore, ele tem a mesma função do .gitignore basta criar um arquivo dentro do diretório onde está o Dockerfile.

Arquivos a serem ignorados

image

11 - Copie somente o que você precisa para dentro da sua imagem

Não utilize carácteres coringa pois pode copiar lixo para dentro da sua imagem.
image

12 - Utilize um usuário NON-ROOT

Não utilize o usuário root para executar um serviço, um exemplo é se você utilizar um webserver em container utilize o usuário www-data/nginx e assim por diante. Utilize sempre um usuário diferente de root!
image

13 - Quais são as diferenças entre CMD, ENTRYPOINT e SHELL

O CMD é utilizado depois que o processo do container é iniciado, os parametros podem ser utilizado de duas formas, a EXEC-FORM e SHELL-FORM.
image
ENTRYPOINT é como se fosse o processo init [PID 1] do Linux, ele é utilizado para “segurar” o principal processo do container, se o processo morrer o container morre, como o CMD podemos também utilizar o exec-form e o shell-form.
image
Podemos utilizar o CMD e o ENTRYPOINT no mesmo Dockerfile, neste caso o CMD vira o parâmetro do meu ENTRYPOINT, mas para utilizar os dois devemos utilizar o modo EXEC-FORM ficando da seguinte forma:
image
Agora o SHELL ele é a mesma coisa que o sh –c para executar o comando no shell.
image

14 - Agora vamos entender melhor o CMD sendo passado como parâmetro para o ENTRYPOINT

Cenário 1

image
Ao executar o comando container run -d ping:1 ele vai pingar o ip 1.1.1.1 que coloquei como parâmetro do CMD no Dockerfile, vamos dar uma olhada no log, veja abaixo que ele pingou o IP do CMD.
image
Agora repare que ao executar o comando docker container run ping:1 8.8.8.8 ele irá pingar o ip 8.8.8.8:
image
Isso é top demais, usar usar uma ferramenta sem ter a necessidade de instalar ela no host ou quando você quer que o container sirva somente para executar uma função que a ferramenta faz e morrer na sequência, um exemplo é usando o busybox, o famoso canivete suíço do linux:
image
Era uma vez um container hehe
image

Cenário 2

image
Agora passando somente o ENTRYPOINT, ele irá executar somente a função que foi atribuída a ele.
image
Vendo isso na prática, repare que ao executar o comando docker container run ping:2 8.8.8.8, para pingar um IP diferente do ENTRYPOINT, ele não irá pingar 8.8.8.8 que passamos abaixo, porque ele já está executando o comando ping 1.1.1.1 que passamos pra ele no ENTRYPOINT.
image

15 - Qual devemos usar? EXEC-FORM ou SHELL-FORM?

Use sempre o EXEC-FORM, pois irá executar o comando como [PID 1], se executar como SHELL-FORM ele irá executar o /bin/sh -c como [PID 1] e o comando que você passou com outro [PID 8], isso te trará problemas quando estiver esperando algum SIGTERM do comando que está executando e ele não irá funcionar!
image
Agora vamos ver usando o EXEC-FORM:
image

É eu sei, que aconteceu isso com a sua mente kkkkk!
image

Ponto importante! Eu fiz o mesmo cenário acima utilizando a imagem alpine:latest e ele funciona como PID 1 mesmo utilizando o SHELL-FORM e EXEC-FORM, então acho que vale a pena validar listando os processos ps aux no container!

16 - Script de START

Você também pode utilizar um script de start, para iniciar a sua aplicação e utilizar o ENTRYPOINT, não é o ideal mas é bom saber que é possível rsrs
image

17 - LABELS

Utilize o LABEL sempre que for inserir um rótulo no container, seja uma descrição, versão, mantainer ou qualquer outra informação que queira adicionar como rótulo.
image

18 - Diferenças entre o COPY e o ADD

Com o COPY eu posso copiar tudo que está no diretório, diretórios ou arquivos para dentro da imagem.
image
O ADD eu consigo fazer tudo que o COPY faz, porém consigo copiar o conteúdo de um arquivo empacotado ou um arquivo em uma URL.
image

19 - VARIÁVEIS

Além de utilizar as variáveis dentro do container, podemos utilizar elas no Dockerfile para ficar mais limpo.
image

20 - Utilizando o ARG

Diferente do VAR, que você passa a variável no dockerfile, com o ARG você pode passar uma variável através da linha de comando, utilizando o parâmetro --build-arg HTTP_PROXY=http://10.20.30.2:1234, passando o nome da variável como chave e valor.

image
image

21 - MULTI-STAGE

É uma maneira de você fazer uma “pipeline” dentro do seu Dockerfile, você copia o artefato da primeira imagem, descartando ela após copiar somente o artefato para dentro da última imagem.
image
Certifique-se de escolher imagens de base compatíveis para cada estágio. É possível misturar imagens que produzem uma imagem final que não funcionará corretamente.

22 - CACHE

Se retirar o cache na hora do build, pode ter certeza que irá demorar muito mais para baixar a imagem.
No Docker podemos ou não utilizar o cache, logo abaixo um exemplo de como forçar para não utilizar o cache.

image

23 - Defina o WORKDIR

O WORKDIR define o diretório de trabalho para quaisquer instruções RUN, CMD, ENTRYPOINT, COPY e ADD que o seguem no Dockerfile. Se o WORKDIR não existir, os arquivos serão enviados para o / (barra) a não ser que você passe uma Path específica.

image

24 - Colocar o HEALTH CHECK

Coloque o Health Check em seu Dockerfile quando necessário.
image
Parâmetros que podem ser utilizados:

--interval=10s DURAÇÃO (default: 30s)
--timeout=30s DURAÇÃO (default: 30s)
--start-period=30s DURAÇÃO (default: 0s)
--retries=3 RETENTATIVAS (default: 3)

O status de saída do comando indica o status de integridade do container. Os possíveis valores são:
0: success - O container está íntegro e pronto para uso
1: unhealthy - O container não está funcionando corretamente
2: reserved - Não use este código de saída

Abaixo um exemplo de como fica os status check.

Dockerfile

25 - Habilite BUILDKIT e o modo DEBUG!

Vantagens da utilização do Buildkit:
↳ Visualizar o tempo do build.
↳ Visualizar por camadas.

Vantagens de ativar o modo debug:
↳ Caso dê problemas irá informar as possíveis causas, pondendo ser: proxy, conexão ou qualquer outro problema.

Habilitando no Linux:
$ sudo vim /etc/docker/daemon.json 

{  
"experimental" : false,  
"debug" : true,  
"features":{ 
    "buildkit" : true 
  }  
} 

Salve e saia do arquivo: ESC :wq Reinicie o serviço do Docker:

$ systemctl restart docker.service 
Habilitando no Windows:

Clique com o botão direito do mouse no ícone do Docker na barra de tarefas do Windows e clique em Settings > Docker Engine.
image
Em seguida clique em Apply & Restart, para aplicar as configurações!

Visualizando o BUILD

Dockerfile

Desafio

Pegue um cenário real, algum Dockerfile do seu trampo e otimize ele com as dicas que eu dei, irei colocar um aqui como exemplo.
Dockerfile

Encontre todos os parâmetros que podemos passar no Dockerfile no documento de refência do Docker.

Hackers hoje é só, espero que tenha ajudado vcs essas algumas dicas de como otimizar seu Dockerfile! ;)

Se leu este artigo até o fim, coloque a resposta da pergunta abaixo nos comentários hehe
A pergunta é: Se eu tiver 100 containers de 1GB utilizando a mesma imagem em um nó do cluster, quanto de espaço em disco estarei utilizando deste nó?

Referências

Docker - Container image layers
Docker - Build Enhancements
Jeferson Fernando - LinuxTips - Images Deep Dive
Docker - Dockerfile best practices

comments powered by Disqus

Assine nossa Newsletter! 🐧

Se una com os assinantes de nossa Newsletter, sempre que tiver postagem nova você será notificado.