Introdução ao Docker
Tags: docker, beginners, devops, braziliandevs
Published: 2022-12-21 11:17:08

Resumo dos conceitos do livro *Learn Docker in a
Month of Lunches* do autor Elton Stoneman.

![visão geral do docker](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zac5mnb30498j08e6jz7.png)

**Docker** começa em um simples conceito: empacotar um aplicativo com todas as suas dependências, para que você possa executar esse aplicativo da mesma maneira em qualquer lugar.

**Docker** facilita a transição entre serviços de nuvem e faz com que o projeto não fique preso a nenhum serviço de PaaS ou IaaS.

Monólitos não são tão úteis quando se usam Docker. Mover uma aplicação para contêineres pode ser o primeiro passo para a modernização da arquitetura do sistema. Dessa forma, usando Docker, pode-se usar a arquitetura de microsserviços e cada função em seu próprio contêiner, possibilitando pequenas funções, que podem ser facilmente testadas e isoladas uma das outras.

## O que é um Contêiner?

Similar a ideia de um contêiner físico, uma caixa, onde dentro dessa caixa contém a aplicação e um computador com próprio endereço IP, disco rígido e nome. Tudo isso criado e gerenciado pelo Docker para executar a aplicação. A aplicação não pode ver nada fora do contêiner, além disso, esse contêiner está sendo executado em um computador, que pode conter outros contêineres, porém todos compartilham a mesma CPU, memória RAM e OS.

![visão geral do docker, exibindo a relação do docker com o sistema operacional](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rexth30f1lvb2kt7ao7o.png)

Contêineres são executados apenas quando a aplicação está sendo executada. Assim que o processo encerra, o contêiner vai para o estado *Exited*, logo não consomem CPU e memória.

Para se conectar a um terminal dentro de um container de uma imagem chamada `diamol/base`,

```bash
docker container run --interactive --tty diamol/base
```

A flag `--interactive` diz ao Docker que deseja estabelecer uma conexão e a flag `--tty` significa que deseja se conectar ao terminal.

Lista todos os contêineres.

```bash
docker container ls --all
```

Lista contêineres em execução.

```bash
docker container ls
```

Mostra informações de um contêiner em execução, como CPU, memória, rede, e disco.

```bash
docker container stats
```

Remover todos os contêineres

```bash
docker container rm --force $(docker container ls --all --quiet)
```

## Hospedando um Website usando Docker

Um simples exemplo de como executar um website em um contêiner a partir da imagem `diamol/ch02-hello-diamol-web`

```bash
docker container run --detach --publish 8088:80 diamol/ch02-hello-diamol-web
```

Onde `--detach` (`-d`) executa o contêiner em background e `--publish` (`-p`) expõe uma porta do contêiner ao computador.

Assim, o tráfego pode ser interceptado pelo Docker e enviado ao contêiner. Neste exemplo, a rede enviada a porta do computador pela porta 8088 será enviada pelo Docker a um contêiner para a porta 80.

![executando um website em um container, a imagem mostra o redirecionamento da porta do computador para o contêiner](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1poqxaltcz5btb6meoul.png)

## Entendendo como Docker funciona

![visão geral dos componentes do docker, docker api, docker engine e docker cache](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9l5xbx99ffo3bh2ojxbf.png)
Docker possui alguns componentes internos

- **Docker Engine** é o componente de gerenciamento do Docker. É o responsável por buscar imagens locais ou baixar imagens quando necessário e reutilizá-las se já baixadas. Trabalha com o sistema operacional para criar contêineres, redes virtuais e outros recursos do Docker.
  
- **Docker API** é o modo que todas as funcionalidades do Docker Engine estão disponíveis, uma REST API padrão. É acessado do computador local por padrão, mas também pode ser configurada para acessada por outros computadores da rede.
  
- **Docker CLI** é o cliente do Docker API. Quando comandos são executados, na verdade, são enviados ao Docker API e o Docker Engine realizada a tarefa.


## Construindo Docker Images

**Dockerfile** é um simples script usado para empacotar uma aplicação e na sua saída gera um arquivo de imagem Docker.

As instruções mais usadas são:

- **FROM:** toda imagem é gerada a partir de outra, em geral, uma depedência;
- **ENV:** configura as variáveis de ambiente. A sintaxe é `[key]="[value]"`;
- **WORKDIR:** cria um diretório na imagem do sistema que será o diretório de trabalho;
- **COPY:** copia arquivos ou diretórios para o diretório local da imagem/container;
- **CMD:** especifica os comandos para executar a aplicação quando o Docker inicia o container a partir da imagem.
- **RUN:** executar comandos

Um exemplo de Dockerfile é mostrado abaixo.

```docker
FROM diamol/node

ENV TARGET="blog.sixeyed.com"
ENV METHOD="HEAD"
ENV INTERVAL="3000"

WORKDIR /web-ping
COPY app.js .

CMD ["node", "/web-ping/app.js"]
```

Para transformar um Dockerfile em uma Docker image execute o comando no diretório onde se encontra o Dockerfile

```bash
docker image build --tag web-ping .
```

Onde `--tag` é o argumento para o nome da imagem e o argumento final é o diretório onde o Dockerfile e os arquivos relacionados se encontram.

Para executar o contêiner a partir da imagem pode-se usar o comando

```bash
docker container run web-ping
```

Ou executar com outras variáveis de ambiente. Isso dá flexibilidade de executar o mesmo contêiner, porém com parâmetros diferentes.

```bash
docker container run -e TARGET=docker.com -e INTERVAL=5000 web-ping
```

## Entendendo Docker images

Uma imagem no Docker é uma coleção de camadas de imagens. Essas camadas são arquivos que ficam no Docker Engine. Assim, essas camadas podem ser compartilhadas entre diferentes contêineres.

![Camadas são compartilhadas entre diferentes imagens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6n6sf3mf75hm7p9di0h5.png)

Lista todas as imagens e o tamanho

```bash
docker image ls
```

Ao executar este comando, o espaço ocupado desconsidera as camadas compartilhadas

Mostra exatamente o espaço ocupado pelo Docker

```bash
docker system df
```

Se o código não mudar entre as builds, o Docker reconhece e utiliza a build anteriormente salva, assim não duplica as imagens. Portanto, é recomendado ordenar as linhas de instruções no Dockerfile pela frequência em que elas mudam, logo as linhas mais improváveis de mudar vem primeiro, justamente para que o Docker utilize o cache e execute apenas as últimas instruções, economizando tempo, disco e rede.

Além disso, se as imagens são compartilhadas, elas não podem ser modificadas, forçando-as a serem apenas para leitura, assim impede que uma modificação altere outras imagens em cascata.

## Empacotando código-fonte em Docker Imagens

Para executar um determinado código, é necessário um conjunto de ferramentas, que pode ser trabalhoso quando se trabalha em equipe. Dessa forma, o Docker pode criar um script com todas as ferramentas de desenvolvimento, além de compilar o código-fonte e o resultado é a aplicação. Isso favorece que no contêiner da aplicação não tenha nada além do necessário, diminuindo o tamanho da imagem, aumentando o tempo de inicialização e menos hardware para executá-lo, além de melhorar a segurança, pois há menos software disponível para potenciais ataques.

Para isso acontecer pode ser usado um Dockerfile multi etapas, onde cada etapa é independente, porém arquivos e pastas podem ser copiadas entre etapas e o resultado é apenas a etapa final.

![Processo de um Dockerfile multi etapas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oa2dojf9l6gr7yqjbvqb.png)

Um exemplo de Dockerfile multi etapas segue abaixo usado para um código em Java, onde cada etapa é identificada pela instrução `FROM`. Cada etapa também pode ser chamada por um nome usando o comando `AS`.

```dockerfile
FROM diamol/maven AS builder

WORKDIR /usr/src/iotd
COPY pom.xml .
RUN mvn -B dependency:go-offline

COPY . .
RUN mvn package

# app
FROM diamol/openjdk

WORKDIR /app
COPY --from=builder /usr/src/iotd/target/iotd-service-0.1.0.jar .

EXPOSE 80
ENTRYPOINT ["java", "-jar", "/app/iotd-service-0.1.0.jar"]
```

Na etapa de `builder`, usa-se a imagem `diamol/maven` que contém o OpenJDK e a ferramenta Maven. Em seguida, cria-se o diretório de trabalho e copia o `pom.xml`, onde o Maven define as configurações para o projeto em Java. Na primeira instrução `RUN` executa-se o comando do Maven para baixar as dependências do projeto. A instrução `COPY` transfere os arquivos e pasta para o diretório da imagem. Na segunda instrução `RUN`, o Maven compila o projeto em Java e gera um arquivo `.jar`.

Na segunda etapa, que começa com a imagem `diamol/openjdk` contém o Java 11, mas não o Maven. Em seguida, cria-se o diretório de trabalho e copia o arquivo `jar` da etapa de `builder`. Por fim, a porta da aplicação é exposta e `ENTRYPOINT` é uma alternativa ao `CMD` que diz o que fazer quando o contêiner iniciar a partir da imagem, neste caso, executar o arquivo compilado.

É interessante notar que as ferramentas de compilação não estão na versão final, colaborando para uma imagem menor. Outro bom exemplo é o `curl`, usado para baixar arquivos da Internet, pode ser necessário usado em alguma etapa do processo, mas não é necessário na imagem final.

## Compartilhando Imagens no Docker Hub

Docker Hub é a maior plataforma para registrar imagens e o padrão do Docker Engine, onde as imagens são buscadas caso não sejam encontradas localmente.

Para identificação de uma imagem, são necessárias 4 partes no nome da imagem que servem como referência.


