Docker é uma ferramenta que simplifica significativamente o processo de desenvolvimento de aplicações, tanto para indivíduos quanto para equipes colaborativas. Por meio do uso de containers, o Docker permite a execução de aplicações em ambientes computacionais isolados. Imagine construir uma aplicação que envolva tecnologias como React e MongoDB. Com o Docker, é possível executar esses componentes em ambientes isolados, facilitando sua comunicação e interação.
No contexto do desenvolvimento em equipe, o Docker oferece uma solução eficaz para enfrentar problemas de versionamento. Em vez de exigir que todos os membros da equipe alinhem manualmente os recursos de desenvolvimento necessários para garantir o correto funcionamento da aplicação – incluindo versões, dependências, configurações e variáveis de ambiente –, podemos optar por utilizar containers por meio do Docker.
Os containers podem ser compreendidos de forma simplificada como pacotes que contêm todas as necessidades da nossa aplicação, já previamente configuradas, para uma execução correta. Esses containers podem ser isoladamente executados, independentes de quaisquer outros processos no nosso sistema. Dessa maneira, para que qualquer membro da equipe obtenha resultados consistentes, é suficiente possuir o Docker para gerenciar esses containers. Portanto, o primeiro passo é instalar esse software.
Para construir um container no Docker, é essencial criar sua imagem. Uma analogia útil é comparar containers a casas totalmente construídas, enquanto as imagens seriam as plantas arquitetônicas usadas para ter como base.
Dentro das imagens, definimos elementos como o ambiente de execução, os códigos essenciais para a construção, as dependências que precisam ser instaladas, configurações adicionais (como variáveis de ambiente) e comandos a serem executados após a criação. Além disso, as imagens possuem arquivos de sistema próprio, independente do computador do usuário.
Containers representam instâncias executáveis das imagens. Cronologicamente, começamos criando imagens, que atuam como 'plantas arquitetônicas', e, posteriormente, executamos essas imagens para gerar nossos containers. Dessa forma, podemos entender os containers como processos que executam nossas aplicações a partir das imagens que criamos. Esses processos operam de forma independente entre si e em relação a outros processos no computador do usuário. No entanto, temos a capacidade de estabelecer comunicação entre esses processos.
Resumindo o processo:
As imagens são construídas em várias camadas. A camada mais fundamental é a que denominamos Parent Image (Camada Base), que engloba o sistema operacional e certos elementos em tempo de execução no ambiente do sistema. Essa camada constitui o ponto de partida, enquanto as camadas subsequentes representam acréscimos progressivos: desde códigos-fonte até dependências.
As imagens base podem ser acessadas e personalizadas por meio do Docker Hub. O Docker Hub é, essencialmente, um repositório online de imagens Docker, que oferece uma variedade de imagens base pré-configuradas. Essas imagens podem ser usadas como a primeira camada nas nossas próprias imagens.
Dockerfile
Para construir nossas próprias imagens com camadas personalizadas, utilizamos um arquivo denominado Dockerfile. Esse arquivo contém instruções que guiam o Docker na criação da imagem, abrangendo todas as distintas camadas ou diretrizes necessárias para construr as layers.
Aqui está um modelo de arquivo com comentários para guiar sua estruturação básica. Cada novo comando executado no arquivo representa a criação de uma nova camada (Layer):
# Escolhe a Base Image, nesse caso do Node, especificando a versão
FROM node:16-alpine
# Após de construir a Base Layer, cira um novo diretório /app e entra nele, todos os comandos a partir daqui serão executando dentro desse diretório
WORKDIR /app
# Copia arquivos para a imagem. O primeiro ponto sinaliza que é da pasta corrente onde foi
criado o Dockerfile. O segundo ponto sinaliza que é a pasta corrente da imagem, nesse caso
a /app
COPY . .
# Agora podemos dizer alguns comandos para rodar na construção da imagem, nesse caso
a instalação das dependências
RUN npm install
# Qual porta o Docker utilizara para o container, nesse caso a 4000
EXPOSE 4000
# Com o comando CMD dizemos para executar um comando toda vez que o container for
executado, após a construção
CMD [“node”, “app.js”]
Após a definição das camadas no arquivo Dockerfile, podemos proceder à construção da imagem de acordo com as especificações. Para realizar esse processo, basta executar um único comando Docker no diretório onde o arquivo foi criado.
$ docker build -t name_image .
# O comando “docker build” é utilizado para especificar que será construída uma imagem,
com -t “name_image” definimos o nome dessa nova imagem, por fim, o “.” sinaliza que o
Dockerfile está na pasta corrente da execução do comando.
No processo de criação das nossas imagens, podemos usar tags. As tags podem ser consideradas como 'versões' das nossas imagens. De maneira semelhante à forma como especificamos a versão do Node em nosso Dockerfile, podemos criar tags para nossas imagens de maneira simples.
$ docker build -t name_image:v1 .
# Os dois pontos após o nome criam uma tag para essa imagem. Todas as vezes que fomos
referenciar essa imagem agora, deveremos especificar a versão.
Dockerignore
Em algumas situações, pode ser desejável evitar a cópia de todos os arquivos presentes no diretório de aplicação para a imagem. Por exemplo, se o projeto já possui uma pasta com os módulos do Node, não é necessário incluí-la na imagem, uma vez que um dos comandos executados durante a construção da imagem é o 'npm install', responsável por adquirir essas pastas. Se incluirmos os 'node_modules' na imagem, ocuparemos espaço desnecessário e retardaremos o processo de construção. Para lidar com esse cenário, a criação de um arquivo '.dockerignore' se torna vantajoso, permitindo definir o que será ignorado durante a construção da imagem. Depois de criar o arquivo '.dockerignore', basta listar nele as pastas, arquivos, etc., que devem ser ignorados na cópia dos arquivos para a imagem.
Deletando uma Imagem
Ao deletar uma imagem, é aconselhável garantir que ela não esteja sendo usada. Portanto, antes de prosseguir com a exclusão, é necessário remover todos os containers que foram criados com base nessa imagem. Se nenhum container estiver vinculado à imagem em questão, a exclusão padrão pode ser realizada sem problemas
# Deletando uma imagem
$ docker image rm image_name
No entanto, podemos forçar a exclusão de uma imagem mesmo que ela esteja em uso, para isso basta adicionar mais um argumento ao comando.
# Forçando a exclusão de uma imagem
$ docker image rm image_name -f
Após a configuração de todas as camadas da imagem, você pode executar os passos para construir os containers. Isso pode ser feito através da execução de alguns comandos no terminal do seu computador, conforme descrito a seguir.
# Verificando as imagens geradas
$ docker images
# Rodando a imagem
$ docker run --name new_container_name -p 4000:4000 -d image_name
# O comando “docker run” constrói o container, passamos um nome para ele por meio de “-
--name”, uma porta (a porta 4000 do container será acessada pela porta 4000 do computado
local), “-d” para não bloquear o terminal e, por fim, a imagem que será rodada.
O comando para executar a construção do container oferece diversas opções adicionais para aprimorar o processo. Para conhecer todas essas opções, é recomendável consultar a documentação oficial do Docker. Uma dessas alternativas é o "--rm", que exclui o conteiner automaticamente após sua parada.
# Rodando a imagem
$ docker run --name new_container_name -p 4000:4000 --rm image_name
# “--rm” faz com que o container seja removido logo após ser parado.
Iniciando um Container
Containers criados com “docker run” podem ser iniciados todas as vezes que desejar.
# Verificando todos os containers criados
$ docker ps -a
# Iniciando o container
$ docker start -i container_name
Parando um Container
Containers também podem ter sua execução interrompida quando for desejado.
# Verificando os containers que estão rodando
$ docker ps
# Parando o container
$ docker stop container_name
Deletando um Container
# Deletando um container
$ docker container rm container_name
Se houver o desejo, existe uma maneira simples de realizar uma limpeza completa, deletando containers, imagens, volumes e tudo o que foi criado com o Docker.
# Deletando tudo
$ docker system prune -a
Docker Images são de natureza read-only, o que significa que, uma vez construídas, quaisquer atualizações feitas em nosso diretório local não serão automaticamente refletidas na imagem. Para propagar as alterações é necessário criar uma nova imagem.
Felizmente, o Docker disponibiliza uma maneira de mapear arquivos locais para dentro da imagem. Dessa forma, quando fazemos modificações localmente, não precisamos criar uma nova imagem; podemos simplesmente propagar essas alterações utilizando o Docker Volume.
Para trabalhar com volumes, basta adicionar uma nova opção ao comando 'docker run' durante a criação do container.
$ docker run --name new_container_name -p 4000:4000 -v diretório_local_do_projeto:pasta_no_container -v
diretório_não_mapeado_no_container image_name
No comando mencionado acima, depois do primeiro uso do parâmetro -v, definimos qual diretório no seu computador será mapeado para um diretório correspondente no container. Com isso, qualquer alteração feita entre esses dois diretórios será refletida. O segundo uso do parâmetro -v é destinado a especificar diretórios no container que permanecerão inalterados, mesmo que haja diferenças nos diretórios locais. Um exemplo desse cenário é a pasta 'node_modules', que é frequentemente ignorada no arquivo '.dockerignore'. Abaixo segue um exemplo do comando:
$ docker run --name my_app -p 4000:4000 -v C:\Users\victor\Documents\api:/app -v
/app/node_modules my_image
Em algumas situações, pode ser que tenhamos vários projetos e desejemos executar todos esses containers de uma vez. Por exemplo, podemos querer executar nossa aplicação e o banco de dados simultaneamente, cada um em seu próprio container, e com comunicação entre eles. Para gerenciar de forma mais eficiente esses containers, o Docker Compose oferece uma solução conveniente. Com ele, podemos configurar todos os containers do nosso projeto em um único arquivo. Isso se mostra extremamente poderoso, uma vez que esse único arquivo é capaz de configurar todos os containers necessários para um projeto específico, incluindo o banco de dados, o backend, o frontend e outros.
Esse arquivo, chamado 'docker-compose.yaml', deve ser criado na raiz (root) do projeto. A estrutura básica de um arquivo Docker Compose é ilustrada a seguir:
Nesse arquivo, começamos especificando a versão do Docker Compose. Os 'services' são essencialmente os projetos de imagens e containers que serão criados. Começamos definindo a imagem no campo 'build', seguido pelo nome do container em 'container_name' e a porta que será utilizada pelo container e compartilhada com o computador, em 'ports'. Adicionalmente, configuramos propriedades de volumes, com a observação de que, dessa vez, não é necessário especificar o diretório completo.
No último serviço, o uso do Docker Volume foi removido. Isso ocorre porque, no ambiente Windows, trabalhar com dois Docker Volumes pode causar conflitos e não funcionar conforme o esperado. As duas últimas propriedades foram empregadas para garantir um funcionamento adequado dos containers, ativando o modo interativo, que é necessário em algumas aplicações.
Executando e Parando o Docker Compose
# No diretório root do projeto executar para iniciar
$ docker-compose up
# Parando o Docker Compose
$ docker-compose down