Skip to content

Relembrando a Ferramenta Docker e sua utilização

Relembrando a Ferramenta Docker e sua utilização

Section titled “Relembrando a Ferramenta Docker e sua utilização”

Vamos relembrar alguns pontos importantes para utilização do Docker e aplicações conteinerizadas.

Problemas comuns no desenvolvimento de soluções de software:

  • Diferentes setup’s de hardware e software;
  • Diferentes conjuntos de requisitos para rodar uma aplicação;
  • Demanda por isolamento entre aplicações;
  • Necessidade de escala de partes da solução.

Quais são as possíveis soluções que temos para esse problema? Podemos utilizar máquinas virtuais! Máquinas Virtuais (VMs) são ambientes computacionais simulados que funcionam como sistemas de computador completos, incluindo CPU, memória, disco e outros recursos. Elas são criadas e executadas em um servidor físico, permitindo que um único hardware execute vários sistemas operacionais e aplicativos independentes.

A virtualização é alcançada através de um software chamado hipervisor ou monitor de máquina virtual, que gerencia e aloca recursos entre as VMs. VMs oferecem isolamento, permitindo a execução segura de aplicativos e sistemas operacionais diferentes em uma mesma máquina física. São úteis para testes, desenvolvimento, consolidação de servidores, backup, recuperação de desastres e fornecimento rápido de ambientes replicáveis. Exemplos de tecnologias de virtualização incluem VMware, Microsoft Hyper-V, KVM (Kernel-based Virtual Machine) e VirtualBox.

A imagem acima apresenta o processo de utilização das máquinas virtuais. Um servidor físico possui um hardware e um sistema operacional próprio. Estes elementos possuem uma característica de ser chamados de elementos de host. Neste servidor, é instalado um software chamado de hypervisor, ele que vai permitir executar os sistemas convidados neste sistema nativo. Cada máquina virtual será criada como uma instância que deve possuir Guest OS e as aplicações que estão sendo executadas dentro dela.

Mas onde entram os containers nessa conversa?

Containers são como cestas de piquinique, tudo que precisamos para comer uma refeição está dentro dela, assim como tudo que precisamos para executar uma aplicação já está dentro dele. Para executar containers, é necessário um Container Engine sendo executado no sistema operacional host.

A partir de agora vamos iniciar a utilização do Docker.

Vamos executar um container de hello-world no docker. No terminal, executar o comando:

Terminal window
docker run hello-world

Vamos obter a seguinte resposta:

Terminal window
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
719385e32844: Pull complete
Digest: sha256:926fac19d22aa2d60f1a276b66a20eb765fbeea2db5dbdaafeb456ad8ce81598
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/

Um detalhe importante é que um container apenas é executado enquanto o processo que ele está executando estiver em execução. Quando o processo termina, o container é encerrado. Para executar um container em modo interativo, é necessário utilizar o comando docker run -it ubuntu bash. Vamos executar este comando no terminal.

Terminal window
docker run -it ubuntu bash

O que vai acontecer agora é que a imagem do Ubuntu será baixada e um container será criado. O terminal será alterado para o terminal do container. Vamos executar o comando ls para listar os arquivos do container.

Terminal window
ls

Repare que os arquivos listados são os arquivos do container. Vamos executar o comando exit para sair do container.

Terminal window
exit

Agora o container foi encerrado e o terminal voltou ao terminal do host. Porque isso aconteceu? Porque o processo que estava sendo executado no container foi encerrado. E qual era o processo? O processo era o terminal bash. Quando o terminal bash foi encerrado, o container foi encerrado.

Agora vamos utilizar algumas opções do comando docker run. Vamos executar o comando docker run -d -p 8080:80 nginx para executar um container do Nginx em modo detached e mapear a porta 8080 do host para a porta 80 do container.

Terminal window
docker run -d -p 8080:80 nginx

Agora o que vai acontecer? Vamos conseguir acessar o Nginx no navegador? Vamos verificar isso! Vamos acessar o endereço http://localhost:8080 no navegador.

Podemos conseguir acessar o Nginx no navegador. O que aconteceu aqui? O comando docker run -d -p 8080:80 nginx foi executado no terminal. O Docker Daemon procurou a imagem do Nginx e não encontrou. Ele baixou a imagem do Nginx do Docker Hub. O Docker Daemon criou um novo container a partir da imagem baixada e mapeou a porta 8080 do host para a porta 80 do container. O Nginx foi executado no container e o Nginx foi acessado no navegador.

Agora vamos parar a execução do container do Nginx. Para parar a execução de um container, é necessário executar o comando docker container stop <CONTAINER_ID>. Vamos parar a execução do container do Nginx.

Terminal window
docker container ls
docker container stop <ID_DO_CONTAINER_NGINX>

Agora vamos executar um container de Nginx, fornecendo uma página HTML para ele. Vamos fazer isso de algumas formas diferentes. Vamos criar um arquivo chamado index.html com o seguinte conteúdo:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Teste de Página HTML</title>
</head>
<body>
<h1>Teste de Página HTML</h1>
<p>Esta é uma página HTML de teste.</p>
</body>
</html>

