
Docker
Por Luiz Mineo
Neste mês convidamos o Luiz Mineo pra compartilhar conosco um tema do interesse de muitos, Docker.
Introdução
Lançado inicialmente como um projeto open source pela startup dotCloud em 2013, o Docker rapidamente se consolidou como a principal ferramenta de implantação e gestão de serviços do mercado, sendo adotado em larga escala por empresas como Google, IBM, RedHat, Microsoft e Amazon.
Até então, os principais processos e ferramentas de implantação de serviços utilizavam máquinas virtuais, que oferecem benefícios como contextos isolados e replicabilidade, mas a um custo computacional alto, uma vez que é necessário emular uma camada de hardware para executar um segundo sistema operacional.
Docker não foi a primeira ferramenta a fazer uso do conceito de containers, sendo que o próprio kernel Linux já oferecia um conjunto de serviços para este fim. No entanto, Docker foi a primeira ferramenta a criar um conjunto de convenções simples, porém versáteis, que abrangem desde a criação e distribuição, até a implantação e gerenciamento de serviços.
Em resumo, o Docker oferece as principais vantagens de uma máquina virtual, mas sem o custo computacional elevado, o que permite seu uso em diversos cenários: Servidores em nuvem, ambientes de desenvolvimento e até mesmo dispositivos embarcados. No entanto, como o Docker é construído sobre o kernel linux, ele não pode ser executado diretamente em (ou executar serviços de) outros sistemas operacionais.
Arquitetura
O Docker é composto por três principais componentes:
Docker Daemon: É o serviço responsável por gerenciar imagens e containers. Possui uma API que é utilizada pelo Docker cli para envio de comandos, e que também pode ser utilizada por outras aplicações e serviços.
- Imagens: Uma imagem pode ser vista como um disco virtual, que contém uma aplicação ou serviço pré-instalado, e que pode ser distribuída através de um registry.
- Containers: São “instâncias” de uma imagem, equivalente a uma máquina virtual.
Docker cli: É a ferramenta de linha de comando que pode ser utilizada para enviar comandos para o daemon.
Docker Registry: É o repositório de imagens, de onde o docker pode fazer download (pull) e upload (push). O principal registry ativo hoje é o Docker Hub (https://hub.docker.com/), onde podemos encontrar imagens dos principais serviços e projetos open source. Também é possível manter uma instalação própria do registry, para distribuição de imagens privadas.
Instalação
Existem várias formas de instalar o docker. Para o Ubuntu e distribuições derivadas, como o Linux Mint, é recomendado instalar do repositório oficial com o apt
$ sudo apt-get update && sudo apt-get install docker.io
Para demais distribuições, pode ser utilizado o instalador do Docker, que irá verificar o melhor repositório para fazer a instalação:
$ curl -fsSL https://get.docker.com -o get-docker.sh
$ sh get-docker.sh
Para Windows, é possível instalar o WSL2 e o Docker para Windows, que irá executar o docker em um ambiente virtualizado.
Criação de containers
Para demonstrar o uso do Docker, vamos configurar uma instância do banco de dados MySQL. Para tal, vamos utilizar a imagem oficial do MySQL no Docker Hub:
https://hub.docker.com/_/mysql
Para criar um container, o primeiro passo é verificar na documentação da imagem os seus parâmetros de configuração. Para configurar qualquer container Docker, são necessários três tipos de parâmetros. São eles:
- Mapeamento de portas: Especifica quais portas de rede do container estarão disponíveis para acesso via host (servidor ou máquina onde o serviço do Docker está instalado). A configuração é feita informando a porta do host e a do container. No caso do MySQL, nós iremos expôr a porta 3306 do container, na porta 3306 do host.
- Mapeamento de volumes: Volumes são diretórios do host que poderão ser acessados pelo container. A configuração é feita informando o diretório do host, e o diretório do container em que ele estará acessível. Caso a imagem especifique quais diretórios do container devem ser volumes, existe a opção de não criar um mapeamento explícito na criação do container. Neste caso, o Docker se encarregará de criar um diretório no host para o volume.
Os volumes possuem duas principais funções. A primeira, é a persistência de arquivos: Todo arquivo criado pelo container é excluído quando o container é recriado, a não ser que o arquivo seja salvo em um volume. A segunda, é permitir transferência de arquivos entre host e container. - Variáveis de ambiente: Permite definir configurações específicas para o serviço. No caso do mysql, podemos utilizar as seguintes variáveis:
- MYSQL_ROOT_PASSWORD: Define a senha do usuário root.
- MYSQL_DATABASE: Nome da base de dados que será criada automaticamente, na primeira execução do container.
- MYSQL_USER e MYSQL_PASSWORD: Caso especificados, um usuário com esses dados será criado na primeira execução do container.
O segundo passo, é utilizar o comando docker run para criar o container. Esse comando permite realizar em uma única etapa o download da imagem, criação e execução do container.
- Para específicar o mapeamento de portas, use o parâmetro -p porta-host:porta-container. Também é possível utilizar o parâmetro -P, que irá alocar uma porta aleatório do host, para cada porta exposta pelo container.
Exemplo: -p 3306:3306 - Para especificar o mapeamento de volumes, use o parâmetro -v diretorio-host:diretorio-container.
Exemplo: -v /home/usuario/mysql:/var/lib/mysql - Para especificar variáveis de ambiente, use o parâmetro -e NOME_VARIAVEL=valor.
Exemplo: -e MYSQL_USER=usuario
Além dos parâmetros da imagem, o ‘docker run’ aceita outros argumentos para configurar o container, entre eles:
- –name nome-container: Especifica um nome para o container. Caso não informado, o Docker irá escolher um nome aleatório
- -d: Permite que o container rode em background. Por padrão, ele é executado no shell do usuário.
- –restart always: Define a política de reinicialização do container, caso o container ou o processo do docker seja encerrado. Por padrão, o container nunca é reiniciado automaticamente. Além de ‘always’, é possível informar os valores ‘no’, ‘on-failure’ e ‘unless-stopped’.
Por fim, o último argumento informado ao docker run é o nome da imagem que será utilizada. Caso a imagem informada não exista localmente, e não seja informado um registry, ela será baixada do Docker Hub. Note que é possível especificar também a tag da imagem, que é uma versão específica.
$ docker run -p 3306:3306 -v /home/usuario/mysql:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=meuprojeto -e MYSQL_USER=usuario -e MYSQL_PASSWD=123456 –name mysql –restart always -d mysql:latest
Após a execução do comando, o mysql deverá estar acessível na porta 3306 do host.
Principais comandos do Docker cli
Além do docker run, o Docker cli oferece diversos comandos para gerenciar containers e imagens.
Por exemplo, para verificar se a nossa instância do MySQL está em execução, podemos usar o comando ‘docker ps’, que lista todos os containers em execução:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ff85277753c5 mysql:latest “docker-entrypoint.s…” 4 weeks ago Up 7 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp mysql
Para ver todos os containers, incluindo os que não estão em execução, pode ser usado o parâmetro ‘-a’
Para parar ou iniciar um container, use docker stop e docker start
$ docker stop mysql
$ docker start mysql
Para baixar uma imagem de um repositório, use docker pull
$ docker pull mysql:latest
Para visualizar o log de um container, use docker logs. O argumento –tail permite trazer apenas uma quantidade de linhas a partir do final do log, e o -f permite visualizar o log em tempo real.
$ docker logs –tail=150 -f mysql
Para visualizar o consumo de cpu, memória, transferência de disco e rede por container, use o ‘docker stats’. Esse comando também é atualizado em tempo real:
$ docker stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
ff85277753c5 mysql 1.08% 360.7MiB / 31.23GiB 1.13% 15.9kB / 0B 79.4MB / 14.3MB 38
Para executar um comando em um container em execução, use o docker exec. Esse comando é utilizado principalmente para ter acesso ao shell de um container.
$ docker exec -it mysql /bin/bash
E para excluir um container que não está em execução, use o comando docker rm.
$ docker rm mysql
Existem vários outros comandos disponibilizados pelo Docker Cli, que podem ser consultados na documentação do Docker:
https://docs.docker.com/engine/reference/run/
Criando imagens
Como vimos anteriormente, uma imagem do Docker é equivalente a um disco virtual, que contém um serviço pré-instalado. Para construir uma imagem, o Docker oferece uma linguagem própria de script, que permite definir a sequência de passos necessários para construí-la. Como exemplo, vamos criar uma imagem para um serviço Java empacotado como um jar executável (que é o artefato gerado na configuração padrão do Spring Boot, assim como outros frameworks).
O primeiro passo, é criar um diretório com os arquivos que devem ser incluídos no container (como o jar do nosso serviço), assim como o arquivo Dockerfile, que define as instruções a serem executadas para a criação da imagem. No nosso caso, o Dockerfile pode ter o seguinte conteúdo:
#FROM define uma imagem base
FROM ubuntu:bionic
#RUN executa comandos
RUN apt-get update && apt-get install openjdk-8-jdk -y
#COPY copia arquivos da pasta de contexto para dentro da imagem
COPY service.jar /app/
#VOLUME define diretórios que devem ser tratados como volume
VOLUME /dados
#ENV define variáveis de ambiente, que podem ser sobrescrita na criação do container
ENV JAVA_OPTS=”-Dfile.encoding=UTF-8 -Duser.timezone=America/Sao_Paulo -Duser.country=BR”
#EXPOSE define uma porta que poderá ser exposta parao host
EXPOSE 8080
#Os comandos executados após esta instrução, serão executados a partir deste diretório
WORKDIR /app
#CMD define o comando inicial do container
CMD java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar service.jar
Este exemplo já demonstra as principais instruções aceitas pelo Dockerfile: FROM, RUN, COPY, VOLUME, EXPOSE, ENV, WORKDIR e CMD.
Para construir e distribuir uma imagem, podemos usar os comandos docker build, docker tag e docker push. Exemplo:
$ docker build -t meu-servico . # cria uma imagem com nome ‘meu-servico’, a partir do diretório corrente, que deve conter um Dockerfile
$ docker tag meu-servico meu-servico:1.0 # cria uma tag para a imagem
$ docker push meu-servico:1.0 # publica a imagem
Note que para fazer o push, isto é, publicar a imagem, é necessário estar autenticado em um registry, como o Docker Hub.
Quando o Docker constrói uma imagem a partir de um Dockerfile, cada comando executado gera uma nova camada (layer), que pode ser distribuída independentemente pelo registry. Ou seja, se imagens distintas possuem uma mesma imagem base, ou se uma nova versão de uma imagem é disponibilizada, as camadas já existentes não precisam ser duplicadas no host ou no registry.
Gerenciando múltiplos containers
Em um cenário real, nossa aplicação terá múltiplos containers, como por exemplo, um banco de dados e um serviço. Nesta situação, é comum surgirem dois problemas. O primeiro, é a comunicação entre containers. Para que o container de um serviço acesse o container de uma base de dados, o segundo container precisa expor uma porta de acesso ao host, e o primeiro container deve acessá-la através do IP do host.
Para solucionar este problema, o Docker permite a criação de redes virtuais, de forma que containers associados a uma mesma rede, possam comunicar-se diretamente, sem depender do host.
Para criar uma rede, basta usar o comando abaixo:
$ docker network create minharede
Com a rede criada, podemos criar containers associados a ela:
$ docker run –net=minharede –name mysql –restart always -d mysql:latest
Quando um container é associado a uma rede, ele pode ser acessado por outros containers da mesma rede através do seu nome. Por exemplo, um outro container poderá acessar o banco de dados do exemplo anterior através do endereço mysql:3306. Note que o nome do container é usado como hostname na rede, e que a porta do banco não precisa ser exposta ao host.
Outro problema é a gestão desses containers. Se uma aplicação contém muitos serviços, gerenciar a implantação utilizando o Docker cli pode ser trabalhoso e propenso a falhas.
Para estes casos, o ecossistema do Docker oferece diversas ferramentas, entre elas estão o docker-compose e o Kubernetes.
O docker-compose é um ferramenta bastante simples, que trabalha diretamente sobre a API do Docker daemon, e pode ser usada para gerenciar múltiplos containers em um mesmo host. Para instalá-la, basta fazer o download do seu executável:
$ sudo curl -L “https://github.com/docker/compose/releases/download/1.29.1/docker-compose-$(uname -s)-$(uname -m)” -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
Uma vez instalado, podemos criar um arquivo docker-compose.yml, que descreve a nossa implantação, conforme exemplo abaixo:
version: ‘3’
services:
web:
container_name: web
build:
context: ./ui
dockerfile: Dockerfile
volumes:
– “./ui/webui:/app”
ports:
– “80:80”
restart: always
depends_on:
– api
– mysqldb
api:
container_name: api
build:
context: ./api
dockerfile: Dockerfile
restart: always
volumes:
– “./api:/app”
depends_on:
– mysqldb
mysqldb:
container_name: mysqldb
image: mysql:8
restart: always
environment:
– MYSQL_DATABASE=meudb
– MYSQL_ROOT_PASSWORD=123456
– MYSQL_USER=meudb
– MYSQL_PASSWORD=meudb
ports:
– “3306:3306”
volumes:
– “./data/db/mysql:/var/lib/mysql”
– “./data/db/mysqlconf:/etc/mysql/conf.d”
Nesta configuração, estão sendo criados três containers: web, api e mysqldb. Os parâmetros de cada container são os mesmos do comando ‘docker run’, com algumas exceções. Por exemplo, note que os containers web e api são construídos diretamente de um Dockerfile, ao invés de uma imagem publicada em um registry, como é o caso do mysql. Note também que é possível definir a ordem de criação dos containers através do parâmetro depends_on.
Outro detalhe importante, é que o docker-compose cria por padrão uma rede para os containers, de forma que eles são acessíveis através do seu nome, conforme explicado anteriormente.
Uma vez que a configuração esteja criada, podemos usar o docker-compose para criar e iniciar os containers:
$ docker-compose up -d
Para parar e remover os containers, podemos usar o comando:
$ docker-compose down
Já o Kubernetes, por sua vez, é uma ferramenta voltada para gerenciamento de containers em um cluster de servidores. Ele é voltado para aplicações que demandam escalabilidade e alta disponibilidade.
Entre os recursos implementados pelo Kubernetes, estão:
– Load balancer
– Service discovery
– Centralização de logs
– Monitoramento
– Quotas de recursos (uso de cpu, memória)
– Políticas de atualização de containers
O Kubernetes tem dois componentes principais: O serviço de administração (apiserver) e os agentes, que são executados em cada nó do cluster. Assim como o docker, o Kubernetes também possui uma ferramenta de linha de comando (kubectl) para comunicação com o api server.
Para executar o kubernetes em ambiente de desenvolvimento, existem implementações da especificação do kubernetes que automatizam a criação de um cluster virtualizado, como o Minikube, Kind e Microk8s
O site do Kubernetes possui uma extensa documentação, que cobre os conceitos básicos para criação e gerenciamento de clusters, assim como a execução em ambiente de desenvolvimento:

