Quem nunca ouviu a frase: “Na minha máquina funciona…”. Esse é (ou era) um problema comum aos desenvolvedores, pois cada máquina tem configurações diferentes e na hora de rodar a aplicação, muitas variáveis podiam levar ela a falhar. Dessa forma, o Docker surge como uma forma de padronizar o ambiente de desenvolvimento para, com poucos comandos, subirmos a aplicação inteira, com suas dependências e conexões.
De início pode parecer complicado, mas entendendo alguns conceitos e comandos, rapidamente conseguimos extrair o melhor que o Docker tem a oferecer e facilitar muito nossa vida.
O docker roda uma máquina linux virtualmente , então é recomendado conhecer um pouco de Linux e comandos do terminal.
Mas antes de aprender a criar ou executar os comandos, vamos entender alguns conceitos iniciais:
Construindo uma casa
Pra entender o funcionamento do Docker na nossa máquina e seu uso, eu gosto de usar a analogia de quando construímos uma casa. No caso, vamos pensar que temos um terreno que é um condomínio de casas, e lá vamos construir várias casas. As casa terão um padrão pra estrutura principal, e depois poderemos personalizar cada uma com móveis e etc.
Primeiramente precisamos pensar no tamanho da nossa casa, as paredes, portas e janelas. Onde ficarão a parte hidráulica e elétrica.
Após isso definido, passamos pro arquiteto ou engenheiro construir a planta das casas.
Com isso, podemos construir uma ou várias casa baseadas na planta da casa.
E depois podemos entrar em cada casa e colocar o que quisermos dentro. Até modificar uma porta ou janela. Enfim, trabalhar cada casa individualmente. Podemos também demolir uma casa e construir outra no mesmo local, com base na planta inicial.
Uma questão além disso são as contas (luz, água). Vamos supor que queremos sempre ter o total de quanto estamos gastando no condomínio. Se demolirmos uma casa, não podemos perder esse valor. Então precisamos ter um local para armazenar as contas fora de cada casa, além de ter uma cópia na própria casa.
Agora que temos uma ideia dos passos hipotéticos pra construir nosso condomínio de casas, vamos relacioná-los a subir uma aplicação com Docker.
Primeiro, definimos os passos pra construir uma image, ou seja, a planta da casa. Esses passos ficam salvos num Dockerfile, um arquivo onde fica definido o que vamos precisar subir na(s) máquina(s) que vamos subir.
Desse arquivo, buildamos a image, a planta da nossa casa. Uma image é basicamente um template pra criação de containers.
Já os containers são nossas casas. Eles são instâncias de uma máquina virtual rodando na nossa máquina, com as configurações especificadas na nossa image. Podemos subir um container e acessá-lo através do terminal. Lá, podemos modificá-lo como quisermos, instalando novas aplicações ou bibliotecas, executar aplicações, criar banco de dados, etc.
Por fim, podemos remover um container. Isso irá apagar qualquer arquivo ou instalação que tivermos feito dentro dele. Ao subirmos um novo container, teremos as especificações iniciais definidas na nossa image.
E se quisermos manter dados do nosso container pro próximo? Podemos utilizar os volumes, para isso. Como a contas da casa, precisamos salvar as informações fora do container para persistí-los. Dessa forma, fazemos uma “ponte” entre nossa máquina e o container, copiando tudo de um pro outro. Ao removermos os containers, mantemos os dados na nossa máquina e podemos usá-los novamente.
Fluxo de trabalho
Depois de entender esses conceitos, fica mais fácil entender um fluxo de trabalho utilizando Docker. Os comandos abaixo estão melhor descritos no fim desse post.
Primeiramente, precisamos de uma image pra trabalhar. Podemos criar um Dockerfile para buildar (docker build) uma ou baixar uma do Docker Hub (docker pull).
Após termos a image (docker images), podemos executá-la (docker run) e gerar um container, uma máquina virtual com as configurações definidas na image.
Container pode executar uma vez e parar, ou ficar rodando na nossa máquina (docker ps). Podemos acessar o container (docker exec) e rodar comandos dentro dele (lembrando que qualquer alteração será perdida quando matarmos o container).
Após utilizar, podemos matar o container (docker stop) (docker rm). E caso desejamos, também remover a image (docker rmi), se não formos mais utilizá-la para criar novos containers.
Criando um Dockerfile
Ao criamos um arquivo, como o exemplo acima, podemos buildar o Dockerfile numa image, rodando docker build -t <NOME-DA-IMAGEM>:<VERSÃO> <DIRETÓRIO-DO-DOCKERFILE>. Isso cria a image com os comandos e instalações especificadas no Dockerfile já executados.
A partir daí, podemos definir os passos para configurar a image com as configurações iniciais. Sempre usamos uma image como base com o comando FROM. O RUN, por exemplo, executa os comandos.
Principais instruções do Dockerfile
FROM: image que vamos utilizar de base.
WORKDIR: Define a pasta que vamos executar os comandos no container.
RUN: Roda os comandos especificados.
COPY: Copia arquivos da nossa máquina pro image.
ENTRYPOINT: Comando a ser executado quando inicializamos o container.
Algumas outras podem ser conferidas aqui.
Principais comandos Docker que utilizo
Abaixo, separei algumas das principais funcionalidades que utilizo no dia-a-dia.
Utilizando Imagens
docker pull <NOME-DA-IMAGEM>:<VERSÃO>: Puxa a image pra nossa máquina local. Se a versão não for passada, se utiliza a latest.
docker build <CAMINHO-DO-DOCKERFILE>: Buildamos em uma image o Dockerfile
-t <NOME-DA-IMAGEM>:<VERSÃO>: Criamos um nome da image. É interessante utilizar o nome do nosso usuário no Docker Hub seguido de / e um nome da image: user/project-abc:v1
docker images: Lista as imagens baixadas na máquina.
docker rmi <IMAGE-ID>: Remove a image.
Listagem e remoção de containers.
docker ps: Lista os containers que estão rodando.
-a: Mostra os containers ativos ou que já foram executados.
-q: Lista os ids dos containers.
docker stop <ID-DO-CONTAINER>: Para o container que esteja rodando.
docker rm <ID-DO-CONTAINER>: Remove o container que está parado.
-f: Força a remoção, mesmo se o container estiver rodando.
Para remover todos os containers ativos e não ativos:
docker rm -f $(docker ps -aq)
Manipulando containers
docker run <IMAGE> <COMANDO>: Roda a image em nosso terminal com o comando informado.
-i: Modulo interativo, mantendo o processo rodando.
-t: Permite digitar coisas no terminal.
-p: Aponta para porta a ser mapeada.
-d: Desatacha o terminal do processo.
-v: Volume para persistência de dados. Passamos o caminho local, com ‘:’ separando o caminho no container. As mudanças na pasta do container persistirão na máquina local.
–rm: Remove o container após terminar de executá-lo.
–name: Define um nome para o container.
Exemplo: docker run -it ubuntu bash rodamos uma image do Ubuntu e executamos o bash, permitindo-nos executar códigos no terminal dentro do container.
Exemplo: docker run -d -p 8080:80 nginx rodamos o nginx e mapeamos a porta 8080 da nossa máquina pra porta 80 do container.
docker exec <NOME-DO-CONTAINER> <COMANDO>: Executa um comando no nosso container já ativo. (Podemos usar as mesmas flags do docker run, como por exemplo, a -it)
Trabalhando com volumes
docker volume ls: Lista volumes.
docker volume create <NOME-DO-VOLUME>: Cria um volume.
docker volume inspect <NOME-DO-VOLUME>: Mostra dados do volume.
docker volume prune: Remove os volumes e libera espaço.
Com isso, podemos criar um volume passando a flag –mount com o nome do volume criado. Exemplo:
docker run -d -p 8080:80 –mount type=volume,source=<NOME-DO-VOLUME>,target=/app nginx
Ou com a flag -v:
docker run -d -p 8080:80 -v <NOME-DO-VOLUME>:/app nginx
Considerações
Tentei resumir bem os conceitos e principais comandos que uso do Docker. Vale a pena se aprofundar e praticar subindo imagens para uso pessoal. Outro ponto é estudar sobre Docker Compose, que orquestra múltiplos containers Docker para funcionarem juntos.
Qualquer dúvida ou comentário, escreve aqui em baixo que ficarei feliz em responder.