Vamos executar o comando docker run -d -p 8080:80 -v $(pwd)/index.html:/usr/share/nginx/html/index.html nginx para executar um container do Nginx em modo detached, mapear a porta 8080 do host para a porta 80 do container e fornecer a página HTML para o Nginx.

Terminal window
docker run -d -p 8080:80 -v $(pwd)/index.html:/usr/share/nginx/html/index.html nginx

Observe que quando acessamos o endereço http://localhost:8080 no navegador, a página HTML que fornecemos é exibida. Vamos fazer uma modificação no código HTML e vamos recarregar a página no navegador.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Teste de Página HTML</title>
</head>
<body>
<h1>Teste de Página HTML</h1>
<p>Esta é uma página HTML de teste.</p>
<img src="https://www.icegif.com/wp-content/uploads/2021/11/icegif-110.gif" />
</body>
</html>

Agora, vamos apenas recarregar a página no navegador. O que aconteceu?

Repare que a aplicação não parou de ser executada. O que aconteceu foi que o Nginx recarregou a página HTML que fornecemos. Isso é uma das vantagens de utilizar containers. Mas como isso aconteceu? Nós ligamos um volume do host para o container. O volume é um diretório ou arquivo que é montado no container. Quando o arquivo é modificado no host, o arquivo é modificado no container. Isso é uma das vantagens de utilizar volumes.

Mas Murilo, e quando eu preciso publicar uma imagem em produção?

Em geral, mantemos os volumes de dados separados dos containers. Isso é feito para garantir que os dados sejam mantidos mesmo que o container seja removido. Para publicar uma imagem em produção, é necessário criar um Dockerfile. O Dockerfile é um arquivo que contém as instruções para criar uma imagem. O Dockerfile contém as instruções para instalar as dependências, copiar os arquivos, configurar o ambiente, entre outras coisas. O Dockerfile é utilizado para criar uma imagem que pode ser executada em produção.

Vamos criar um Dockerfile para publicar a página HTML em produção. Vamos criar um arquivo chamado Dockerfile com o seguinte conteúdo:

FROM nginx:latest
COPY index.html /usr/share/nginx/html/index.html

Agora vamos construir uma imagem a partir do Dockerfile. Para construir uma imagem a partir de um Dockerfile, é necessário executar o comando docker build -t <NOME_DA_IMAGEM> .. Vamos construir uma imagem a partir do Dockerfile.

Terminal window
docker build -t meu-nginx .

Agora vamos executar um container a partir da imagem que construímos. Vamos executar o comando docker run -d -p 8080:80 meu-nginx para executar um container a partir da imagem que construímos.

Terminal window
docker run -d -p 8080:80 meu-nginx

Agora vamos acessar o endereço http://localhost:8080 no navegador. A página HTML que fornecemos é exibida. A imagem que construímos pode ser utilizada em produção. Quando alguma modificação é feita no código HTML, é necessário construir uma nova imagem e executar um novo container.

Legal até aqui avançamos bastante no desenvolvimento utilizando Docker. Vamos continuar estudando mais sobre Docker e containers.


Em diversas aplicações que vamos desenvolver, vai ser necessário utilizar mais de um container. Para facilitar a execução de múltiplos containers, o Docker Compose é uma ferramenta que permite definir e executar aplicações multi-container.

O Docker Compose é utilizado para definir os serviços, as redes e os volumes que compõem a aplicação. O Docker Compose é utilizado para executar os containers da aplicação.

Vamos lançar um serviço que utiliza o Postgres e o Adminer. Para isso, vamos criar um arquivo chamado docker-compose.yml com o seguinte conteúdo:

# Use postgres/example user/password credentials
version: '3.9'
services:
db:
image: postgres
restart: always
# set shared memory limit when using docker-compose
shm_size: 128mb
# or set shared memory limit when deploy via swarm stack
#volumes:
# - type: tmpfs
# target: /dev/shm
# tmpfs:
# size: 134217728 # 128*2^20 bytes = 128Mb
environment:
POSTGRES_PASSWORD: example
adminer:
image: adminer
restart: always
ports:
- 8080:8080

Para executar os containers, é necessário executar o comando docker-compose up -d. Vamos executar este comando no terminal.

Terminal window
docker-compose up -d

Agora vamos acessar o endereço http://localhost:8080 no navegador. O Adminer é exibido. O Adminer é um cliente de banco de dados que permite gerenciar os bancos de dados.

Agora como temos o acesso ao Adminer. O uusário padrão da imagem do Postgres é postgres e a senha é example. Vamos acessar o Adminer e vamos preencher os campos de conexão com o banco de dados.

Agora vamos verificar se conseguimos acessar o banco de dados utilizando uma aplicação externa. Podemos utilizar o DBeaver para acessar o banco de dados. Vamos acessar o DBeaver e vamos criar uma nova conexão com o banco de dados.