![identificação de uma imagem, primeiro o domínio, autor, nome da imagem, tag](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tvft6gyec67q40ukvzje.png)

O versionamento pode ser feito da seguinte forma:

```
[major].[minor].[patch]
```

Um versionamento que acrescenta um *patch* pode ser apenas resolução de bugs e ter as mesmas funções que a última versão. Quando *minor* é adicionado significa serem adicionadas novas funções, porém nenhuma é removida. Por fim, *major* pode ter funções completamente diferentes.

Para fazer login no Docker Hub,

```bash
export dockerId="<docker-id>"
docker login --username $dockerId
```

Para criar uma nova referência a imagem, neste caso na versão v1,

```bash
docker image tag <image-name> $dockerId/<image-name>:v1
```

Por fim, para enviar ao Docker,

```bash
docker image push $dockerId/<image-name>:v1
```

Logo, é possível ir ao website visualizar a imagem em que foi feita upload.

Além disso, também é possível executar um próprio Docker registry como uma forma de backup ou de economizar banda.

> :warning: **Importante ressalvar o uso de imagens verificadas ou oficiais para evitar problemas de segurança.**

## Usando Docker Volumes

Um contêiner é um sistema de arquivo único, preenchidos com os arquivos da imagem. As imagens são armazenadas como camadas, assim o disco do contêiner é apenas um sistema de arquivos virtual.

Contêineres possuem um espaço que podem ser usado para escrever ou modificar arquivos, porém esse espaço é exclusivo de cada contêiner, assim, caso o contêiner seja removido ou alterado (como em uma atualização, por exemplo) os dados serão perdidos.

Quando o contêiner tenta editar um arquivo da imagem, é gerada uma cópia que será exclusiva daquele contêiner e não interfere nos outros.

O exemplo abaixo mostra um número aleatório escrito em um arquivo por dois contêineres gerados a partir da mesma imagem.

![A camada de escrita ela é única de cada contêiner e imagens compartilhadas são read-only](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0kb39e760ibpdvs3ch75.png)

Portanto, Docker tem funcionalidades que permitem armazenar dados de forma independente do ciclo de vida dos containers, são eles: *volumes* e *mounts*.

Volumes são usados para persistirem informações. Pode-se criar um volume e associar a um container, que aparecerá como um diretório. Assim, ao salvar dados nesse diretório dentro do container, na verdade, será salvo no volume. Logo, ao serem criados novos containers e associados ao volume, todos os dados salvos estarão disponíveis.

Compartilhar o mesmo volume entre diferentes aplicações não é o ideal. Aplicações, geralmente, possuem acesso exclusivo a banco de dados. Volumes são melhores usados para preservarem atualizações nas aplicações, assim, é possível criar várias versões e associar o volume a nova versão ou a versão desejada.

Um modo de compartilhar armazenamento entre contêineres é usando *bind mounts*, que cria um diretório na máquina *host* disponível para um contêiner. Isso pode ser usado como backup ou um armazenamento distribuído acessível pela rede do computador.

Os *bind mounts* são bidirecionais, tanto o host como o contêiner podem ler ou escrever no diretório, então vale ressaltar o aspecto de segurança que contêineres, geralmente, são executados com o menor privilégio possível, sendo necessário autorização do usuário para escrever ou ler nesse diretório.

Cada contêiner possui um único disco virtual que o Docker une de várias fontes, isso é chamado *union filesystem*. Assim, o contêiner vê tudo como apensa um disco rígido e trabalha com arquivos e diretórios da mesma forma, estejam eles no disco ou não. O autor pode decidir quais serão as fontes de dados deste disco rígido, podem existir diversos volumes ou *bind mounts*, mas o contêiner verá tudo como um só.

![descrição de cada camada de um contêiner](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y3r72c85ilsvfhg0qmtc.png)

Segue, de maneira geral, como as formas de armazenamento podem ser usadas.

- **Camada de Escrita:** dados temporários, como cache ou cálculos. São únicos de cada contêiner e tem o mesmo ciclo de vida que o contêiner.
  
- **Bind Mounts locais:** compartilhar dados entre o host e o contêiner, por exemplo, código-fonte, realizando modificações locais sem precisar criar uma nova imagem.
  
- **Bind Mounts distribuídos:** podem serem usados para o compartilhamento de configurações, fontes de leitura de informações ou cache distribuído ou para a escrita de dados que podem serem usados por qualquer contêiner ou máquina na rede.
  
- **Volume:** compartilhar dados entre contêineres e armazenamento gerenciado pelo Docker. Quando uma aplicação atualiza para um novo contêiner, os dados serão salvos no volume da versão anterior.
  
- **Camadas das Imagens:** sistema de arquivos inicial do contêiner. As últimas camadas sempre sobrepõe as mais novas. Camadas são sempre de leitura e compartilhada entre contêineres.