Como utilizar o Docker

Imagens, volumes, networks, Dockerfile, Docker Compose, Comandos gerais

SHARE:

Olá! (◠‿◠✿)! Vamos conhecer mais a fundo sobre o Docker? Neste post de Introdução ao Docker tem alguns conceitinhos bacanas que recomendo a leitura caso ainda não tenha visto.

O primeiro Hello World com o Docker

Após você ter instalado direitinho, abra seu terminal e digite o seguinte:

docker run hello-world

Este comando significa que eu estou falando para o Docker Enginee que eu quero criar um container com a imagem hello-world.

O docker vai tentar encontrar uma imagem localmente, quando ele não encontra nada ele vai e baixa do Docker Hub (lá tem várias imagens que podemos utilizar em nossos projetos). Depois que ele baixa, vai ser exibido uma mensagenzinha no terminal de Hello World! (junto com os passos que o docker executou) ^_^

Captura de tela de 2020 07 14 20 51 26

Imagens e containers

Quando você executa o comando acima, o docker vai primeiro verificar se você tem essa imagem localmente que você está tentando rodar, se não tiver ele vai baixar lá do Docker Hub, e essa imagem nada mais é que uma serie de instruções do que você tem que fazer para criar um container, a imagem é como se fosse uma receita que o docker utiliza pra criar o container, no qual vai conter as instruções do Hello World que foi nosso caso. Em seguida, o container é executado!

frame 6 2

Layered Filesystem

Toda imagem que você baixa é composta por mais de uma camada, principalmente se for uma imagem mais complexa, isso pode trazer benefícios, pois uma imagem é composta por várias camadas que podem ser reaproveitadas em outras imagens. Por exemplo, eu baixei a imagem do ubuntu e agora quero do CentOS, se eu quero baixar uma imagem do CentOS e compartilhar uma camada que já tem na imagem no ubuntu, o docker vai ser esperto e só vai baixar as camadas referentes ao CentOS, ele não vai baixar as camadas que já tenho.

As camadas bases de uma imagem são read only, ou seja, bloqueadas para escrita. Quando criamos um container, não estamos necessariamente escrevendo nas imagens, o container cria uma segunda layer principal, e essa layer é read/write e é nela que eu consigo escrever.

frame 7

Uma vantagem disso é que, como tenho uma imagem base, eu posso reutilizar em vários containers, fazendo com que me traga economia de espaço, aproveitando só a camada read only e inserindo para cada container uma camada de read/write.

Volumes

Quando eu removo um container, a camada de dados dele também vai embora! Imagina que eu queira criar um container pra colocar meu banco de dados, os meus logs, etc... toda vez que esse container for removido ele vai levar junto todos os meus dados. Não é isso que quero! Quero fazer com que haja a persistência de dados.

Esse lugar onde salvamos os dados no container são chamados de Volumes.

frame 8

Se eu escrever apenas no container, assim que ele for removido os dados somem. Mas eu posso criar um local especial nesse container, chamado de volume que especifica a pasta que será meu volume de dados. O que eu estou fazendo, na verdade, é apontando essa pasta para uma pequena pasta do Docker Host (o docker host é o que está hosteando nossa maquina). Então, quando eu crio um volume, o que eu estou fazendo é criando uma pastinha dentro do meu container dizendo "olha, o que eu escrever nessa pasta do container na verdade eu to escrevendo em uma pasta do meu docker host".

Digite o seguinte comando no terminal:

docker run -v "/var/www" ubuntu

A flag -v possibilita que eu crie o meu volume no diretório /var/www do container, e quando eu criar o container baseado na imagem do ubuntu, eu posso dar em seguida uma inspecionada pra ver se ele conseguiu criar o meu volume.

O comando docker inspect ID_CONTAINER exibe na aba de Mounts, na label Destination, o caminho /var/www do meu container, no qual o que eu escrever neste caminho vai ser escrito localmente no meu computador lá no caminho especificado em Source. É neste endereço em Source (/var/lib/docker/volumes.../) que o docker automaticamente gerou pra salvar os dados de /var/www. Portanto, quando eu remover o container, essa pasta de source ainda existe, e o que eu escrevi no /var/www vai continuar no meu computador.

