Introdução ao Docker

Conteúdos

Introdução

À esse ponto, você tem todas as ferramentas necessárias para construir web apps simples e completos. Hoje, iremos trabalhar com uma ferramenta essencial em engenharia de software, conhecida como Docker.

Ícone de definição

Definição

“O Docker é um conjunto de produtos que utiliza virtualização em nível de sistema operacional para distribuir software em pacotes chamados contêineres. O Docker automatiza a implantação de aplicativos em contêineres leves, permitindo que eles sejam executados de forma consistente em diferentes ambientes de computação.”

Fonte: Wikipedia

Como analogia, podemos pensar no transporte de produtos antes dos anos 60. Nessa época, os itens era carregados e descarregados de forma manual de um meio de transporte para outro, em diferentes pacotes, diferentes tamanhos, ou mesmo de forma solta. E então, contêineres de transporte foram criados, o que fez com que o carregamento de mercadorias e itens entre navios, trens e caminhões se tornasse mais fácil. A ideia do Docker é a mesma (note a logo de Docker, uma baleia com contêineres): simplificar o envio e implantação de códigos, apps e etc, com uso consistente em qualquer plataforma.

Quase qualquer plataforma... desde que esteja rodando um Kernel Linux.

No mundo de desenvolvimento de software, em times de múltiplos desenvolvedores, problemas podem ocorrer devido à configuração da máquina de uma pessoa ser diferente da máquina de outra pessoa - geralmente ligado à clássica frase "mas funciona na minha máquina"-. Como exemplo, você pode ter uma versão diferente de Python, ou pacotes adicionais (sistema de banco de dados, servidor proxy, etc), que permitem a aplicação rodar tranquilamente no seu computador, enquanto em outras máquinas irá falhar.

Para evitar esses problemas, o Docker surge como uma ferramenta que permite desenvolvedores a isolar as aplicações em uma sandbox (chamados **containers** ou **contêineres**) que rodam no sistema hospedeiro. Uma vantagem do Docker é que nos permite empacotar a aplicação com todas as suas dependências em uma unidade padronizada, para desenvolvimento e envio. Ao contrário de máquinas virtuais, containers não possuem uma sobrecarga alta, isto é, são extremamente leves em comparação à máquinas virtuais enquanto "servem" o mesmo propósito em algumas coisas, o que torna o uso do sistema e dos recursos mais eficiente.

Containers

Como dito, o método para isolar aplicações era de se usar VMs. Em contrapartida, o custo desse método é a sobrecarga computacional para virtualização de hardware que possibilita a existência do SO visitante.

Os containers, em comparação, possuem uma abordagem diferente. São **processos de sistema** (como qualquer outro processo) que recebem algumas configurações especiais. Alguns desses mecanismos são do Kernel Linux, como:

Com esses mecanismos e alguns outros, os containers operam dentro de limites bem-definidos. E como esses processos estão isolados do sistema por meio de OS-level virtualization, cria-se uma ilusão de que a aplicação está rodando em um sistema totalmente independente, embora compartilhe os recursos com o sistema hospedeiro.

A vantagem de ter um ambiente isolado do sistema é que, além de ser possível compartilhar seu aplicativo/ambiente de desenvolvimento com outras pessoas - evitando o clássico erro de "funciona na minha máquina..." - também possibilita o deployment dessas aplicações de forma mais fácil e consistente, independente se o ambiente alvo é um data center privado, um servidor em cloud, ou mesmo o notebook do seu colega.

Ícone de aviso

Observação

Os containers não são exclusivos ao Docker, e nem criados pelo Docker. O propósito do Docker é ser uma interface simples para o gerenciamento desses containers. Outras alternativas existem, como podman, cri-o, etc.

Imagens

Vendo que os containers são processos isolados, como exatamente definimos os pacotes, arquivos e binários para usar nele? Como compartilhamos esses ambientes?

É aqui que as imagens de container se tornam essenciais. Uma imagem de container é como um blueprint, um pacote padronizado que inclui os arquivos, binários, bibliotecas e configurações para rodar um container. Como exemplo, uma imagem de um web app Python irá incluir o ambiente de runtime Python, o código do web app, e suas dependências/bibliotecas. Há alguns conceitos importantes sobre imagens:

Ao desenvolver projetos, interagimos com imagens de duas formas: Usando imagens prontas online ou criando imagens.

Com essa base em mente, vamos colocar em prática o uso de Docker e containerização com o nosso aplicativo!

Construindo e rodando nosso primeiro container com Dockerfile

Antes de mais nada, tenha o Docker instalado em seu sistema. Na documentação oficial do Docker temos instruções de como instalar a depender do seu OS, método de instalação preferido, entre outras coisas. Tenha em mente que essa aula irá usar Docker via terminal, então caso tenha interesse no Docker Desktop, terá que aprender por fora.

Além do Docker, será necessário baixar o docker-compose e, possivelmente, o docker-buildx. O primeiro é uma ferramenta para gerenciamento de aplicações multi-container, enquanto o segundo é um motor substituto para a construção de imagens de container.

Na raiz de seu projeto Python, iremos criar o arquivo Docker, nomeado de Dockerfile. Nesse arquivo, iremos providenciar as instruções de como criar uma imagem e quais arquivos, bibliotecas a incluir no container. Aqui está um exemplo de como o arquivo se parece:

FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONBUFFERED=1

WORKDIR /code

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . ./

CMD ["fastapi", "dev", "main.py", "--host", "0.0.0.0"]

Vamos ver o que cada um desses comandos acima faz:

Ícone de aviso

Observação

Copiar o requirements.txt antes do projeto em si nos traz algumas otimizações relacionadas à Layer caching. Se mudarmos o código fonte main.py, as dependências não terão que ser reinstaladas na imagem, graças à esse caching por camadas! Também existem outras otimizações, sinta-se livre para pesquisar pela documentação do Docker.

Agora que possuimos essa imagem, podemos construir nossa imagem. O comando utilizado para criar a imagem é o docker build, que se baseia em um Dockerfile. Para criar a imagem, rode na raiz do projeto:

>> docker build -t app .

Isso irá iniciar o processo de construção da imagem. A flag -t é usada para nomear a imagem.

Rodando docker images, podemos ver nossa imagem listada:

Finalmente, para iniciar nosso app em um container baseado na imagem construída, utilizamos o docker run:

>> docker run --name app_container -p 8000:8000 app:latest

Note que, acessando http://127.0.0.1:8000/, acessamos o site através da porta 8000 do container, que está exposta.

Introdução ao Docker Compose

Ícone de aviso

Observação

A seção a seguir aborda alguns conceitos e programas que não foram e nem serão cobrados nesta fase da disciplina, como PostgreSQL, Redis, etc. Serve mais como uma parte expositiva, e como uma preparação para um dos exercícios.

Mesmo que nosso web app esteja quase pronto, em ambientes reais, ele nunca será "apenas" um script em python. Geralmente, precisa de:

O problema clássico "funciona na minha máquina" também poderia ocorrer com esses sistemas, o que nos leva à querer containerizar os mesmos. Ainda assim, parece meio ineficiente obter várias imagens para essas aplicações, criar uma rede compartilhada que conecte os containers de forma manual, e lembrar das variáveis de ambiente toda vez que iniciarmos o desenvolvimento no projeto. Para isso, temos uma ferramenta adicional que irá nos ajudar: Docker Compose.

Ícone de definição

Definição

“O Docker Compose é uma ferramenta para definir e executar aplicações com múltiplos contêineres. É a chave para uma experiência de desenvolvimento e implantação simplificada e eficiente. O Compose simplifica o controle de aplicações, facilitando o gerenciamento de serviços, redes e volumes em um único arquivo de configuração YAML. Em seguida, com um único comando, você cria e inicia todos os serviços a partir do seu arquivo de configuração.”

Fonte: Docker Docs

Note que em um app completo, é intuitivo precisar de vários containers para rodar a aplicação, então, ao invés de rodar vários comandos no terminal para poder conectar esses containers, utilizamos um arquivo YAML chamado docker-compose.yml que permitirá a configuração de todos os serviços.

Para um exemplo um pouco mais complexo, considere o seguinte main.py em FastAPI:

from fastapi import FastAPI
from redis import Redis
import os

app = FastAPI()

redis_host = os.getenv("REDIS_HOST", "localhost")
cache = Redis(host=redis_host, port=6379, decode_responses=True)

@app.get("/")
def index():
    hits = cache.incr("visitor_count")
    return {
        "message": "Olá visitante!",
        "total_visitors": hits
    }

Para o Dockerfile, não iremos mudar nada, o que definimos anteriormente funcionará bem.

Finalmente, para o docker-compose.yml, iremos definir dois serviços: um para o nosso código, e um para uma imagem Redis:

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - REDIS_HOST=redis
    depends_on:
      - redis

  redis:
    image: redis:7-alpine

Por fim, para rodar o sistema por completo, usamos o comando docker compose up. Isso irá fazer a etapa de build da imagem, além de configurar os containers corretamente:

"Exemplos do sistema rodando"

E ao acessar o site, podemos ver a variável total_visitors incrementando à cada vez que visitamos, significando que os containers estão se comunicando corretamente.

Para desconectar dos logs do Docker Compose, aperte a tecla d no terminal, ou, alternativamente, adicione a flag -d ao rodar docker compose up para voltar ao seu terminal após o início do app. Para parar os containers, utilize docker compose down na raiz do projeto.

No dia-a-dia, são utilizados vários outros comandos além de docker run, docker build ou docker compose up. Alguns dos mais úteis são:

Há muito mais a ser explorado com Docker, Docker Compose e as ferramentas correlatas, principalmente quando utilizado em conjunto com banco de dados como PostgreSQL, MySQL, entre outros, ou serviços/aplicações diferentes. Ainda assim, esperamos que essa aula tenha servido bem para entender melhor o que é essa ferramenta, e os conceitos por trás dela :)

Desafios proposto

  1. Containerize sua aplicação Python com FastAPI, criando um Dockerfile. Caso seu projeto utilize programas externos como Redis, ou um banco de dados diferente do visto em aula, configure um arquivo docker-compose.yml para comunicação entre os serviços.
  2. Baseando-se no main.py proposto nesta aula, copie o seguinte Dockerfile
    FROM python:latest
    RUN apt-get update && apt-get install -y gcc git vim
    
    COPY . /app
    WORKDIR /app
    
    RUN pip install -r requirements.txt
    
    ENV REDIS_HOST=localhost
    
    CMD fastapi dev main.py --port 8000