2 Docker 🐳
Docker é uma plataforma para o gerenciamento de containers (Schommer 2014), que ajuda desenvolvedores e administradores de sistemas a desenvolver, distribuir, implantar e executar aplicações em ambientes isolados, sem problemas com dependências do ambiente ou configurações.
Ao utilizar o Docker, todos os problemas relacionados a instalação, configuração e dependências são facilmente resolvidos. Atualmente o Docker vem sendo aplamente utilizado por sua facilidade de uso e baixa utilização de recursos computacionais para a implantação de sistemas e execução de algoritmos.
Todo o funcionamento do Docker é baseado em uma comunicação cliente-servidor, onde o cliente, através de uma API Rest envia comandos para o docker-daemon, que por sua vez representa o gerenciador de containers que realiza todas as operações e verificações necessárias para o funcionamento correto e simples do Docker. Esta estrutura é resumida na Figura abaixo.
A forma como fazemos a utilização da estrutura acima pode variar, já que se trata de uma API Rest, básicamente, qualquer linguagem de programação que suporte a comunicação através de protocolos de rede como HTTP conseguem se comunicar e interagir com o docker-daemon. Veja a Figura abaixo.
Na Figura acima é possível entender como o processo de comunicação e interação com o docker-daemon funciona. O cliente envia comandos através da API Rest e o servidor recebe tais requisições, verifica sua validade e então realiza as operações necessárias.
Este tipo de estrutura permite que, através de uma máquina seja feito o gerenciamento de vários servidores Docker, sem contar que, por ser uma API Rest, aplicações podem fazer interações com o servidor, o que facilita ainda mais a criação e disponibilização de ferramentas para o gerenciamento de containers através do Docker.
O ambiente do Dockeré constituido por três componentes principais (Chung et al. 2016), sendo eles, Docker images e Dockerfiles, Docker registry e Docker containers. Cada um desses explicados nas subseções seguintes.
2.1 Instalação
Com a visão geral de funcionamento do Docker, vamos fazer sua instalação para começarmos a utilização e entender os conceitos na prática! Por uma questão de facilidade, recomenda-se a utilização de um ambiente Linux.
Os passos abaixo, apresentam a instalação do Docker no Linux Ubuntu (16.04 ou superior).
$ curl -fsSL https://get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh
Caso você queira permitir a execução do Docker para usuários que não sejam root você pode executar o comando abaixo
$ sudo usermod -aG docker seu-usuario
Caso você não execute o comando de permissão acima, todos os seus comandos do Docker deverão ser executados pelo root ou junto ao comando
sudo
.
Feito! A instalação do Docker já está pronta e funcionando na sua máquina, para testar execute o comando docker -v
, o retorno deve ser algo parecido com isto
Docker version 18.09.7, build 2d0083d
O comando docker
e todos os seus parâmetros representam a ferramenta Docker CLI, que foi citada anteriormente e já vem instalada na distribuição padrão do Docker.
Se você estiver utilizado uma outra plataforma que não a apresentada acima, você pode consultar o site do Docker (https://docs.docker.com/install/) para verificar como prosseguir com a instalação.
2.2 Containers
Containers são instâncias de Imagens Docker que estão sendo executadas em ambientes isolados, sendo que, nestes ambientes há todos os recursos necessários para a execução do processo que foi definido. Por exemplo, quando você quiser executar um container com o Postgres, ao realizar a execução, dentro do ambiente que é gerado, todas as bibliotecas necessárias para a execução do Postgres, inclusive os binários do banco propriamente dito já estarão disponíveis.
2.3 Imagens de containers
Como citado anteriormente, um container representa uma imagem Docker que está sendo executada. As imagens Docker, por sua vez, representam arquivos executáveis que possuem todo o descritivo de arquivos e processos que devem ser feitos no momento em que são executadas para a geração de um container.
Com isto, as imagens Docker garantem que, todos os containers gerados através da mesma imagem sejam padronizados, tendo uma mesma estrutura.
2.3.1 Criando imagens
A criação de imagens Docker é feita através da utilização de arquivos Dockerfiles
, estes que descrevem qual será a estrutura das imagens e suas operações.
Dentro do Dockerfile
existem diversas instruções para ditar cada caracteristica que deve ser empregada na imagem que está sendo gerada.
Para você entender melhor, vamos criar um exemplo de uma imagem Docker que gera um container que executa um script Python.
Vamos começar criando o script Python, fazemos isto utilizando o comando abaixo
echo "print('Oi! Esta é minha primeira imagem Docker! E ela funciona!')" > ola.py
Agora, no mesmo diretório onde está o arquivo do script, vamos criar um arquivo com o nome Dockerfile
, dentro deste arquivo vamos inserir o seguinte conteúdo (Não se preocupe se você não entender agora, cada uma das partes deste arquivo será explicada).
FROM python:3
COPY ola.py ./
CMD [ "./ola.py" ]
ENTRYPOINT [ "python" ]
Ao finalizar a edição do arquivo, vá até o diretório onde o arquivo está criado, e execute o comando docker build
.
docker build -t "minha_primeira_imagem:1.0" .
Com este comando a sua imagem Docker será gerada Para verificar se ela realmente foi criada, execute o comando docker images
, que lista todas as imagens disponíveis em sua máquina. Ao digitar este comando você perceberá que existe uma imagem com REPOSITORY
de nome minha_primeira_imagem:1.0
.
Para utilizar a imagem criada para gerar um container, vamos fazer a execução da imagem
docker run minha_primeira_imagem:1.0
Caso queira apagar a imagem que criamos, utilize o comando
docker rmi
(docker rmi minha_primeira_imagem:1.0)
2.3.1.1 Entendendo o Dockerfile
Anteriormente foi visto um simples Dockerfile
, que criou uma imagem para a execução de um script Python, vamos analisa-lo para entender o que foi feito.
Inicialmente no arquivo foi importado uma imagem com nome python:3
, isto é feito com o comando FROM
. Ou seja, sua imagem foi criada com base em uma outra image que já possuia o Python 3 instalado.
Após a definição da imagem base, foi feito uma cópia do script para dentro do container, através do comando COPY
.
A definição do processo que o container irá executar é feita através do comando ENTRYPOINT
, ou seja, no nosso caso o processo principal será uma execução python, e o comando CMD
faz o auxílio ao processo principal, indicando os parâmetros que serão passados para o ENTRYPOINY
.
Existem muitas outras instruções que poderiam ser aplicadas no Dockerfile
, para saber mais sobre eles utilize a documentação do Docker (https://docs.docker.com/engine/reference/builder/).
2.3.1.2 Camadas de uma imagem
Para a geração de imagens, o Docker utiliza um conceito de camadas, onde cada comando do Dockerfile
indica uma camada que será gerada na imagem. Estas camadas são utilizadas para permitir o versionamento da imagem e suas mudanças.
As camadas são organizadas como pilhas, onde a última camada sempre estará no topo, além disso, é sempre a última camada que pode ser modificada, todas as demais abaixo desta são read-only, não podendo ser modificadas. Para fazer mudanças em conteúdos das camadas read-only uma copia desta é criada e alterada.
Para entendermos melhor como as camadas funcionam, vamos criar um pequeno exemplo. Veja o seguinte Dockerfile
.
FROM ubuntu
RUN apt update -y
RUN apt install vim -y
ENTRYPOINT [ "bash" ]
Ao executar o comando docker build
com o Dockerfile
acima, a seguinte estrutura de camadas será criada.
A saída do comando
docker build
já apresenta as camadas que estão sendo criadas, onde para cada uma há umID
.
Para cada comando foi criado uma camada, que por sua vêz tem um peso. Das camadas 1 a 4 nada mais pode ser alterado, o que fez o exemplo acima ter um problema, quando o comando apt update -y
foi executado, um cache foi criado e certamente não vai mais ser utilizado. O problema é que este cache ficou em uma camada read-only e não poderá mais ser modificado. Se o comando para limpar o cache for utilizado, a camada onde do cache será copiada para o topo e então editada.
Ou seja, mais uma camada foi criada, porém o cache ainda continua lá. Para resolve reste problema é preciso melhorar a forma como o Dockerfile
foi criado, tentando executar tudo o que for possível em uma única camada, por exemplo.
FROM ubuntu
RUN apt update -y && apt install vim -y && apt clean
ENTRYPOINT [ "bash" ]
Com o Dockerfile
acima, somente três camadas serão criadas, já que, toda a modificação para a instalação do vim
é feita em uma única camada, que tem o cache removido.
Com isto é possível perceber a necessidade de otimizar os
Dockerfiles
e evitar camadas desnecessárias que só ocupam espaço
Para saber mais formas de otimização de Dockerfiles
, consulte a documentação do Docker (https://docs.docker.com/develop/develop-images/dockerfile_best-practices/).
2.3.2 Aquisição de imagens
Além da criação é possível também fazer a aquisição de imagens disponibilizadas pela comunidade, ou mesmo distribuída por algum instituto ou empresa com seus sistemas já configurados e prontos para a execução.
Para isso utiliza-se o Docker Registry, um componente que está dentro da plataforma Docker e que facilita muito a distribuição de imagens. Registry pode ser público ou privado. Um exemplo muito útil de Registry público é o Dockerhub.
Por fazer parte da plataforma, o registry já está integrado ao funcionamento do docker-daemon. Vamos fazer alguns testes para entender.
Com o comando docker images
, liste todas as imagens em sua máquina. Você provavelmente terá a imagem criada e a imagem do python:3
(Você já vai entender de onde ela veio).
Vamos então tentar executar uma imagem que não está na sua máquina.
docker run centos:7
Mesmo você não tendo a imagem nomeada centos:7
em sua máquina o comando está sendo executado, isso porque o docker-daemon, por padrão, ao não encontrar em sua máquina vai até o Docker hub e verifica se tem uma imagem com o nome que você inseriu, caso tenha ele baixa e continua a execução de seu comando. Veja este fluxo na Figura abaixo.
Durante os passos da criação do Dockerfile essa feature foi utilizada, ao inserir a imagem de nome python:3
na instrução FROM
do Dockerfile
, o docker-daemon
verifica se há a imagem na máquina, como não tinha ele baixou para então continuar a criação da imagem.
2.4 Arquitetura
Com todos os componentes básicos do Docker já conhecidos, há a possibilidade de expansão da visão geral de toda a estrutura e funcionamento do Docker. Inicialmente na parte estrutural haviamos definido apenas a forma de comunicação, veja agora na Figura abaixo, como todos os componentes apresentados até aqui fazem integração.
Com isso estamos prontos para iniciar as atividades práticas com o Docker e o gerenciamento de containers.
2.5 Administrando containers
Esta seção apresenta exemplos para a administração de containers através da utilização do Docker CLI.
2.5.1 Criando containers
Vamos começar com exemplos das diferentes formas de criação de container. Começando com a criação básica de um container do Debian.
docker create debian
Depois de criar, utilize o comando docker ps -a
para listar todos os containers criados. Quando você listar, vai ver várias informações sobre o container criado, como por exemplo a imagem que ele utilizou, o ID, se há portas de rede abertas e o nome. Por padrão, não é necessário definir o nome do container, mas é recomendado que o faça, então vamos excluir o container criado e gerar um novo com um nome definido.
# Excluíndo container
docker rm ID_DO_CONTAINER_CRIADO
# Criando um novo container de nome exemplo_debian
docker create --name exemplo_debian debian
Ao utilizar novamente o comando docker ps -a
, você vai ver o novo container, com o nome que foi definido. O container ainda não está sendo executado, uma vez que apenas fizemos sua criação, vamos então executar ele com o comando docker start
.
docker start exemplo_debian
Ele foi executado, para listar somente os containers que estão sendo executados, utilize o comando docker ps
, sem qualquer outro parâmetro.
Sua listagem estará vazia já que o container do Debian está configurado para executar o comando bash
, então, ao iniciarmos o container ele executou o comando e em seguida finalizou, se quisermos manter o container ligado, devemos definir que ele terá um terminal interativo, para isto no momento da criação, os parâmetros -ti
devem ser passados. Vamos lá então, excluir o que haviamos criado e gerar ele novamente com os novos parâmetros
# Excluíndo container exemplo_debian
docker rm exemplo_debian
# Gerando um novo container que trabalha em segundo plano
docker create -ti --name exemplo_debian debian
# Executando o novo container criado
docker start exemplo_debian
Ao executar os comandos o novo container já estará sendo executado, para verificar, liste novamente os containers em execução. Vamos agora acessar o container criado, para isto usamos o comando docker attach
, que recupera o bash
do container caso ele não esteja executando outro processo.
docker attach exemplo_debian
Em outros cenários o
bash
pode não estar disponível, então ao invês dodocker attach
pode ser utilizado odocker exec
(https://docs.docker.com/engine/reference/commandline/exec/)
Feito o attach
você estará dentro do terminal do container. Como dito anteriormente, este ambiente é isolado de sua máquina, então faça testes, navegue entre os diretórios, para você ver que é uma instância completamente isolada. Para sair do terminal e não finalizar o container utilize os seguintes botões de seu teclado: CTRL + p + q
. Ao listar novamente os containers em execução, lá estará ele sendo executado.
Nos exemplos anteriores criavamos o container depois faziamos sua execução, porém podemos já criar um container e imediatamente realizar sua inicialização, para isto utilizamos o comando docker run
, que recebe os mesmos parâmetros que o docker create
, com a diferença de que ele executa o container após sua criação.
docker rm -f exemplo_debian
docker run -ti --name exemplo_debian debian
Executando o comando acima, o container irá iniciar e você já estará em seu
bash
.
2.5.2 Gerenciando as execuções de um container
Com os containers em execução, vamos entender quais são os estados de um container e como podemos fazer a manipulação desses.
Ao criar um container com docker create
o status dele é CREATED
e as modificações dessa estado são feitos com os comandos na Tabela abaixo, junto aos comandos, há os estados que eles geram no container.
Comando | Estado gerado |
---|---|
docker create | CREATED |
docker run | UP |
docker start | UP |
docker stop | EXITED |
docker restart | UP |
docker pause | UP (PAUSED) |
docker unpause | UP |
Para entender cada um dos estados que estes comandos geram em um container vejamos a Tabela abaixo.
Estado | Descrição |
---|---|
CREATED | Representa que o container foi criado, possui uma camada que pode ser escrita e está pronto para ser iniciado |
UP | Indica que o container está em execução |
UP (PAUSED) | Indica que o container está “ligado” porém sua execução está parada |
EXITED | Indica que a execução do container foi finalizada |
Estes estados nos ajudam a entender o que está ocorrendo com os containers, se estão trabalhando sem nenhum problema ou mesmo se estão ou não sendo executados.
2.5.3 Visualizando status do container
Além do estado do container, em certos casos é necessário avaliar outras informações do container. A Tabela abaixo descreve comandos que podem ser úteis para a avaliação e verificação de status dos containers.
Comando | Descrição |
---|---|
docker stats | Exibe estatísticas de utilização de recursos feitas pelo container em tempo real |
docker top | Exibe os processos que estão sendo executados no container |
docker logs | Exibe os logs do container. A forma que os logs são exibidas depende da forma como o serviço que está sendo executado registrou os logs |
docker inspect | Exibe informações gerais do container, no formato JSON |
2.5.4 Gerenciamento de memória e CPU
Durante as verificações de estado e utilização de um container, pode ser necessário atribuir limites de utilização, evitando o consumo excessivo de recursos, que pode prejudicar o servidor onde o Docker está trabalhando.
Quando não é atribuido limites de recursos a um container, se necessário, ele pode consumir todos os recursos da máquina onde está sendo executado.
Vamos começar criando um container normalmente, como haviamos feito até aqui.
docker run -ti --name debian_de_teste debian
Agora, com a ajuda do comando docker inspect
, vamos avaliar a quantidade de memória configurada para uso deste container.
docker inspect debian_de_teste | grep -i memory
A saída do comando acima é parecida com isto:
"Memory": 0,
"KernelMemory": 0,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
O campo Memory
está com valor 0, isto indica que, para este container não há limitação de uso de memória. Para resolver este problema, vamos excluir este container e gerar um novo com um limite de memória.
# Excluíndo container sem limite de memória
# -f Para excluir mesmo se o container estiver com estado UP
docker rm -f debian_de_teste
# Criando container com limite de 512 MB
docker run -ti --name debian_de_teste --memory 512m debian
Após os comandos, caso eu execute novamente o docker inspect
será possível visualizar a mudança no limite de memória.
docker inspect debian_de_teste | grep -i memory
Saída:
"Memory": 536870912,
"KernelMemory": 0,
"MemoryReservation": 0,
"MemorySwap": -1,
"MemorySwappiness": null,
Com isso, o container não vai consumir mais que 512 MB de memória da máquina onde está sendo executado. Outra cenário possível para a mudança de quantidade de memória é quando o container já está funcionando e você precisa mudar seu limite de memória. Isto pode ser feito através do comando docker update
.
Vamos mudar a quantidade de memória do container de testes que geramos acima.
docker update --memory 256m debian_de_teste
O comando
docker update
básicamente recebe o parâmetro que precisa ser alterado, o novo valor e ocontainer id
oucontainer name
para fazer a mudança.
Ao consultar novamente a quantidade de memória, veremos que ela foi atualizada para 256 MB
"Memory": 268435456,
"KernelMemory": 0,
"MemoryReservation": 0,
"MemorySwap": -1,
"MemorySwappiness": null,
Além da memória pode ser necessário limitar a quantidade de CPU que o container pode utilizar. Todos os principios apresentados na mudança de memória podem ser aplicados aqui, ou seja, pode ser atribuido valores de limitação na criação ou em containers já em execução, o ponto para a CPU é que os valores atribuidos funcionam um pouco diferentes, através de uma lógica de proporção.
Vamos para um exemplo, neste três containers serão criados.
docker create -ti --name debian_1 --cpu-shares 1024 debian
docker create -ti --name debian_2 --cpu-shares 512 debian
docker create -ti --name debian_3 --cpu-shares 512 debian
docker start debian_1 debian_2 debian_3
O parâmetro
--cpu-shares
é quem define os valores de utilização da CPU
Lembre-se que, para sair do container sem fecha-lo utilize as teclas
CTRL + p + q
.
Após criar os três containers vamos entender o pensamento por trás da proporcionalidade. O primeiro container possui 1024 cpu-shares
e os outros dois containers criados possuem 512 cpu-shares
cada. O segundo e o terceiro container criados, tem cada um 50% do valor atribuido para o primeiro container.
Quando todos os valores atribuidos a cada container são somados, chegamos em um valor de 2048. Sobre este valor, se aplicarmos uma proporcionalidade, é possível entender que o primeiro container possui 50% do valor total e os outros dois 25% cada um. Estes valores da proporcionalidade, são exatamente as quantidades que cada um poderá utilizar do 100% total de utilização da CPU, assim, o primeiro poderá utilizar até 50%, enquanto os outros dois poderão utilizar até 25% cada um.
Lembrando que o comando
docker update
funciona da mesma forma como apresentado no controle de memória, com a diferença de que o parâmetro inserido para este caso será o--cpu-shares
.
2.5.5 Armazenamento
Para finalizar este conteúdo básico sobre Docker, vamos tratar de algumas formas de armazenamento possíveis nos containers. Existem três formas possíveis de armazenamento em um container, cada uma dessas formas é apresentada na Tabela abaixo.
Tipo de armazenamento | Descrição |
---|---|
Volumes | São formas de armazenamento que ficam registradas em lugares específicos do sistema de arquivos da máquina que executa o Docker e todo seu controle é feito pelo docker-daemon |
Bind mounts | Podem ser armazenados em qualquer lugar do sistema de arquivos da máquina que executa o Docker e o docker não faz seu gerenciamento |
tmpfs | Sistema de arquivos temporários criados diretamente na memória e não são salvos |
A escolha dentre essas possibilidades pode variar de acordo com seu cenário de utilização. Não deixe de consultar a documentação do Docker para dicar e formas de utilização de cada uma dessas (https://docs.docker.com/storage/)
Cada uma dessas formas listadas na Tabela anterior, indicam formas de criar mapeamentos de unidades e arquivos entre o container e a máquina onde o docker-daemon está sendo executado. Veja a Figura abaixo.
Como é possível perceber na Figura acima, o mapeamento faz básicamente com que os arquivos ou diretórios do sistema de arquivos do container sejam armazenados em outros contextos que não o do próprio container, tendo como formas de mapeamento, os itens listados na Tabela acima.
Vamos focar aqui em apresentar os Volumes
e os Bind mounts
. Vamos começar criando um container que possui um volume.
docker run -ti -v /volume --name ubuntu_com_volume ubuntu
Ao executar o comando e estar dentro do container vá até o diretório raiz /
e execute o comando ls
, para listar os diretórios.
Na listagem você verá que há um diretório de nome volume
, acesse ele e crie um arquivo qualquer.
cd /volume && > arquivo_qualquer.txt
Feito isto, vamos sair do container e verificar em qual local do disco o volume foi criado.
Para sair do container não esqueca de utilizar o
CTRL + p + q
, o que evita a finalização do processo principal do container.
Após sair vamos verificar onde o volume foi criado com o auxílio do comando docker inspect
.
docker inspect -f {{.Mounts}} ubuntu_com_volume
O parâmetro
-f
ajuda na filtragem do comandodocker inspect
.
A saída do comando é parecido com isto
[{volume 3af1c580d4679973c86fbe7b6cdc755595a726f41aec7c8c59186f10885732fa /var/lib/docker/volumes/3af1c580d4679973c86fbe7b6cdc755595a726f41aec7c8c59186f10885732fa/_data /volume local true }]
Veja que, ele indica onde o diretório /volume
que foi montado dentro do container está na máquina local. Vamos então acessar este diretório e verificar seu conteúdo.
cd /var/lib/docker/volumes/3af1c580d4679973c86fbe7b6cdc755595a726f41aec7c8c59186f10885732fa/_data
Após mudar o diretório e listar seu conteúdo, você provavelmente verá o arquivo que foi criado arquivo_qualquer.txt
, isto ocorre já que, como foi explicado, os volumes permitem o mapeamento de arquivos e diretórios que estão presentes no sistema de arquivos do container.
Outra da forma de realizar o mesmo processo é fazendo a utilização de bind mounts que evita a criação de volumes e permite que o mapeamento seja feito diretamente entre um diretório/arquivo da máquina onde o docker-daemon é executado e um diretório/arquivo do container. Para vermos isto na prática, vamos excluir o container de testes que criamos anteriormente e gerar um novo contendo esta forma de mapeamento.
# Excluindo o container gerado anteriormente
docker rm -f ubuntu_com_volume
# Gerando container com bind mounts
mkdir /arquivos_containers
docker run -ti -v /arquivos_containers:/volume --name ubuntu_com_volume ubuntu
Antes de criar o container foi necessário criar um diretório, isto porque este será vinculado diretamente com o
/volume
que está dentro do container, assim, todo o conteúdo que foi criado lá, estará sendo criado dentro deste diretório que foi gerado.
Dentro do container, vá até o diretório volume
e crie um arquivo qualquer.
cd /volume && > arquivo_qualquer_2.txt
Agora saia do container e liste o diretório /arquivos_containers
, você vai perceber que o arquivo criado no container está lá.
Lembrando que, a forma que você escolher para mapear os dados pode depender completamente de seu contexto e sua necessidade.
Então, chegamos ao fim desta parte da documentação, tudo o que foi mostrado até é um guia básico de como realizar a utilização do Docker, não deixe de conferir a documentação no site oficial e também de fazer testes para ir aprendendo mais!