Captura de tela de 2020 07 25 22 22 52

Captura de tela de 2020 07 26 09 43 26

Eu posso configurar esse endereço Source para outro diretório com o seguinte comando:

docker run -it -v "/home/mandy:/var/www" ubuntu

O sinal de : (dois pontos) é pra separar o que é o do meu computador para o que é do meu container.

Neste caso, eu rodei o comando acima e utilizei a flag -it para que já abra o terminal integrado da imagem do ubuntu. Veja o teste:

Captura de tela de 2020 07 25 22 48 21

Captura de tela de 2020 07 25 22 49 52

Observe acima que, depois que eu rodei o comando e já entrei diretamente no terminal neste meu container do ubuntu, eu fui até a pasta /var/www e criei uma pasta chamada meus_textos, em seguida criei o arquivo textinho-docker.txt e escrevi um Hello World nele.

Como eu mudei o caminho da pasta Source para que os dados fiquem salvos em /home/mandy, ao acessar este caminho no meu computador, já foi possível encontrar a pasta meus_textos criada ali também! :)

Rodando código em um container

Assim como tudo que escrevo no /var/www vai parar dentro do /home/mandy, tudo que eu escrever no meu /home/mandy também vai ser acessível no meu /var/www. Imagina que eu tenho um código fonte escrito em node mas não tenho o node na minha maquina, então eu posso criar um container e colocar meu código pra rodar dentro do container, e se meu container tiver por exemplo o node, eu não preciso ter ele instalado na minha maquina, o meu ambiente de desenvolvimento pode ser dentro do container.

O código utilizado neste exemplo será um simples servidorzinho em nodejs que vai renderizar uma página html: https://repl.it/@allonsmandy/docker-code-volume

Como faço para criar um container que vai rodar esse código node e pegar o código no desktop e rodar dentro do container? Vamos utilizar os volumes!

docker run -d -p 8080:3000 -v "/home/mandy/docker-code-volume:/var/www" -w "/var/www" node npm start

No comando acima, estou dizendo que quero executar uma imagem do node, visto que preciso de um container com uma imagem baseada no node, e ai eu quero criar um volume que vai pegar minha pasta com os arquivos do meu código em /home/mandy/docker-code-volume/ e linkar com a pasta /var/www lá do container.

Pra iniciar eu tenho que dar um npm start, certo? Eu poderia usar o terminal iterativo mas como é algo curtinho posso executar logo diretamente.

Também tenho que disponibilizar a porta 3000 para meu container no lado de fora, utilizando a flag -p em que especifica que a porta 8080 do meu computador será a 3000 do meu container.

Só com essas informações ele não vai conseguir encontrar o package.json pra iniciar, porque quando o container inicia ele da o "start" numa pasta aleatória e não na /var/www, por isso tenho que especificar que pasta vou abrir esse meu container, por isso coloquei a flag -w que especifica qual diretório que vai executar todo esse comando, no caso é onde vai ta meu package.json, que é lá na pasta /docker-code-volume/, só que dentro do meu container essa pasta é a /var/www, lembra? Por fim, o -d só pra não travar meu terminal e eu continuar utilizando :)

Captura de tela de 2020 07 26 07 05 53

Eu posso criar um ambiente de desenvolvimento pra mim todo baseado em container. O container pode ter tudo que preciso, as ferramentas, bibliotecas, e posso até falar pra todo mundo da minha equipe utilizar aquele container que, desta forma, todo mundo vai ter o mesmo ambiente de desenvolvimento, mesma versão do node, php, ferramentas, bibliotecas... :)

Dockerfile

Vamos aprender agora a construir nossas próprias imagens. Se você der um docker images no terminal, verá todas as imagens que você já tem baixadas, no meu caso já tenho do ubuntu, hello-world, node e várias outras.

Se lembra que a imagem é como se fosse uma receita de bolo? Vamos construir esta nossa receita que vai ensinar ao docker a criar a imagem a partir da aplicação pra poder distribuir para que ela seja usada em outros locais, por exemplo.