Vamos obter o seguinte erro: `Conexão recusada’.

Mas Murilo isso não faz sentido! O que aconteceu aqui? Nós estamos conectados com o banco de dados no Adminer, mas não conseguimos conectar com o banco de dados no DBeaver. O que aconteceu?

O que aconteceu foi que o banco de dados está sendo executado em um container. O banco de dados está sendo executado em um container e a porta 5432 do host não está mapeada para a porta 5432 do container. Para acessar o banco de dados no DBeaver, é necessário mapear a porta 5432 do host para a porta 5432 do container. Vamos fazer isso alterando o nosso arquivo docker-compose.yml.

# Use postgres/example user/password credentials
version: '3.9'
services:
db:
image: postgres
restart: always
# set shared memory limit when using docker-compose
shm_size: 128mb
# or set shared memory limit when deploy via swarm stack
#volumes:
# - type: tmpfs
# target: /dev/shm
# tmpfs:
# size: 134217728 # 128*2^20 bytes = 128Mb
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: example
adminer:
image: adminer
restart: always
ports:
- 8080:8080

Vamos derrubar os containers e subir novamente.

Terminal window
docker-compose down
docker-compose up -d

Agora conseguimos acessar o banco de dados no DBeaver. O que aconteceu aqui foi que a porta 5432 do host foi mapeada para a porta 5432 do container.

Pessoal até aqui avançamos bastante no desenvolvimento utilizando Docker. Vamos continuar estudando mais sobre Docker e containers, mas agora limitando os recursos disponíveis para nossa aplicação.

Primeiro vamos criar uma aplicação em Flask que conecta com nosso banco de dados Postgres. Nossa aplicação vai ser simples, ela vai enviar os dados que estamos recebendo para o banco e devolver todos os dados que estão no banco. Portanto ela possui apenas duas rotas.

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:example@db:5432/postgres'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_USERNAME'] = 'postgres'
app.config['SQLALCHEMY_PASSWORD'] = 'example'
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
created_at = db.Column(db.DateTime, server_default=db.func.now())
def __repr__(self):
return '<User %r>' % self.name
@app.route('/users', methods=['POST'])
def create_user():
name = request.json['name']
user = User(name=name)
db.session.add(user)
db.session.commit()
return jsonify({'message': 'User created'}), 201
@app.route('/users', methods=['GET'])
def get_users():
users = User.query.all()
return jsonify([{'name': user.name, 'created_at': user.created_at} for user in users]), 200
if __name__ == '__main__':
app.app_context().push()
db.create_all()
app.run(host='0.0.0.0', port=5000, threaded=False)

Vamos primeiro compreender o que nossa aplicação está fazendo:

  1. Estamos criando uma aplicação em Flask;
  2. Estamos configurando a conexão com o banco de dados Postgres;
  3. Configuramos a rota para conectar com o banco de dados e criar um usuário;
  4. Configuramos o SQLAlchemy para não rastrear as modificações;
  5. Estamos criando uma tabela User no banco de dados;
  6. Estamos criando duas rotas: uma para criar um usuário e outra para listar todos os usuários;
  7. Estamos criando um usuário no banco de dados quando a rota POST /users é acessada;
  8. Estamos listando todos os usuários do banco de dados quando a rota GET /users é acessada.

Para nossa aplicação ser executada, é necessário instalar as dependências. Para instalar as dependências, é necessário criar um arquivo chamado requirements.txt com o seguinte conteúdo:

Flask==2.2
Werkzeug==2.2.2
Flask-SQLAlchemy==3.0.3
psycopg2-binary==2.9.1

Agora vamos criar um arquivo chamado Dockerfile com o seguinte conteúdo:

FROM python:3.9-slim-buster
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]

Agora vamos construir o arquivo docker-compose.yml com o seguinte conteúdo:

version: '3.9'
services:
db:
image: postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: example
app:
build: .
restart: always
ports:
- "5000:5000"
depends_on:
- db

Vamos agora iniciar o processo.

Terminal window
docker-compose up

Pessoal deixamos o console preso a aplicação para conseguirmos ver o que está acontecendo. Vamos utilizar o Insomnia para testar nossa aplicação. Vamos criar um novo usuário utilizando a rota POST /users. Podemos verificar o resultado da requisição e a inserção do usuário no banco de dados, verificando a rota GET /users.

Agora vamos estrangular nossa aplicação para ver o comportamento do sistema. Vamos limitar a quantidade de memória e cpu disponível para a aplicação. Vamos alterar o arquivo docker-compose.yml para limitar a quantidade de memória e cpu disponível para a aplicação.

version: '3.9'
services:
db:
image: postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: example
resources:
limits:
# Segura que esse limite não está funcionando no nosso note
#cpus: '0.5'
memory: 128M
app:
build: .
restart: always
ports:
- "5000:5000"
depends_on:
- db
deploy:
resources:
limits:
# Segura que esse limite não está funcionando no nosso note
#cpus: '0.5'
memory: 128M

Agora vamos reiniciar os containers.

Terminal window
docker-compose down
docker-compose up