Para isso, precisamos criar um arquivo chamado Dockerfile. O código dele ficará assim:

FROM node:latest 
LABEL maintainer="Mandy Matos"
COPY . /var/www 
RUN npm install 
ENV PORT=3000
EXPOSE $PORT
ENTRYPOINT ["npm", "start"] 
WORKDIR /var/www 

A gente começa a montar utilizando o comando FROM. Odockerfile vai montar a imagem a partir de uma imagem base já existente, você pode criar do zero mas é muito mais comum você aproveitar uma imagem base e adicionar suas modificações. Levando em conta a aplicaçãozinha deste exemplo, a gente precisa do node pra executar o código, certo? Então posso falar que minha imagem vai se basear no node e posso especificar a versão também :) (se não colocar nada ele vai entender que é a ultima versão)

Outra coisa que utilizamos é o LABEL, no qual ele vai possuir o rotulo de maintainer que é quem cuida e quem criou a imagem.

Agora eu vou especificar o que quero que tenha na imagem! Nos comandos anteriores eu colocava o código do /docker-code-volume/ na pasta /var/www, e é isso que farei agora. Podemos mover o código pra dentro da pasta utilizando o COPY, ele vai copiar o que você indicar pra dentro da imagem, então quando você distribuir a imagem pra outra pessoa ele já vai ter esse código fonte embutido. Neste caso, eu quero que os arquivos do diretório que eu estou (home/mandy/docker-code-volume/) sejam copiados para /var/www.

Quando estamos trabalhando com node nós temos que instalar as dependências do projeto, né? Elas ficam na node_modules, mas não quero enfiar essa pasta no container, eu quero que a própria imagem instale as dependências, então assim que a imagem estiver sendo construída eu quero que ele dê um npm install.Eu consigo fazer isso utilizando o RUN. (você pode referenciar essa pasta no arquivo .dockerignore)

Podemos dizer também que o container vai utilizar a porta 3000 utilizando o EXPOSE, note que neste caso foi utilizado as variáveis de ambiente através do ENV.

Quando o container inicia ele sempre executa um comando. O do node, por exemplo, a gente utiliza o npm start, então podemos adicionar essa informação no ENTRYPOINT que é o comando que vai ser executado assim que carregar o container.

Eu quero que, quando ele der um npm install na pasta onde estar meus arquivos, que no caso é em /var/www, o meu WORKDIR será na pasta /var/www, dai assim que meu container carregar, o comando npm start vai ser executado nesta pasta.

Vamos buildar a imagem?

docker build -f Dockerfile -t allonsmandy/node .

-f: Diz o nome do arquivo do meu Dockerfile, se você deixa por padrão não precisa especificar, mas se colocar por exemplo node.dockerfile, precisa passar essa flag

-t: Como você quer taguear sua imagem, ou seja, seu-nome/nome-da-imagem

E por fim, aonde está o contexto que está o meu arquivo dockerfile, como eu já estou na pasta só utilizei o . (ponto)

Captura de tela de 2020 07 26 07 26 22

Se estiver tudo ok, você poderá visualizar a imagem com o comando docker images.

Captura de tela de 2020 07 26 07 28 16

Agora é só criar um container a partir desta imagem. Bora?

docker run -d -p 8080:3000 allonsmandy/node

Como a minha porta 8080 já estava sendo usada por outro container, eu tive que matar ele e subir o outro com a minha imagem!

Captura de tela de 2020 07 26 07 31 41

Você pode colocar essa sua imagem no Docker Hub para que outras pessoas possam baixar ela e construir containers a partir dela.

Network

Normalmente, uma aplicação é composta por diversas partes, então é bem costumeiro separar cada pedaço em um container, como por exemplo o banco em um container, a aplicação em outra, etc. Assim, cada container fica com uma responsabilidade só. Se eu tenho 1 pedaço da minha aplicação em cada container, como eu posso fazer pra esses pedaços falarem entre si? Visto que eles tem que conseguir trocar dados pra que consiga funcionar como um todo?

Por padrão existe no docker uma default network que, quando você cria seus containers, todos já estão funcionando na mesma rede.

docker run -it ubuntu

Se você rodar o ubuntu e em seguida dar um docker ps em outro terminal, nós podemos pegar o id do container e inspecionar ele utilizando o docker inspect ID_CONTAINER

Lá vai ter a aba de Network Settings que, por padrão, está em uma rede chamada bridge, e todo container já está por padrão na rede default.

Captura de tela de 2020 07 26 07 36 42

Captura de tela de 2020 07 26 07 37 23

No terminal dentro do meu container do ubuntu eu posso adicionar o comando hostname -i e ver o ip da rede.

Captura de tela de 2020 07 26 07 38 16

Vamos fazer uns testes ^.^!! Vou criar dois containers da imagem do ubuntu e já irei abrir no modo iterativo (docker run -it ubuntu), o primeiro container e o segundo container.

No primeiro container eu posso dar um ping no segundo container, porém vai aparecer uma mensagem informando que o ping não é um comando encontrado no ubuntu, isso porque a imagem do docker só tem o essencial para o ubuntu funcionar, então nesse caso o ping não vem instalado, mas é super simples resolver isto! Só digitar o comandinho abaixo no ubuntu que desejar utilizar o comando ping :)

apt-get update && apt-get install iputils-ping

Agora posso pingar o outro container. Veja que agora ta dando pra fazer a comunicação! :)

Captura de tela de 2020 07 26 07 47 22

Porém, cada hora que subir o container novo, ele vai receber um novo ip determinado pelo docker. Isto não é muito útil em alguns casos pois dependendo do que eu for fazer eu posso precisar referenciar direitinho. Pra saber exatamente eu tenho que configurar o nome do meu container.

docker run -it --name meu-primeiro-container ubuntu

Só que dentro do outro container sem nome eu não posso dar um ping meu-primeiro-container porque a rede default do docker não permite que você atribua um hostname a um determinado container, ou seja, na rede default eu só posso me comunicar utilizando IP, se eu criar minha própria rede eu vou poder batizar o meu container de banco de dados, aplicaçao, etc, e com esses nomes eu posso fazer essa conexão, fazendo com que tenha uma camada de abstração acima do ip, assim eu não preciso dizer pra que ip especificamente conectar.

Porém isso não pode ser feito na rede default do docker, só quando crio a minha própria rede.

docker network create --driver bridge minha-rede

Quando eu to criando uma network, eu tenho que dizer qual driver eu quero utilizar, o padrão desse que estamos falando aqui é o de bridge, ele é o mais comum, mas você pode utilizar outros também, geralmente o de bridge é o que utilizamos quando queremos fazer com que um container fale com outro. Depois que você especificar o driver é só dar um nome pra sua rede.

Para listar todos os nomes da sua rede você pode utilizar o seguinte comando:

docker network ls

Quando eu to criando um container, em vez de deixar ele ser associado por padrão na rede default do docker, ele pode ser atrelado a uma rede que eu especificar.

docker run -it --name primeiro-container --network minha-rede ubuntu

Se você der um docker inspect nele, vai ver que na aba de network irá conter que ele está atrelado a minha-rede. Se eu criar um segundo-container e atrelar ele na minha-rede, eu posso dar um ping segundo-container no terminal lá do meu primeiro-container

Captura de tela de 2020 07 26 08 02 21

Captura de tela de 2020 07 26 08 02 42

Docker Compose

Como eu posso subir múltiplos containers? Se a sua aplicação começar a crescer, você vai acabar tendo que trabalhar com diversos comandos, portanto, precisaria subir os containers manualmente e digitar vários comandos. Desvantagens desta abordagem:

  • Muitas flags para se lembrar
  • É muito manual
  • Fácil de errar
  • Tenho que garantir a ordem, por exemplo, eu tenho que subir o meu container do banco antes da aplicação

O Docker Compose vai orquestrar quando quero subir múltiplos containers. Ele funciona seguindo um arquivo de texto do tipo YML, em que vamos descrever tudo que deve acontecer pra subir a aplicação, assim eu não preciso executar muitos comandos no terminal. Veja um exemplo de arquivo do docker compose para analisarmos e estudarmos juntos.

version: '3'
services:
  nginx:
    build:
      dockerfile: ./docker/nginx.dockerfile
      context: .
    image: allonsmandy/nginx
    container_name: nginx
    ports:
      - "1234:80"
    networks:
      - minha-rede
    depends_on: 
      - "node"
       
  mongo-db:
    image: mongo
    networks: 
      - minha-rede
        
  node:
    build:
      dockerfile: ./docker/books.dockerfile
      context: .
    image: allonsmandy/mybooks
    container_name: books
    ports: 
      - "3000"
    networks:
      - minha-rede
    depends_on: 
      - "mongodb"

# cria minha rede
networks:
  minha-rede
    driver: bridge


A primeira coisa a se colocar no arquivo do docker compose é a versao dele, neste caso é a versão 3.

Em services é onde você vai colocar um serviço, que é uma das diferentes partes da nossa aplicação, é como se o node fosse um serviço, o banco outro serviço, etc... O serviço é cada parte da nossa aplicação destrinchada, se eu quero construir 5 containers, vão ser 5 serviços, cada um com nomes específicos.

Neste caso, ele vai construir o primeiro serviço que se chama nginx que vai ser criado a partir de um Dockerfile, no qual irá procurar o arquivo na pasta especificada no comando dockerfile do build, e a partir desse contexto da pasta principal eu chamo o nome lá da imagem.

Quando ele criar um container a partir dessa imagem, eu quero que dê um nome de nginx. La no dockerfile do nginx ele trabalha com a porta 80 e 443, então eu falo pra ele exibir a porta 80 (só irei utilizar ela mas poderia colocar mais) e posso mapear da mesma forma que fazia antigamente. A porta 80 do meu container vai ser mapeada pra porta 1234 do meu host.

Eu também tenho que falar qual a network que ele vai fazer parte, pois os containers precisam estar na mesma network.

Eu crio a network fora da camada de services, o nome dela vai ser minha-rede, e toda network tem que utilizar o driver que neste caso eu coloquei o mesmo visto anteriormente que é o bridge.

Quando eu criar o container do ngnix eu já atribuo a network minha-rede a ele.

Em seguida criei o serviço de mongo, o mongo não vai ser buildado a partir do dockerfile, ele vai ser buildado pela imagem padrão do mongo.

Lembra que eu tenho que subir em uma ordem certinha? Eu posso botar que eles dependem que um serviço suba antes deles utilizando o depends_on.

Se eu quero construir esses containers eu tenho que garantir que tenho todas as imagens do docker compose, pra isso eu posso digitar no terminal docker-compose build, e então ele vai procurar e executar o yml.

Executando docker-compose up ele vai levantar todos os serviços que especifiquei, vai criar todo o passo a passo descrito :)

Se eu rodasse agora localhost:1234 já exibiria direitinho a aplicação exemplificada. E se eu rodasse no terminal docker-compose ps eu poderia visualizar os meus containers rodando direitinho!

Comandos e anotações simplificadas

docker version -> Exibe a versão do docker que está instalada
docker ps -> Exibe os containers em atividade
docker ps -a -> Exibe todos os containers criados, incluindo os que estiverem parados.
docker run ubuntu -> Executa um container utilizando a imagem do ubuntu
docker run -it ubuntu -> Executa a imagem do ubuntu e em seguida já integra o terminal dele
docker start ID_CONTAINER -> Ativa meu container
docker stop ID_CONTAINER -> Para meu container
docker start -a -i ID_CONTAINER -> Ativa meu container e liga o terminal com ele, possibilitando interação entre ambos
docker rm ID_CONTAINER -> Remove o container
docker container prune -> Remove todos os containers que estão parados
docker images -> Exibe todas as minhas imagens
docker rmi NOME_DA_IMAGEM -> Remove alguma imagem
docker run -d -P --name meu-site dockersamples/static-site -> Atrela um nome ao container
docker run -d -p 12345:80 dockersamples-static-site -> Define uma porta específica para ser atribuída à porta 80 do container, neste caso 12345.
docker stop -t 0 ID_CONTAINER -> Passa o tempo de espera pra parar esse container, neste caso é 0
docker run -d -P dockersamples/static-site -> O docker atribui uma porta para que minha maquina possa acessar
docker port ID_CONTAINER -> Exibe a porta do meu container
docker build -f Dockerfile -> Cria uma imagem a partir de um Dockerfile
docker build -f Dockerfile -t NOME_USUARIO/NOME_IMAGEM -> Contrói e nomeia uma imagem não-oficial
docker run -d -p 1235:00 dockersamples/static-site -> Mapeia a porta 12345 do meu computador pra ser a porta 80 do container que vou criar
docker run -d -P -e AUTHOR="Amanda" dockersamples/static-site -> Seta uma variável de ambiente no meu container
docker stop -t 0 $(docker ps -q) -: Executa esse docker ps, "armazena" os resultados ele e da um docker stop no que ele retornar, ou seja, vai parar ao mesmo tempo os containers que estão rodando

Captura de tela de 2020 07 26 08 51 17

docker run -v "C:\Users\Mandy\Desktop:var/www" ubuntu -> Cria o container em que os dados salvos em var/www sejam salvos no meu diretorio local do Desktop
docker inspect ID_CONTAINER -> Informações do container

Resumo sobre Dockerfile

FROM
  dockerfile vai montar sua imagem a partir de outra existente (onde se baseia ela?)
LABEL
  Quem cuida da imagem, ver se ela ta sendo atualizada
COPY
  Copia o que voce indicar pra dentro da imagem, entao a imagem ja terá o codigo fonte. Se você distribuir pra outra pessoa ela ja vai ter aquele codigo fonte imbutido na imagem
RUN
  Executa o comando quando a imagem estiver sendo construida
EXPOSE
  Porta
ENTRYPOINT
  Carregado assim que for executado o seu container
ENV
  Setar variavel de ambiente

WORKDIR
  É onde vai começar!  Assim que meu container carregar, ele carrega nessa pasta para o npm ser executado nela

Buildando a imagem

docker build -f Dockerfile -t allonsmandy/node .

Agora que temos a imagem pronta (docker images) da pra criar um container com ela

docker run -d -p 8080:3000 allonsmandy/node

docker ps

Subindo sua imagem no Docker Hub para compartilhar sua imagem com outras pessoas

  • Crie sua conta no dockerhub
  • Digite os seguintes comandos no seu terminal:

    • docker login
    • docker push allonsmandy/node (envia a imagem)
    • docker pull allonsmandy/node (baixa a imagem)

Resumo sobre networking

No docker existe uma default networking, quando você cria seus containers por padrão todos eles estão funcionando na mesma rede

hostname -i => ip que o container recebeu e que foi atribuído a ele na rede local (funciona apenas dentro do container)

O docker cria uma rede default e para cada container que for criar, cria novos ips. Dentro da rede eles podem se falar pelos ips.

Na rede default os containers só podem se comunicar utilizando IPS, se eu criar minha própria rede poderei batizar cada container, e a partir disso eles se conectam com o nome.

Criando minha própria rede

docker network create --driver bridge minha-rede
docker network ls
docker run -it --name meu-container-de-ubuntu --network minha-rede ubuntu
docker inspect meu-container-de-ubuntu -> "Networks" atrelado a "minha-rede"
docker run -it --name segundo-ubuntu --network minha-rede ubuntu
ping segundo-ubuntu
ping meu-container-de-ubuntu


Espero que com estas informações e explicações você consiga entender melhor e brincar um pouco com o docker a partir dos exemplos abordados!

Até logo! (◠‿◕✿)

Comentários

Prefere comentar em ânonimo? Siga os seguintes passos:

  • Clique no campo "Nome"
  • Marque os itens necessários, principalmente o último: "Prefiro publicar como um visitante"
  • Adicione um email
  • Agora só enviar seu comentário :)